diff --git a/Gemfile b/Gemfile index 1423e58f7..a24fd281c 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,7 @@ gem 'p3p', '~> 2.0' gem 'panoptes-client' gem 'pg', '~> 1.4' gem 'pg_search' -gem 'puma', '~> 6.1.1' +gem 'puma', '~> 6.3.1' gem 'pundit', '~> 2.3.0' gem 'rack-cors', '~> 1.0', require: 'rack/cors' if next? diff --git a/Gemfile.lock b/Gemfile.lock index 209edae61..a8d7285a0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -119,11 +119,11 @@ GEM congestion (0.1.0) connection_pool (>= 2.0) redis (>= 3.1) - connection_pool (2.4.0) + connection_pool (2.4.1) crack (0.4.5) rexml crass (1.0.6) - dalli (3.2.4) + dalli (3.2.5) database_cleaner (1.99.0) date (3.3.3) deep_cloneable (3.2.0) @@ -225,7 +225,7 @@ GEM httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.13.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) jmespath (1.6.1) jquery-rails (4.5.1) @@ -255,14 +255,14 @@ GEM listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - lograge (0.12.0) + lograge (0.13.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.20.0) + loofah (2.21.3) crass (~> 1.0.2) - nokogiri (>= 1.5.9) + nokogiri (>= 1.12.0) lumberjack (1.2.8) mail (2.8.1) mini_mime (>= 0.1.1) @@ -275,10 +275,10 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) mini_mime (1.1.2) - mini_portile2 (2.8.1) + mini_portile2 (2.8.4) mini_racer (0.6.3) libv8-node (~> 16.10.0.0) - minitest (5.18.0) + minitest (5.19.0) mock_redis (0.36.0) ruby2_keywords multi_json (1.15.0) @@ -297,10 +297,10 @@ GEM net-smtp (0.3.3) net-protocol netrc (0.11.0) - newrelic_rpm (9.1.0) - nio4r (2.5.8) - nokogiri (1.14.3) - mini_portile2 (~> 2.8.0) + newrelic_rpm (9.3.1) + nio4r (2.5.9) + nokogiri (1.15.3) + mini_portile2 (~> 2.8.2) racc (~> 1.4) notiffany (0.1.3) nenv (~> 0.1) @@ -342,13 +342,13 @@ GEM coderay (~> 1.1) method_source (~> 1.0) public_suffix (4.0.7) - puma (6.1.1) + puma (6.3.1) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.6.2) - rack (2.2.7) + racc (1.7.1) + rack (2.2.8) rack-cors (1.1.1) rack (>= 2.0.0) rack-protection (3.0.5) @@ -370,11 +370,13 @@ GEM bundler (>= 1.15.0) railties (= 6.1.7.3) sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rails-dom-testing (2.1.1) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.5.0) - loofah (~> 2.19, >= 2.19.1) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) railties (6.1.7.3) actionpack (= 6.1.7.3) activesupport (= 6.1.7.3) @@ -405,25 +407,25 @@ GEM rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) rspec-mocks (~> 3.12.0) - rspec-core (3.12.0) + rspec-core (3.12.2) rspec-support (~> 3.12.0) - rspec-expectations (3.12.0) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-its (1.3.0) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.12.0) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.0.1) + rspec-rails (6.0.3) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) - rspec-core (~> 3.11) - rspec-expectations (~> 3.11) - rspec-mocks (~> 3.11) - rspec-support (~> 3.11) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) rspec-support (3.12.0) rubocop (0.91.1) parallel (~> 1.10) @@ -448,12 +450,12 @@ GEM rubocop-ast (>= 0.7.1) ruby-progressbar (1.11.0) ruby2_keywords (0.0.5) - sanitize (6.0.1) + sanitize (6.0.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) scientist (1.6.4) shellany (0.0.1) - sidekiq (6.5.8) + sidekiq (6.5.9) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) @@ -482,7 +484,7 @@ GEM sprockets (>= 3.0.0) standby (4.0.0) activerecord (>= 3.0.0) - stringex (2.8.5) + stringex (2.8.6) strong_migrations (1.4.4) activerecord (>= 5.2) ten_years_rails (0.2.0) @@ -490,7 +492,7 @@ GEM activesupport colorize (>= 0.8.1) rest-client (>= 2.0.2) - thor (1.2.1) + thor (1.2.2) timeout (0.3.2) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -514,7 +516,7 @@ GEM websocket-extensions (0.1.5) yard (0.9.27) webrick (~> 1.7.0) - zeitwerk (2.6.8) + zeitwerk (2.6.10) zoo_stream (1.0.1) aws-sdk @@ -563,7 +565,7 @@ DEPENDENCIES pg (~> 1.4) pg_search pry - puma (~> 6.1.1) + puma (~> 6.3.1) pundit (~> 2.3.0) rack-cors (~> 1.0) rails (~> 6.1) diff --git a/app/controllers/api/v1/project_preferences_controller.rb b/app/controllers/api/v1/project_preferences_controller.rb index fd3a65586..8a0b49b24 100644 --- a/app/controllers/api/v1/project_preferences_controller.rb +++ b/app/controllers/api/v1/project_preferences_controller.rb @@ -22,9 +22,11 @@ def find_upp_for_update_settings user_id: params_for[:user_id], project_id: params_for[:project_id] ) - unless @upp.project.owners_and_collaborators.include?(api_user.user) - raise Api::Unauthorized.new("You must be the project owner") - end + raise Api::Unauthorized, 'You must be the project owner or a collaborator' unless user_allowed? + end + + def user_allowed? + @upp.project.owners_and_collaborators.include?(api_user.user) || api_user.is_admin? end def update_settings_response diff --git a/app/controllers/api/v1/user_groups_controller.rb b/app/controllers/api/v1/user_groups_controller.rb index 08a3dc48e..1234d486b 100644 --- a/app/controllers/api/v1/user_groups_controller.rb +++ b/app/controllers/api/v1/user_groups_controller.rb @@ -9,8 +9,8 @@ class Api::V1::UserGroupsController < Api::ApiController alias_method :user_group, :controlled_resource - allowed_params :create, :name, :display_name, links: [ users: [] ] - allowed_params :update, :name, :display_name + allowed_params :create, :name, :display_name, :stats_visibility, links: [users: []] + allowed_params :update, :name, :stats_visibility, :display_name search_by do |name, query| query.search_name(name.join(" ")) diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb new file mode 100644 index 000000000..d45ccbaa5 --- /dev/null +++ b/app/controllers/confirmations_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ConfirmationsController < Devise::ConfirmationsController + protected + + def after_confirmation_path_for(resource_name, resource) + 'https://www.zooniverse.org' + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 73dc7850b..260d89943 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -48,7 +48,7 @@ class Project < ApplicationRecord has_many :project_versions, dependent: :destroy - versioned association: :project_versions, attributes: %w(private live beta_requested beta_approved launch_requested launch_approved display_name description workflow_description introduction url_labels researcher_quote) + versioned association: :project_versions, attributes: %w[private live beta_requested beta_approved launch_requested launch_approved display_name description workflow_description introduction url_labels researcher_quote] enum state: [:paused, :finished] @@ -82,7 +82,7 @@ class Project < ApplicationRecord ranks :beta_row_order def self.translatable_attributes - %i(display_name title description workflow_description introduction researcher_quote url_labels) + %i[display_name title description workflow_description introduction researcher_quote url_labels] end def available_languages @@ -102,7 +102,7 @@ def expert_classifier?(classifier) end def owners_and_collaborators - users_with_project_roles(%w(owner collaborator)).select(:id) + users_with_project_roles(%w[owner collaborator]).select(:id) end def create_talk_admin(client) @@ -166,6 +166,6 @@ def users_with_project_roles(roles) end def communication_emails - users_with_project_roles(%w(owner communications)).pluck(:email) + users_with_project_roles(%w[owner collaborator communications]).pluck(:email) end end diff --git a/app/models/user.rb b/app/models/user.rb index 28b5bad67..384064897 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,7 +13,7 @@ class User < ApplicationRecord attr_accessor :minor_age devise :database_authenticatable, :registerable, - :recoverable, :rememberable, :trackable, :validatable, + :recoverable, :rememberable, :trackable, :validatable, :confirmable, :omniauthable, omniauth_providers: [:facebook, :google_oauth2] has_many :classifications, dependent: :restrict_with_exception @@ -56,7 +56,6 @@ class User < ApplicationRecord validates_with IdentityGroupNameValidator after_create :set_zooniverse_id - after_create :send_welcome_email, unless: :migrated before_create :set_ouroboros_api_key delegate :projects, to: :identity_group @@ -64,7 +63,6 @@ class User < ApplicationRecord delegate :subjects, to: :identity_group delegate :owns?, to: :identity_group - pg_search_scope :search_name, against: [:login], using: { @@ -204,6 +202,10 @@ def self.find_by_lower_login(login) find_by("lower(login) = ?", login.downcase) end + def after_confirmation + send_welcome_email + end + def subject_limit super || Panoptes.max_subjects end diff --git a/app/models/user_group.rb b/app/models/user_group.rb index 7e16fd173..a9a81882b 100644 --- a/app/models/user_group.rb +++ b/app/models/user_group.rb @@ -19,6 +19,31 @@ class UserGroup < ApplicationRecord has_many :collections, through: :owned_resources, source: :resource, source_type: "Collection" + ## + # Stats_Visibility Levels (Used for ERAS stats service) + # private_agg_only (default): Only members of a user group can view aggregate stats. Individual stats only viewable by only admins of the user group + # + # private_show_agg_and_ind: Only members of a user group can view aggregate stats. Individual stats is viewable by BOTH members and admins of the user group. + # + # public_agg_only: Anyone can view aggregate stats of the user group. Only admins of the user group can view individual stats. + # + # public_agg_show_ind_if_member: Anyone can view aggregate stats of the user group. Members and admins of the user group can view individual stats. + # + # public_show_all: Anyone can view aggregate stats of the user group and can view individual stats of the user group. + ## + STATS_VISIBILITY_LEVELS = { + private_agg_only: 0, + private_show_agg_and_ind: 1, + public_agg_only: 2, + public_agg_show_ind_if_member: 3, + public_show_all: 4 + }.freeze + enum stats_visibility: STATS_VISIBILITY_LEVELS + + validate do + errors.add(:stats_visibility, "Not valid stats_visibility type, please select from the list: #{STATS_VISIBILITY_LEVELS.keys}") if @invalid_stats_visibility + end + validates :display_name, presence: true validates :name, presence: true, uniqueness: { case_sensitive: false }, @@ -66,6 +91,14 @@ def verify_join_token(token_to_verify) join_token.present? && join_token == token_to_verify end + def stats_visibility=(value) + if STATS_VISIBILITY_LEVELS.stringify_keys.keys.exclude?(value) && STATS_VISIBILITY_LEVELS.values.exclude?(value) + @invalid_stats_visibility = true + else + super value + end + end + private def default_display_name diff --git a/app/serializers/user_group_serializer.rb b/app/serializers/user_group_serializer.rb index 33e628d77..d46f9bdd8 100644 --- a/app/serializers/user_group_serializer.rb +++ b/app/serializers/user_group_serializer.rb @@ -3,7 +3,7 @@ class UserGroupSerializer include RecentLinkSerializer include CachedSerializer - attributes :id, :name, :display_name, :classifications_count, :created_at, :updated_at, :type, :href, :join_token + attributes :id, :name, :display_name, :classifications_count, :created_at, :updated_at, :type, :href, :join_token, :stats_visibility can_include :memberships, :users, projects: { param: "owner", value: "name" }, collections: { param: "owner", value: "name" } diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index ee7e6045d..e3339fc8b 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -8,7 +8,8 @@ config.strip_whitespace_keys = [ :email ] config.skip_session_storage = [:http_auth] config.stretches = Rails.env.test? ? 1 : 10 - config.reconfirmable = true + config.reconfirmable = false + config.allow_unconfirmed_access_for = nil config.password_length = 8..128 config.reset_password_within = 6.hours config.paranoid = true diff --git a/config/routes.rb b/config/routes.rb index d38f662ba..40178522a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -21,7 +21,7 @@ end devise_for :users, - controllers: { omniauth_callbacks: 'omniauth_callbacks', passwords: 'passwords' }, + controllers: { confirmations: 'confirmations', omniauth_callbacks: 'omniauth_callbacks', passwords: 'passwords' }, skip: [ :sessions, :registrations ] as :user do diff --git a/db/migrate/20230613165746_add_stats_visibility_to_user_groups.rb b/db/migrate/20230613165746_add_stats_visibility_to_user_groups.rb new file mode 100644 index 000000000..e61565665 --- /dev/null +++ b/db/migrate/20230613165746_add_stats_visibility_to_user_groups.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddStatsVisibilityToUserGroups < ActiveRecord::Migration[6.1] + def change + add_column :user_groups, :stats_visibility, :integer + # defaulting to private_agg_only stats_visibility view (where members can view aggregate stats but only admins can view detailed stats) + change_column_default :user_groups, :stats_visibility, from: nil, to: 0 + end +end diff --git a/db/migrate/20231025200957_add_confirmable_to_devise.rb b/db/migrate/20231025200957_add_confirmable_to_devise.rb new file mode 100644 index 000000000..2c05aeb42 --- /dev/null +++ b/db/migrate/20231025200957_add_confirmable_to_devise.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AddConfirmableToDevise < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_column :users, :confirmation_token, :string + add_column :users, :confirmed_at, :datetime + add_column :users, :confirmation_sent_at, :datetime + + add_index :users, :confirmation_token, unique: true, algorithm: :concurrently + end + + def down + remove_column :users, :confirmation_token + remove_column :users, :confirmed_at + remove_column :users, :confirmation_sent_at + + remove_index :users, :confirmation_token + end +end diff --git a/db/structure.sql b/db/structure.sql index 2115ddc5c..0c3e5dc97 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,10 +1,3 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 11.15 (Debian 11.15-1.pgdg90+1) --- Dumped by pg_dump version 11.15 - SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; @@ -60,8 +53,6 @@ COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching SET default_tablespace = ''; -SET default_with_oids = false; - -- -- Name: access_control_lists; Type: TABLE; Schema: public; Owner: - -- @@ -1671,7 +1662,8 @@ CREATE TABLE public.user_groups ( display_name character varying, private boolean DEFAULT true NOT NULL, lock_version integer DEFAULT 0, - join_token character varying + join_token character varying, + stats_visibility integer DEFAULT 0 ); @@ -1808,7 +1800,10 @@ CREATE TABLE public.users ( upload_whitelist boolean DEFAULT false NOT NULL, ux_testing_email_communication boolean DEFAULT false, intervention_notifications boolean DEFAULT true, - nasa_email_communication boolean DEFAULT false + nasa_email_communication boolean DEFAULT false, + confirmation_token character varying, + confirmed_at timestamp without time zone, + confirmation_sent_at timestamp without time zone ); @@ -3574,6 +3569,13 @@ CREATE INDEX index_users_on_activated_state ON public.users USING btree (activat CREATE INDEX index_users_on_beta_email_communication ON public.users USING btree (beta_email_communication) WHERE (beta_email_communication = true); +-- +-- Name: index_users_on_confirmation_token; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_users_on_confirmation_token ON public.users USING btree (confirmation_token); + + -- -- Name: index_users_on_display_name; Type: INDEX; Schema: public; Owner: - -- @@ -4581,6 +4583,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20211007125705'), ('20211124175756'), ('20211201164326'), -('20221018032140'); +('20221018032140'), +('20230613165746'), +('20231025200957'); diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index aaf4242ca..5c5e8b916 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - activesupport (6.1.7.3) + activesupport (6.1.7.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -81,7 +81,7 @@ GEM middleman-core (>= 3.2) rouge (~> 2.0) mini_portile2 (2.8.1) - minitest (5.18.0) + minitest (5.19.0) nokogiri (1.14.3) mini_portile2 (~> 2.8.0) racc (~> 1.4) @@ -117,7 +117,7 @@ GEM uglifier (3.2.0) execjs (>= 0.3.0, < 3) webrick (1.7.0) - zeitwerk (2.6.7) + zeitwerk (2.6.11) PLATFORMS ruby diff --git a/kubernetes/db-migrate-production.tmpl b/kubernetes/db-migrate-production.tmpl index c811dfb6b..6897c79e1 100644 --- a/kubernetes/db-migrate-production.tmpl +++ b/kubernetes/db-migrate-production.tmpl @@ -9,6 +9,9 @@ spec: - name: panoptes-migrate-db-production image: ghcr.io/zooniverse/panoptes:__IMAGE_TAG__ command: ["/rails_app/migrate.sh"] + env: + - name: PG_STATEMENT_TIMEOUT + value: '0' envFrom: - secretRef: name: panoptes-common-env-vars diff --git a/kubernetes/db-migrate-staging.tmpl b/kubernetes/db-migrate-staging.tmpl index 3aa4d856f..8d5840f48 100644 --- a/kubernetes/db-migrate-staging.tmpl +++ b/kubernetes/db-migrate-staging.tmpl @@ -9,6 +9,9 @@ spec: - name: panoptes-migrate-db-staging image: ghcr.io/zooniverse/panoptes:__IMAGE_TAG__ command: ["/rails_app/migrate.sh"] + env: + - name: PG_STATEMENT_TIMEOUT + value: '0' envFrom: - secretRef: name: panoptes-common-env-vars diff --git a/kubernetes/job-task-production.tmpl b/kubernetes/job-task-production.tmpl index 7f0f6851d..6f7a71cb2 100644 --- a/kubernetes/job-task-production.tmpl +++ b/kubernetes/job-task-production.tmpl @@ -9,6 +9,9 @@ spec: - name: panoptes-rake-task-production image: ghcr.io/zooniverse/panoptes:__IMAGE_TAG__ command: ["bundle", "exec", "rake", __RAKE_TASK_NAME__] + env: + - name: PG_STATEMENT_TIMEOUT + value: '0' envFrom: - secretRef: name: panoptes-common-env-vars diff --git a/kubernetes/job-task-staging.tmpl b/kubernetes/job-task-staging.tmpl index 1efdd477f..5388ffc90 100644 --- a/kubernetes/job-task-staging.tmpl +++ b/kubernetes/job-task-staging.tmpl @@ -9,6 +9,9 @@ spec: - name: panoptes-rake-task-staging image: ghcr.io/zooniverse/panoptes:__IMAGE_TAG__ command: ["bundle", "exec", "rake", __RAKE_TASK_NAME__] + env: + - name: PG_STATEMENT_TIMEOUT + value: '0' envFrom: - secretRef: name: panoptes-common-env-vars diff --git a/lib/project_copier.rb b/lib/project_copier.rb index 11c8f254a..a4bbc5ed0 100644 --- a/lib/project_copier.rb +++ b/lib/project_copier.rb @@ -4,13 +4,13 @@ class ProjectCopier EXCLUDE_ATTRIBUTES = %i[classifications_count launched_row_order beta_row_order].freeze INCLUDE_ASSOCIATIONS = [ :tutorials, - :field_guides, :pages, :tags, :tagged_resources, :avatar, :background, - { active_workflows: %i[tutorials attached_images] } + { active_workflows: %i[tutorials attached_images] }, + { field_guides: %i[attached_images] } ].freeze def initialize(project_id, user_id) @@ -22,7 +22,7 @@ def copy # Should this all be wrapped in a transaction? # to ensure we rollback an sub resource creations, # e.g. inband primary lang strings for the associated resources.... - Project.transaction(requires_new: true) do + copied_project = Project.transaction(requires_new: true) do copied_project = copy_project # save the project and create the project versions for use in translation strings @@ -35,9 +35,14 @@ def copy # to keep the translations system working with these copied resources setup_associated_primary_language_translations(copied_project) - # return the newly copied project copied_project end + + # Creates Talk roles via background worker + TalkAdminCreateWorker.perform_async(copied_project.id) + + # return the newly copied project + copied_project end private diff --git a/lib/tasks/user.rake b/lib/tasks/user.rake index 6e8f5ce87..ab6be1f67 100644 --- a/lib/tasks/user.rake +++ b/lib/tasks/user.rake @@ -48,6 +48,18 @@ namespace :user do end end + desc 'Backfill confirmed_at in batches (restartable)' + task backfill_confirmed_at: :environment do + mass_confirmed_at = Time.current.to_s(:db) + User.select(:id).find_in_batches do |users| + null_confirmed_at_user_scope = User.where( + id: users.map(&:id), + confirmed_at: nil + ) + null_confirmed_at_user_scope.update_all(confirmed_at: mass_confirmed_at) + end + end + namespace :limit do class UpdateUserLimitArgsError < StandardError; end diff --git a/lib/tasks/user_groups.rake b/lib/tasks/user_groups.rake new file mode 100644 index 000000000..3b1581c4c --- /dev/null +++ b/lib/tasks/user_groups.rake @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +namespace :user_groups do + desc 'Backfill stats_visibility column default in batches' + task backfill_stats_visibility_column_default: :environment do + UserGroup.where(stats_visibility: nil).select(:id).find_in_batches do |user_group| + user_group_ids_to_update = user_group.map(&:id) + UserGroup.where(id: user_group_ids_to_update).update_all(stats_visibility: 0) + end + end +end diff --git a/spec/controllers/api/v1/project_preferences_controller_spec.rb b/spec/controllers/api/v1/project_preferences_controller_spec.rb index 3dbbea3cb..68bbf6e7d 100644 --- a/spec/controllers/api/v1/project_preferences_controller_spec.rb +++ b/spec/controllers/api/v1/project_preferences_controller_spec.rb @@ -168,6 +168,17 @@ end end + describe 'updating a project as an admin' do + let(:admin_user) { create(:admin_user) } + + it 'lets the admin update UPP settings' do + default_request user_id: admin_user.id, scopes: scopes + settings_params[:admin] = true + run_update + expect(response.status).to eq(200) + end + end + describe "trying to update a resource that doesn't exist" do let(:unused_project) { create(:project) } let(:settings_params) do diff --git a/spec/controllers/api/v1/user_groups_controller_spec.rb b/spec/controllers/api/v1/user_groups_controller_spec.rb index 3b22a6d43..a1c22ce26 100644 --- a/spec/controllers/api/v1/user_groups_controller_spec.rb +++ b/spec/controllers/api/v1/user_groups_controller_spec.rb @@ -69,6 +69,60 @@ end it_behaves_like 'is updatable' + + describe 'updating stats_visibility' do + let(:params) { + { + id: resource.id, + user_groups: { + display_name: 'A-Different-Name', + stats_visibility: 'public_agg_only' + } + } + } + + describe 'as group_admin' do + it 'updates stats_visibility' do + default_request scopes: scopes, user_id: authorized_user.id + put :update, params: params + expect(response.status).to eq(200) + + group = UserGroup.find(resource.id) + expect(group.stats_visibility).to eq('public_agg_only') + end + + it 'does not update user_group if invalid stats_visibility' do + default_request scopes: scopes, user_id: authorized_user.id + user_groups = { + display_name: 'A-Different-Name', + stats_visibility: 'fake_stats_visibility' + } + params[:user_groups] = user_groups + put :update, params: params + expect(response.status).to eq(400) + end + end + + describe 'as admin' do + it 'updates user_group_stats_visibility' do + admin_user = create(:user, admin: true) + default_request scopes: scopes, user_id: admin_user.id + params[:admin] = true + put :update, params: params + expect(response.status).to eq(200) + end + end + + describe 'as group_member' do + it 'does not update user_group stats_visibility' do + group_member_user = create(:user) + create(:membership, user: group_member_user, user_group: resource, roles: ['group_member']) + default_request scopes: scopes, user_id: group_member_user.id + put :update, params: params + expect(response.status).to eq(404) + end + end + end end describe '#show' do @@ -120,6 +174,35 @@ end end + describe 'setting stats_visibility' do + describe 'as group_admin' do + it 'sets the stats_visiblity when sending in stats_visiblity as string' do + default_request scopes: scopes, user_id: authorized_user.id + post :create, params: { user_groups: { name: 'GalaxyZoo', stats_visibility: 'public_agg_show_ind_if_member' } } + expect(response.status).to eq(201) + group = UserGroup.find(created_instance_id('user_groups')) + + expect(group.stats_visibility).to eq('public_agg_show_ind_if_member') + end + + it 'sets the stats_visibility when sending related integer corresponding to visibility level' do + default_request scopes: scopes, user_id: authorized_user.id + post :create, params: { user_groups: { name: 'GalaxyZoo', stats_visibility: 3 } } + expect(response.status).to eq(201) + + # see app/models/user_group.rb L22-L40 for explanations of stats_visibliity levels + group = UserGroup.find(created_instance_id('user_groups')) + expect(group.stats_visibility).to eq('public_agg_show_ind_if_member') + end + + it 'does not create group if stats_visibility is invalid' do + default_request scopes: scopes, user_id: authorized_user.id + post :create, params: { user_groups: { name: 'GalaxyZoo', stats_visibility: 7 } } + expect(response.status).to eq(400) + end + end + end + describe 'when only a name is provided' do it 'sets the display name' do default_request scopes: scopes, user_id: authorized_user.id diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb index b5748eecc..2c33c1057 100644 --- a/spec/controllers/passwords_controller_spec.rb +++ b/spec/controllers/passwords_controller_spec.rb @@ -72,8 +72,9 @@ expect(response.status).to eq(200) end - it "should not send an email to the account email address" do - expect(ActionMailer::Base.deliveries).to be_empty + it 'should send confirmation instructions to the account email address' do + expect(ActionMailer::Base.deliveries).not_to be_empty + expect(ActionMailer::Base.deliveries.first.body).to include('confirm your account') end end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index e0ef85c3a..96dfbea7e 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -149,8 +149,8 @@ post :create, params: { user: user_attributes } end - it "should queue a welcome worker to send an email" do - expect(UserWelcomeMailerWorker).to receive(:perform_async) + it "should send a confirmation instructions email" do + expect_any_instance_of(User).to receive(:send_confirmation_instructions).once post :create, params: { user: user_attributes } end @@ -212,8 +212,8 @@ end end - it "should not queue a welcome worker to send an email" do - expect(UserWelcomeMailerWorker).to_not receive(:perform_async) + it "should not send a confirmation instructions email" do + expect_any_instance_of(User).to_not receive(:send_confirmation_instructions) post :create, params: { user: user_attributes } end end @@ -279,8 +279,8 @@ post :create, params: { user: user_attributes } end - it "should queue a welcome worker to send an email" do - expect(UserWelcomeMailerWorker).to receive(:perform_async) + it "should send a confirmation instructions email" do + expect_any_instance_of(User).to receive(:send_confirmation_instructions).once post :create, params: { user: user_attributes } end end @@ -313,8 +313,8 @@ expect(User.where(login: user_attributes[:login])).to_not exist end - it "should not queue a welcome worker to send an email" do - expect(UserWelcomeMailerWorker).to_not receive(:perform_async) + it "should not send a confirmation instructions email" do + expect_any_instance_of(User).to_not receive(:send_confirmation_instructions) post :create, params: { user: user_attributes } end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 9196bcfa6..9d1a11be9 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -8,6 +8,8 @@ sequence(:email) {|n| "example#{n}@example.com"} password { 'password' } encrypted_password { User.new.send(:password_digest, 'password') } + confirmed_at { Time.current } + confirmation_sent_at { Time.current } credited_name { 'Dr User' } activated_state { :active } sequence(:login) { |n| "new_user_#{n}" } diff --git a/spec/lib/project_copier_spec.rb b/spec/lib/project_copier_spec.rb index 51e85d1c5..4a01ceaf2 100644 --- a/spec/lib/project_copier_spec.rb +++ b/spec/lib/project_copier_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ProjectCopier do @@ -39,6 +41,14 @@ expect(copied_project.configuration['source_project_id']).to be(project.id) end + it 'creates Talk roles for the new project and its owner' do + allow(TalkAdminCreateWorker).to receive(:perform_async) + copied_project + expect(TalkAdminCreateWorker) + .to have_received(:perform_async) + .with(be_kind_of(Integer)) + end + context 'when a project has active_worklfows' do it 'creates a valid workflow copy' do expect(copied_project.active_workflows.first).to be_valid @@ -84,6 +94,12 @@ field_guide = copied_project.field_guides.first expect(field_guide.translations.first.language).to eq(project.primary_language) end + + it 'copies the field guide attached images' do + fg = create(:field_guide, project: project) + fg.attached_images << create(:medium, type: 'field_guide_attached_image', linked: fg) + expect(copied_project.field_guides.first.attached_images[0]).to be_valid + end end context 'when a project has a project page' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 383330dfa..7c0251713 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -505,28 +505,55 @@ end end - describe "#communication_emails" do + describe '#communication_emails' do let(:project) { create(:project) } let(:owner_email) { project.owner.email } - it 'should return the owner by default' do + it 'return the owner email by default' do expect(project.communication_emails).to match_array([owner_email]) end - context "with communication project roles" do + context 'with non-owner communication project roles' do let(:comms_user) { create(:user) } + let(:collaborator) { create(:user) } + before do create( :access_control_list, user_group: comms_user.identity_group, resource: project, - roles: ["communications"] + roles: ['communications'] ) end - it 'should return the owner and comms roles emails' do + it 'returns the owner and comms roles emails' do expect(project.communication_emails).to match_array([owner_email, comms_user.email]) end + + it 'returns emails of owner, collaborator, and comms roles' do + create( + :access_control_list, + user_group: collaborator.identity_group, + resource: project, + roles: ['collaborator'] + ) + expect(project.communication_emails).to match_array([owner_email, comms_user.email, collaborator.email]) + end + + it 'does not include email of user that is not associated with project' do + expect(project.communication_emails).not_to include(collaborator.email) + end + + it 'does not include email of user with non-communications type role' do + translator = create(:user) + create( + :access_control_list, + user_group: translator.identity_group, + resource: project, + roles: ['translator'] + ) + expect(project.communication_emails).not_to include(translator.email) + end end end diff --git a/spec/models/user_group_spec.rb b/spec/models/user_group_spec.rb index 350cbbad4..6b8b847d9 100644 --- a/spec/models/user_group_spec.rb +++ b/spec/models/user_group_spec.rb @@ -71,6 +71,32 @@ end end + describe '#stats_visibility' do + it 'validates that it is one of the STATS_VISIBILITY levels' do + ug = build(:user_group, name: 'abc') + ug.stats_visibility = 'public_agg_only' + expect(ug).to be_valid + end + + it 'allows stats_visibility to be integer corresponding to STATS_VISIBILITY level' do + ug = build(:user_group, name: 'abc') + ug.stats_visibility = 4 + expect(ug).to be_valid + end + + it 'does not allow stats_visibility to be outside STATS_VISIBILITY levels' do + ug = build(:user_group, name: 'abc') + ug.stats_visibility = 'fake_stats_level' + expect(ug).not_to be_valid + end + + it 'does not allow stats_visibility to be outside STATS_VISIBILITY range' do + ug = build(:user_group, name: 'abc') + ug.stats_visibility = 9 + expect(ug).not_to be_valid + end + end + describe "#users" do let(:user_group) { create(:user_group_with_users) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9a17765e4..13e070d26 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -213,6 +213,17 @@ def dormant_user_ids(num_days_since_activity=5) end end + describe 'confirmation' do + context 'when a new user is saved' do + let(:user) { build(:user, confirmed_at: nil, confirmation_sent_at: nil) } + + it 'sends a confirmation email' do + expect_any_instance_of(User).to receive(:send_confirmation_instructions).once + user.save! + end + end + end + describe '::send_reset_password_instructions' do context 'when the user exists' do let(:user) { create(:user) } @@ -872,17 +883,17 @@ def dormant_user_ids(num_days_since_activity=5) end end - describe "#send_welcome_email after_create callback" do - let(:user) { build(:user) } + describe "#send_welcome_email on confirm" do + let(:user) { create(:user, confirmed_at: nil) } it "should send the welcome email" do expect(user).to receive(:send_welcome_email).once - user.save! + user.confirm end it "should queue the worker with the user id" do allow(UserWelcomeMailerWorker).to receive :perform_async - user.save! + user.confirm expect(UserWelcomeMailerWorker).to have_received(:perform_async).with(user.id, nil).ordered end @@ -890,24 +901,24 @@ def dormant_user_ids(num_days_since_activity=5) it "should not raise an error on save" do [ Timeout::Error, Redis::TimeoutError, Redis::CannotConnectError ].each do |redis_error| allow(UserWelcomeMailerWorker).to receive(:perform_async).and_raise(redis_error) - expect { user.save! }.not_to raise_error + expect { user.confirm }.not_to raise_error end end it "should send the welcome email in band" do allow(UserWelcomeMailerWorker).to receive(:perform_async).and_raise(Redis::CannotConnectError) expect_any_instance_of(UserWelcomeMailerWorker).to receive(:perform).with(Integer, nil) - user.save! + user.confirm end end context "when the user has a project id" do let(:project) { create :project } - let!(:user) { build(:user, project_id: project.id) } + let!(:user) { create(:user, project_id: project.id, confirmed_at: nil) } it "should queue the worker with the user id and project id" do allow(UserWelcomeMailerWorker).to receive :perform_async - user.save! + user.confirm expect(UserWelcomeMailerWorker).to have_received(:perform_async).with(user.id, project.id).ordered end end @@ -917,7 +928,7 @@ def dormant_user_ids(num_days_since_activity=5) it "should not queue the worker with the user id" do expect(UserWelcomeMailerWorker).not_to receive(:perform_async) - user.save! + user.confirm end end end