diff --git a/.rubocop.yml b/.rubocop.yml index 3e0d86588b2..98ae8d56834 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -154,6 +154,8 @@ Rails/OutputSafety: Rails/Output: Exclude: + # Allow patches to print warnings to console: + - 'config/initializers/monkeypatches/*.rb' # Allow migrations to print pt-osc comments to console: - 'db/migrate/*.rb' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9a71a4b64d..b9aa8fd30ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Reporting bugs -We maintain a [Jira issue tracker](https://otwarchive.atlassian.net) for developers, +We maintain a [Jira issue tracker](https://otwarchive.atlassian.net/projects/AO3/issues) for developers, and a [list of Known Issues](https://archiveofourown.org/known_issues) for [Archive of Our Own](https://archiveofourown.org) users, neither of which are publicly editable. @@ -35,30 +35,21 @@ with the exception of spelling corrections and documentation improvements (e.g. any Markdown files). We also do not accept code generated by AI tools; for more information, please refer to [our commit policy](https://github.com/otwcode/otwarchive/wiki/Commit-Policy#scary-legal-stuff). -If you'd like the ability to comment on, assign, and transition issues, -you're welcome to create a Jira account! (It makes things a bit easier for us -on the organizational side if the Full Name on your Jira account either closely -matches the name you'd like us to credit in the release notes or includes it in -parentheses, e.g. "Nickname (CREDIT NAME).") We'll give you permissions when -you create your first pull request. - Please check out our development wiki for more information on: - [how to set up a development environment](https://github.com/otwcode/otwarchive/wiki) - [code conventions](https://github.com/otwcode/otwarchive/wiki/Commit-policy) -Please follow the checklist on [our template](https://github.com/otwcode/otwarchive/blob/master/.github/PULL_REQUEST_TEMPLATE.md) when submitting pull requests. - -Please be patient with us! Due to our workload, it may take some time before we -can review and eventually merge your pull request. - -Once your pull request is merged, it will be deployed to our internal testing site -and our QA team will check that everything is working as intended. If not, we may -set the issue to ["Broken on Test"](https://github.com/otwcode/otwarchive/wiki/Issue-Tracking-with-Jira) -and ask you to make further changes in new pull requests. +### Workflow -If all is well, your contribution will be deployed to the [Archive of Our Own](https://archiveofourown.org) -and you will be credited in the [release notes](https://archiveofourown.org/admin_posts?tag=1)! +1. If you're a new contributor, find a task on the [issues reserved for first timers](https://otwarchive.atlassian.net/issues/?filter=13119). Otherwise, or if you're up for a challenge, pick a task from the general [open and unassigned issues](https://otwarchive.atlassian.net/issues/?filter=10800). (If you a new contributor, don't worry about claiming the issue for now. If you make a Jira account, you'll get permissions for claiming issues in step 5.) +2. Write code to address the issue. +3. Optional: Create a Jira account if you'd like the ability to comment on, assign, and transition issues. Please make sure the Full Name on your Jira account either closely matches the name you'd like us to credit in the release notes or includes it in parentheses, e.g. "Nickname (CREDIT NAME)." +4. Submit the code with a pull request following the checklist on [our template](https://github.com/otwcode/otwarchive/blob/master/.github/PULL_REQUEST_TEMPLATE.md). +5. Once you've submitted a pull request, we'll review your code and give you permissions on Jira. Please be patient with us! Due to our workload, it may take some time before we can review and eventually merge your pull request. +6. Once your pull request is merged, we will deploy it to our internal testing site and our QA team will check that everything is working as intended. +7. If something is not working as intended, we may set the issue to ["Broken on Test"](https://github.com/otwcode/otwarchive/wiki/Issue-Tracking-with-Jira) and ask you to make further changes in new pull requests. +8. If all is well, your contribution will be deployed to the [Archive of Our Own](https://archiveofourown.org) and you will be credited in the [release notes](https://archiveofourown.org/admin_posts?tag=1)! ## Volunteering for the OTW diff --git a/Gemfile.lock b/Gemfile.lock index a4e628b9c13..71edb8fe7fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,75 +24,75 @@ GEM remote: https://rubygems.org/ specs: aaronh-chronic (0.3.9) - actioncable (7.0.8.5) - actionpack (= 7.0.8.5) - activesupport (= 7.0.8.5) + actioncable (7.0.8.7) + actionpack (= 7.0.8.7) + activesupport (= 7.0.8.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.5) - actionpack (= 7.0.8.5) - activejob (= 7.0.8.5) - activerecord (= 7.0.8.5) - activestorage (= 7.0.8.5) - activesupport (= 7.0.8.5) + actionmailbox (7.0.8.7) + actionpack (= 7.0.8.7) + activejob (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.5) - actionpack (= 7.0.8.5) - actionview (= 7.0.8.5) - activejob (= 7.0.8.5) - activesupport (= 7.0.8.5) + actionmailer (7.0.8.7) + actionpack (= 7.0.8.7) + actionview (= 7.0.8.7) + activejob (= 7.0.8.7) + activesupport (= 7.0.8.7) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.5) - actionview (= 7.0.8.5) - activesupport (= 7.0.8.5) + actionpack (7.0.8.7) + actionview (= 7.0.8.7) + activesupport (= 7.0.8.7) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) actionpack-page_caching (1.2.4) actionpack (>= 4.0.0) - actiontext (7.0.8.5) - actionpack (= 7.0.8.5) - activerecord (= 7.0.8.5) - activestorage (= 7.0.8.5) - activesupport (= 7.0.8.5) + actiontext (7.0.8.7) + actionpack (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.5) - activesupport (= 7.0.8.5) + actionview (7.0.8.7) + activesupport (= 7.0.8.7) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) active_record_query_trace (1.8.2) activerecord (>= 6.0.0) - activejob (7.0.8.5) - activesupport (= 7.0.8.5) + activejob (7.0.8.7) + activesupport (= 7.0.8.7) globalid (>= 0.3.6) - activemodel (7.0.8.5) - activesupport (= 7.0.8.5) + activemodel (7.0.8.7) + activesupport (= 7.0.8.7) activemodel-serializers-xml (1.0.2) activemodel (> 5.x) activesupport (> 5.x) builder (~> 3.1) - activerecord (7.0.8.5) - activemodel (= 7.0.8.5) - activesupport (= 7.0.8.5) - activestorage (7.0.8.5) - actionpack (= 7.0.8.5) - activejob (= 7.0.8.5) - activerecord (= 7.0.8.5) - activesupport (= 7.0.8.5) + activerecord (7.0.8.7) + activemodel (= 7.0.8.7) + activesupport (= 7.0.8.7) + activestorage (7.0.8.7) + actionpack (= 7.0.8.7) + activejob (= 7.0.8.7) + activerecord (= 7.0.8.7) + activesupport (= 7.0.8.7) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.5) + activesupport (7.0.8.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -257,7 +257,7 @@ GEM rainbow rubocop smart_properties - erubi (1.13.0) + erubi (1.13.1) escape_utils (1.2.1) et-orbi (1.2.11) tzinfo @@ -435,22 +435,22 @@ GEM rack-protection (3.2.0) base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) - rack-test (2.1.0) + rack-test (2.2.0) rack (>= 1.3) - rails (7.0.8.5) - actioncable (= 7.0.8.5) - actionmailbox (= 7.0.8.5) - actionmailer (= 7.0.8.5) - actionpack (= 7.0.8.5) - actiontext (= 7.0.8.5) - actionview (= 7.0.8.5) - activejob (= 7.0.8.5) - activemodel (= 7.0.8.5) - activerecord (= 7.0.8.5) - activestorage (= 7.0.8.5) - activesupport (= 7.0.8.5) + rails (7.0.8.7) + actioncable (= 7.0.8.7) + actionmailbox (= 7.0.8.7) + actionmailer (= 7.0.8.7) + actionpack (= 7.0.8.7) + actiontext (= 7.0.8.7) + actionview (= 7.0.8.7) + activejob (= 7.0.8.7) + activemodel (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) bundler (>= 1.15.0) - railties (= 7.0.8.5) + railties (= 7.0.8.7) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -459,15 +459,15 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.1) + rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails-i18n (7.0.8) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.0.8.5) - actionpack (= 7.0.8.5) - activesupport (= 7.0.8.5) + railties (7.0.8.7) + actionpack (= 7.0.8.7) + activesupport (= 7.0.8.7) method_source rake (>= 12.2) thor (~> 1.0) diff --git a/README.md b/README.md index b7234cdb7a2..8724d779bc5 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ We welcome pull requests for bugs described in our issue tracker. Please see our We do not have a public chat, but you are welcome to contact us at otw-coders@transformativeworks.org if you have any questions. +We grant your Jira account permissions for commenting on, assigning, and transitioning issues [after you create your first pull request](https://github.com/otwcode/otwarchive/blob/master/CONTRIBUTING.md#workflow). + API ---------- There is currently no API for the OTW-Archive software. While it is something we're considering for the future, we ask that contributors instead focus on issues already in our [Jira issue tracker](https://otwarchive.atlassian.net/). diff --git a/app/controllers/chapters_controller.rb b/app/controllers/chapters_controller.rb index 3fe5a4478ad..bdab981c760 100644 --- a/app/controllers/chapters_controller.rb +++ b/app/controllers/chapters_controller.rb @@ -1,6 +1,6 @@ class ChaptersController < ApplicationController # only registered users and NOT admin should be able to create new chapters - before_action :users_only, except: [ :index, :show, :destroy, :confirm_delete ] + before_action :users_only, except: [:index, :show, :destroy, :confirm_delete] before_action :check_user_status, only: [:new, :create, :update, :update_positions] before_action :check_user_not_suspended, only: [:edit, :confirm_delete, :destroy] before_action :load_work @@ -28,53 +28,55 @@ def manage # GET /work/:work_id/chapters/:id.xml def show @tag_groups = @work.tag_groups - if params[:view_adult] - cookies[:view_adult] = "true" - elsif @work.adult? && !see_adult? - render "works/_adult", layout: "application" and return - end - if params[:selected_id] - redirect_to url_for(controller: :chapters, action: :show, work_id: @work.id, id: params[:selected_id]) and return - end + redirect_to url_for(controller: :chapters, action: :show, work_id: @work.id, id: params[:selected_id]) and return if params[:selected_id] + @chapters = @work.chapters_in_order( include_content: false, include_drafts: (logged_in_as_admin? || @work.user_is_owner_or_invited?(current_user)) ) - if !@chapters.include?(@chapter) + + unless @chapters.include?(@chapter) access_denied + return + end + + chapter_position = @chapters.index(@chapter) + if @chapters.length > 1 + @previous_chapter = @chapters[chapter_position - 1] unless chapter_position.zero? + @next_chapter = @chapters[chapter_position + 1] + end + + if @work.unrevealed? + @page_title = t(".unrevealed") + t(".chapter_position", position: @chapter.position.to_s) else - chapter_position = @chapters.index(@chapter) - if @chapters.length > 1 - @previous_chapter = @chapters[chapter_position-1] unless chapter_position == 0 - @next_chapter = @chapters[chapter_position+1] - end + fandoms = @tag_groups["Fandom"] + fandom = fandoms.empty? ? t(".unspecified_fandom") : fandoms[0].name + title_fandom = fandoms.size > 3 ? t(".multifandom") : fandom + author = @work.anonymous? ? t(".anonymous") : @work.pseuds.sort.collect(&:byline).join(", ") + @page_title = get_page_title(title_fandom, author, @work.title + t(".chapter_position", position: @chapter.position.to_s)) + end - if @work.unrevealed? - @page_title = t(".unrevealed") + t(".chapter_position", position: @chapter.position.to_s) - else - fandoms = @tag_groups["Fandom"] - fandom = fandoms.empty? ? t(".unspecified_fandom") : fandoms[0].name - title_fandom = fandoms.size > 3 ? t(".multifandom") : fandom - author = @work.anonymous? ? t(".anonymous") : @work.pseuds.sort.collect(&:byline).join(", ") - @page_title = get_page_title(title_fandom, author, @work.title + t(".chapter_position", position: @chapter.position.to_s)) - end + if params[:view_adult] + cookies[:view_adult] = "true" + elsif @work.adult? && !see_adult? + render "works/_adult", layout: "application" and return + end - @kudos = @work.kudos.with_user.includes(:user) + @kudos = @work.kudos.with_user.includes(:user) - if current_user.respond_to?(:subscriptions) - @subscription = current_user.subscriptions.where(subscribable_id: @work.id, - subscribable_type: 'Work').first || - current_user.subscriptions.build(subscribable: @work) - end - # update the history. - Reading.update_or_create(@work, current_user) if current_user + if current_user.respond_to?(:subscriptions) + @subscription = current_user.subscriptions.where(subscribable_id: @work.id, + subscribable_type: "Work").first || + current_user.subscriptions.build(subscribable: @work) + end + # update the history. + Reading.update_or_create(@work, current_user) if current_user - respond_to do |format| - format.html - format.js - end + respond_to do |format| + format.html + format.js end end @@ -86,14 +88,14 @@ def new # GET /work/:work_id/chapters/1/edit def edit - if params["remove"] == "me" - @chapter.creatorships.for_user(current_user).destroy_all - if @work.chapters.any? { |c| current_user.is_author_of?(c) } - flash[:notice] = ts("You have been removed as a creator from the chapter.") - redirect_to @work - else # remove from work if no longer co-creator on any chapter - redirect_to edit_work_path(@work, remove: "me") - end + return unless params["remove"] == "me" + + @chapter.creatorships.for_user(current_user).destroy_all + if @work.chapters.any? { |c| current_user.is_author_of?(c) } + flash[:notice] = ts("You have been removed as a creator from the chapter.") + redirect_to @work + else # remove from work if no longer co-creator on any chapter + redirect_to edit_work_path(@work, remove: "me") end end @@ -250,7 +252,7 @@ def chapter_cannot_be_saved? # fetch work these chapters belong to from db def load_work @work = params[:work_id] ? Work.find_by(id: params[:work_id]) : Chapter.find_by(id: params[:id]).try(:work) - unless @work.present? + if @work.blank? flash[:error] = ts("Sorry, we couldn't find the work you were looking for.") redirect_to root_path and return end @@ -263,18 +265,15 @@ def load_work def load_chapter @chapter = @work.chapters.find_by(id: params[:id]) - unless @chapter - flash[:error] = ts("Sorry, we couldn't find the chapter you were looking for.") - redirect_to work_path(@work) - end - end + return if @chapter + flash[:error] = ts("Sorry, we couldn't find the chapter you were looking for.") + redirect_to work_path(@work) + end def post_chapter - if !@work.posted - @work.update_attribute(:posted, true) - end - flash[:notice] = ts('Chapter has been posted!') + @work.update_attribute(:posted, true) unless @work.posted + flash[:notice] = ts("Chapter has been posted!") end private @@ -284,6 +283,5 @@ def chapter_params :"published_at(2i)", :"published_at(1i)", :summary, :notes, :endnotes, :content, :published_at, author_attributes: [:byline, ids: [], coauthors: []]) - end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index edff4637cab..86e83630c1e 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -8,24 +8,24 @@ def unicorn_test end def content - @page_title = t(".page_title") + @page_subtitle = t(".page_title") render action: "content", layout: "application" end def privacy - @page_title = t(".page_title") + @page_subtitle = t(".page_title") render action: "privacy", layout: "application" end # terms of service def tos - @page_title = t(".page_title") + @page_subtitle = t(".page_title") render action: "tos", layout: "application" end # terms of service faq def tos_faq - @page_title = t(".page_title") + @page_subtitle = t(".page_title") render action: "tos_faq", layout: "application" end diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb index fd87f3e7946..0285291d0bb 100644 --- a/app/helpers/comments_helper.rb +++ b/app/helpers/comments_helper.rb @@ -210,6 +210,10 @@ def can_review_comment?(comment) is_author_of?(comment.ultimate_parent) || policy(comment).can_review_comment? end + def can_review_all_comments?(commentable) + commentable.is_a?(AdminPost) || is_author_of?(commentable) + end + #### HELPERS FOR REPLYING TO COMMENTS ##### # return link to add new reply to a comment diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index 2669e1699ef..56ac370dbb4 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -240,7 +240,9 @@ def show_hidden_tag_link_list_item(item, category, options = {}) def get_title_string(tags, category_name = "") if tags && tags.size > 0 - tags.collect(&:name).join(", ") + # We don't use .to_sentence because these aren't links and we risk making any + # connector word (e.g., "and") look like part of the final tag. + tags.pluck(:name).join(t("support.array.words_connector")) elsif tags.blank? && category_name.blank? "Choose Not To Use Archive Warnings" else diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index ebe3070ebf9..1eb33ced20f 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -149,12 +149,10 @@ def batch_subscription_notification(subscription_id, entries) def invite_increase_notification(user_id, total) @user = User.find(user_id) @total = total - I18n.with_locale(@user.preference.locale.iso) do - mail( - to: @user.email, - subject: default_i18n_subject(app_name: ArchiveConfig.APP_SHORT_NAME) - ) - end + mail( + to: @user.email, + subject: default_i18n_subject(app_name: ArchiveConfig.APP_SHORT_NAME) + ) end # Emails a user to say that their request for invitation codes has been declined @@ -199,14 +197,12 @@ def potential_match_generation_notification(collection_id, email) def challenge_assignment_notification(collection_id, assigned_user_id, assignment_id) @collection = Collection.find(collection_id) @assigned_user = User.find(assigned_user_id) - assignment = ChallengeAssignment.find(assignment_id) - @request = (assignment.request_signup || assignment.pinch_request_signup) - I18n.with_locale(@assigned_user.preference.locale.iso) do - mail( - to: @assigned_user.email, - subject: default_i18n_subject(app_name: ArchiveConfig.APP_SHORT_NAME, collection_title: @collection.title) - ) - end + @assignment = ChallengeAssignment.find(assignment_id) + @request = (@assignment.request_signup || @assignment.pinch_request_signup) + mail( + to: @assigned_user.email, + subject: default_i18n_subject(app_name: ArchiveConfig.APP_SHORT_NAME, collection_title: @collection.title) + ) end # Asks a user to validate and activate their new account diff --git a/app/models/challenge_assignment.rb b/app/models/challenge_assignment.rb index 80b37738451..714fecdf537 100755 --- a/app/models/challenge_assignment.rb +++ b/app/models/challenge_assignment.rb @@ -271,7 +271,11 @@ def send_out (self.pinch_hitter ? self.pinch_hitter.user : nil) end request = self.request_signup || self.pinch_request_signup - UserMailer.challenge_assignment_notification(collection.id, assigned_to.id, self.id).deliver_later if assigned_to && request + if assigned_to && request + I18n.with_locale(assigned_to.preference.locale.iso) do + UserMailer.challenge_assignment_notification(collection.id, assigned_to.id, self.id).deliver_later + end + end end end diff --git a/app/models/concerns/justifiable.rb b/app/models/concerns/justifiable.rb index dea0c09d80c..c4c4df3e8c5 100644 --- a/app/models/concerns/justifiable.rb +++ b/app/models/concerns/justifiable.rb @@ -5,9 +5,12 @@ module Justifiable attr_accessor :ticket_number attr_reader :ticket_url + before_validation :strip_octothorpe validates :ticket_number, presence: true, - numericality: { only_integer: true }, + # i18n-tasks-use t("activerecord.errors.messages.numeric_with_optional_hash") + numericality: { only_integer: true, + message: :numeric_with_optional_hash }, if: :enabled? validate :ticket_number_exists_in_tracker, if: :enabled? @@ -15,6 +18,12 @@ module Justifiable private + def strip_octothorpe + return if ticket_number.is_a?(Integer) + + self.ticket_number = self.ticket_number.delete_prefix("#") unless self.ticket_number.nil? + end + def enabled? # Only require a ticket if the record has been changed by an admin. User.current_user.is_a?(Admin) && changed? diff --git a/app/models/invitation.rb b/app/models/invitation.rb index cc7bb464a88..d8437ab8c7e 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -31,7 +31,9 @@ def self.grant_all(total) total.times do user.invitations.create end - UserMailer.invite_increase_notification(user.id, total).deliver_later + I18n.with_locale(user.preference.locale.iso) do + UserMailer.invite_increase_notification(user.id, total).deliver_later + end end User.out_of_invites.update_all('out_of_invites = 0') end @@ -43,7 +45,9 @@ def self.grant_empty(total) total.times do user.invitations.create end - UserMailer.invite_increase_notification(user.id, total).deliver_later + I18n.with_locale(user.preference.locale.iso) do + UserMailer.invite_increase_notification(user.id, total).deliver_later + end end User.out_of_invites.update_all('out_of_invites = 0') end diff --git a/app/models/story_parser.rb b/app/models/story_parser.rb index 96477074800..2c2e9e69d9e 100644 --- a/app/models/story_parser.rb +++ b/app/models/story_parser.rb @@ -799,6 +799,8 @@ def download_with_timeout(location, limit = 10) # we do a little cleanup here in case the user hasn't included the 'http://' # or if they've used capital letters or an underscore in the hostname uri = UrlFormatter.new(location).standardized + raise Error, I18n.t("story_parser.on_archive") if ArchiveConfig.PERMITTED_HOSTS.include?(uri.host) + response = Net::HTTP.get_response(uri) case response when Net::HTTPSuccess diff --git a/app/models/user_invite_request.rb b/app/models/user_invite_request.rb index 549fbb58955..0095bb24770 100644 --- a/app/models/user_invite_request.rb +++ b/app/models/user_invite_request.rb @@ -26,7 +26,9 @@ def grant_request self.quantity.times do self.user.invitations.create end - UserMailer.invite_increase_notification(self.user.id, self.quantity).deliver_after_commit + I18n.with_locale(self.user.preference.locale.iso) do + UserMailer.invite_increase_notification(self.user.id, self.quantity).deliver_after_commit + end end end end diff --git a/app/views/comments/unreviewed.html.erb b/app/views/comments/unreviewed.html.erb index 51ad90d82e6..f70d57605ec 100644 --- a/app/views/comments/unreviewed.html.erb +++ b/app/views/comments/unreviewed.html.erb @@ -3,10 +3,12 @@ - +<% if can_review_all_comments?(@commentable) %> + +<% end %> diff --git a/app/views/pseuds/_pseuds_form.html.erb b/app/views/pseuds/_pseuds_form.html.erb index a3d4b95a041..b7b1c1d558c 100644 --- a/app/views/pseuds/_pseuds_form.html.erb +++ b/app/views/pseuds/_pseuds_form.html.erb @@ -62,8 +62,6 @@
<%= f.label :ticket_number, class: "required" %>
<%= f.text_field :ticket_number, class: "required" %> - <%= live_validation_for_field("pseud_ticket_number", numericality: true) %> -

<%= t(".ticket_footnote") %>

<% end %> diff --git a/app/views/user_mailer/challenge_assignment_notification.html.erb b/app/views/user_mailer/challenge_assignment_notification.html.erb index 632e0e870f7..9237a13c180 100644 --- a/app/views/user_mailer/challenge_assignment_notification.html.erb +++ b/app/views/user_mailer/challenge_assignment_notification.html.erb @@ -12,7 +12,7 @@ <% def styled_tag_list(tags) %> <% return nil if !tags || tags.empty? %> - <% tags.map { |tag| style_link(tag.name, tag_works_url(tag)) }.to_sentence.html_safe %> + <% to_sentence(tags.map { |tag| style_link(tag.name, tag_works_url(tag)) }) %> <% end %> <% fandoms = prompt.any_fandom ? t(".any") : styled_tag_list(tag_groups["Fandom"]) %> @@ -28,15 +28,51 @@ <%= index + 1 %>. <%= style_bold(prompt.title) %>

- <% if fandoms %><%= style_bold(Fandom.human_attribute_name("name_with_colon", count: prompt.any_fandom ? 1 : tag_groups["Fandom"].count)) %> <%= fandoms %>
<% end %> - <% if chars %><%= style_bold(Character.human_attribute_name("name_with_colon", count: prompt.any_character ? 1 : tag_groups["Character"].count)) %> <%= chars %>
<% end %> - <% if ships %><%= style_bold(Relationship.human_attribute_name("name_with_colon", count: prompt.any_relationship ? 1 : tag_groups["Relationship"].count)) %> <%= ships %>
<% end %> - <% if ratings %><%= style_bold(Rating.human_attribute_name("name_with_colon")) %> <%= ratings %>
<% end %> - <% if warnings %><%= style_bold(ArchiveWarning.human_attribute_name("name_with_colon", count: prompt.any_archive_warning ? 1 : tag_groups["ArchiveWarning"].count)) %> <%= warnings %>
<% end %> - <% if categories %><%= style_bold(Category.human_attribute_name("name_with_colon", count: prompt.any_category ? 1 : tag_groups["Category"].count)) %> <%= categories %>
<% end %> - <% if atags %><%= style_bold(Freeform.human_attribute_name("name_with_colon", count: prompt.any_freeform ? 1 : tag_groups["Freeform"].count)) %> <%= atags %>
<% end %> - <% if otags %><%= style_bold("#{t(".optional_tags")}") %> <%= otags %>
<% end %> - <% if prompt.url && !prompt.url.blank? %><%= style_bold("#{t(".prompt_url")}") %> <%= style_link(prompt.url, prompt.url) %>
<% end %> + <% if fandoms %> + <%= style_bold(t("activerecord.models.fandom", count: prompt.any_fandom ? 1 : tag_groups["Fandom"].count) + t("mailer.general.metadata_label_indicator")) %> + <%= fandoms %> +
+ <% end %> + <% if chars %> + <%= style_bold(t("activerecord.models.character", count: prompt.any_character ? 1 : tag_groups["Character"].count) + t("mailer.general.metadata_label_indicator")) %> + <%= chars %> +
+ <% end %> + <% if ships %> + <%= style_bold(t("activerecord.models.relationship", count: prompt.any_relationship ? 1 : tag_groups["Relationship"].count) + t("mailer.general.metadata_label_indicator")) %> + <%= ships %> +
+ <% end %> + <% if ratings %> + <%= style_bold(t("activerecord.models.rating", count: 1) + t("mailer.general.metadata_label_indicator")) %> + <%= ratings %> +
+ <% end %> + <% if warnings %> + <%= style_bold(t("activerecord.models.archive_warning", count: prompt.any_archive_warning ? 1 : tag_groups["ArchiveWarning"].count) + t("mailer.general.metadata_label_indicator")) %> + <%= warnings %> +
+ <% end %> + <% if categories %> + <%= style_bold(t("activerecord.models.category", count: prompt.any_category ? 1 : tag_groups["Category"].count) + t("mailer.general.metadata_label_indicator")) %> + <%= categories %> +
+ <% end %> + <% if atags %> + <%= style_bold(t("activerecord.models.freeform", count: prompt.any_freeform ? 1 : tag_groups["Freeform"].count) + t("mailer.general.metadata_label_indicator")) %> + <%= atags %> +
+ <% end %> + <% if otags %> + <%= style_bold(t(".optional_tags")) %> + <%= otags %> +
+ <% end %> + <% if prompt.url && !prompt.url.blank? %> + <%= style_bold(t(".prompt_url")) %> + <%= style_link(prompt.url, prompt.url) %> +
+ <% end %> <% if prompt.description && !prompt.description.blank? %> <%= style_bold(t(".description")) %> <%= style_quote(prompt.description) %> @@ -50,7 +86,7 @@ <%= style_bold(t(".due")) %> <%= time_in_zone(@collection.challenge.assignments_due_at, (@collection.challenge.time_zone || Time.zone.name), @assigned_user) %>.

-

<%= t(".html.look_up", link: style_link(t(".html.look_up_link"), user_assignments_url(@assigned_user))).html_safe %>

+

<%= t(".look_up.html", your_assignments_link: style_link(t(".look_up.your_assignments"), user_assignments_url(@assigned_user))) %>

<% if @collection && !@collection.assignment_notification.blank? %>

<%= escape_html_and_create_linebreaks(@collection.assignment_notification) %>

@@ -58,9 +94,9 @@ <% end %> <% content_for :footer_note do %> - <%= t(".html.footer", title: style_footer_link(@collection.title, collection_url(@collection)), footer_link: style_footer_link(t(".html.footer_link"), collection_profile_url(@collection))).html_safe %> + <%= t(".footer.html", title: style_footer_link(@collection.title, collection_url(@collection)), challenge_profile_link: style_footer_link(t(".footer.challenge_profile"), collection_profile_url(@collection))) %> <% end %> <% content_for :sent_at do %> - <%= l(Time.now) %> + <%= l(@assignment.sent_at) %> <% end %> diff --git a/app/views/user_mailer/challenge_assignment_notification.text.erb b/app/views/user_mailer/challenge_assignment_notification.text.erb index 8cb06b561f4..f7db169b07d 100644 --- a/app/views/user_mailer/challenge_assignment_notification.text.erb +++ b/app/views/user_mailer/challenge_assignment_notification.text.erb @@ -1,5 +1,5 @@ <% content_for :message do %> -<%= t ".text.assignment", collection_title: @collection.title, collection_url: collection_url(@collection) %> +<%= t ".assignment.text", collection_title: @collection.title, collection_url: collection_url(@collection) %> <%= t ".recipient" %> <%= @request.nil? ? t(".recipient_missing") : text_pseud(@request.pseud) %> @@ -22,26 +22,50 @@ <%= index + 1 %>. <%= prompt.title %> -<% if fandoms %><%= Fandom.human_attribute_name("name_with_colon", count: prompt.any_fandom ? 1 : tag_groups["Fandom"].count) %> <%= fandoms %><% end %><% if chars %> -<%= Character.human_attribute_name("name_with_colon", count: prompt.any_character ? 1 : tag_groups["Character"].count) %> <%= chars %><% end %><% if ships %> -<%= Relationship.human_attribute_name("name_with_colon", count: prompt.any_relationship ? 1 : tag_groups["Relationship"].count) %> <%= ships %><% end %><% if ratings %> -<%= Rating.human_attribute_name("name_with_colon") %> <%= ratings %><% end %><% if warnings %> -<%= ArchiveWarning.human_attribute_name("name_with_colon", count: prompt.any_archive_warning ? 1 : tag_groups["ArchiveWarning"].count) %> <%= warnings %><% end %><% if categories %> -<%= Category.human_attribute_name("name_with_colon", count: prompt.any_category ? 1 : tag_groups["Category"].count) %> <%= categories %><% end %><% if atags %> -<%= Freeform.human_attribute_name("name_with_colon", count: prompt.any_freeform ? 1 : tag_groups["Freeform"].count) %> <%= atags %><% end %><% if otags %> -<%= t ".optional_tags" %> <%= otags %><% end %><% if prompt.url && !prompt.url.blank? %> -<%= t ".prompt_url" %> <%= prompt.url %><% end %><% if prompt.description && !prompt.description.blank? %> -<%= t ".description" %> - <%= to_plain_text(prompt.description) %><% end %> +<% if fandoms %> +<%= t("activerecord.models.fandom", count: prompt.any_fandom ? 1 : tag_groups["Fandom"].count) + t("mailer.general.metadata_label_indicator") %><%= fandoms %> +<% end %> +<% if chars %> +<%= t("activerecord.models.character", count: prompt.any_character ? 1 : tag_groups["Character"].count) + t("mailer.general.metadata_label_indicator") %><%= chars %> +<% end %> +<% if ships %> +<%= t("activerecord.models.relationship", count: prompt.any_relationship ? 1 : tag_groups["Relationship"].count) + t("mailer.general.metadata_label_indicator") %><%= ships %> +<% end %> +<% if ratings %> +<%= t("activerecord.models.rating", count: 1) + t("mailer.general.metadata_label_indicator") %><%= ratings %> +<% end %> +<% if warnings %> +<%= t("activerecord.models.archive_warning", count: prompt.any_archive_warning ? 1 : tag_groups["ArchiveWarning"].count) + t("mailer.general.metadata_label_indicator") %><%= warnings %> +<% end %> +<% if categories %> +<%= t("activerecord.models.category", count: prompt.any_category ? 1 : tag_groups["Category"].count) + t("mailer.general.metadata_label_indicator") %><%= categories %> +<% end %> +<% if atags %> +<%= t("activerecord.models.freeform", count: prompt.any_freeform ? 1 : tag_groups["Freeform"].count) + t("mailer.general.metadata_label_indicator") %><%= atags %> +<% end %> +<% if otags %> +<%= t(".optional_tags") %> <%= otags %> +<% end %> +<% if prompt.url && !prompt.url.blank? %> +<%= t(".prompt_url") %> <%= prompt.url %> +<% end %> +<% if prompt.description && !prompt.description.blank? %> +<%= t(".description") %> + <%= to_plain_text(prompt.description) %> +<% end %> <% end %><%= text_divider %> -<%= t '.due' %> <%= to_plain_text(time_in_zone(@collection.challenge.assignments_due_at, (@collection.challenge.time_zone || Time.zone.name), @assigned_user)).gsub(/\n\s*/, "") %>. +<%= t(".due") %> <%= to_plain_text(time_in_zone(@collection.challenge.assignments_due_at, (@collection.challenge.time_zone || Time.zone.name), @assigned_user)).gsub(/\n\s*/, "") %>. -<%= t ".text.look_up", link: user_assignments_url(@assigned_user) %> +<%= t(".look_up.text", your_assignments_url: user_assignments_url(@assigned_user)) %> <% if @collection && !@collection.assignment_notification.blank? %> <%= @collection.assignment_notification %><% end %><% end %> -<% content_for :footer_note do %><%= t ".text.footer", title: @collection.title, url: collection_url(@collection), profile_url: collection_profile_url(@collection) %><% end %> -<% content_for :sent_at do %><%= l(Time.now) %><% end %> +<% content_for :footer_note do %> +<%= t(".footer.text", title: @collection.title, url: collection_url(@collection), challenge_profile_url: collection_profile_url(@collection)) -%> +<% end %> +<% content_for :sent_at do %> +<%= l(@assignment.sent_at) -%> +<% end %> diff --git a/app/views/user_mailer/invite_increase_notification.html.erb b/app/views/user_mailer/invite_increase_notification.html.erb index 28f3ec28dfb..eb5036e5deb 100644 --- a/app/views/user_mailer/invite_increase_notification.html.erb +++ b/app/views/user_mailer/invite_increase_notification.html.erb @@ -1,7 +1,7 @@ <% content_for :message do %>

<%= t("mailer.general.greeting.informal.addressed_html", name: style_bold(@user.login)) %>

-

<%= t(".html.body", count: @total, invitation_page_link: style_link(t(".invitation_page_link_text"), user_invitations_url(@user))).html_safe %>

+

<%= t(".body.html", count: @total, invitation_page_link: style_link(t(".invitation_page_link_text"), user_invitations_url(@user))) %>

<%= t("mailer.general.closing.informal") %>
diff --git a/app/views/user_mailer/invite_increase_notification.text.erb b/app/views/user_mailer/invite_increase_notification.text.erb index 71db974dd6d..499d6c02e2c 100644 --- a/app/views/user_mailer/invite_increase_notification.text.erb +++ b/app/views/user_mailer/invite_increase_notification.text.erb @@ -1,7 +1,7 @@ <% content_for :message do %> <%= t("mailer.general.greeting.informal.addressed_html", name: @user.login) %> -<%= t(".text.body", count: @total, invitation_page_url: user_invitations_url(@user)) %> +<%= t(".body.text", count: @total, invitation_page_url: user_invitations_url(@user)) %> <%= t("mailer.general.closing.informal") %> <%= t("mailer.general.signature.app_short_name") %> diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index 9f308eb0c11..e9a5c2d42f8 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -46,8 +46,6 @@

<%= p.label :ticket_number, class: "required" %>
<%= p.text_field :ticket_number, class: "required" %> - <%= live_validation_for_field("profile_attributes_ticket_number", numericality: true) %> -

<%= t(".admin.ticket_number.numbers_only") %>

<% end %> diff --git a/config/config.yml b/config/config.yml index 99ec7d30672..f93e615ec07 100644 --- a/config/config.yml +++ b/config/config.yml @@ -676,7 +676,8 @@ HIT_COUNT_ROLLOVER_HOUR: 3 # The batch size for calculating a work's filters from its tags: FILTER_UPDATE_BATCH_SIZE: 100 -# URLs for which we should not display the proxy notice. Alphabetical by +# URLs for which we should not display the proxy notice. URLs from these hosts +# are allowed in Abuse reports and disallowed in Work imports. Alphabetical by # environment. PERMITTED_HOSTS: [ # Production diff --git a/config/initializers/monkeypatches/deliver_after_commit.rb b/config/initializers/monkeypatches/deliver_after_commit.rb index d289ce0ba8a..80e8c591683 100644 --- a/config/initializers/monkeypatches/deliver_after_commit.rb +++ b/config/initializers/monkeypatches/deliver_after_commit.rb @@ -7,3 +7,29 @@ def deliver_after_commit end end end + +module AfterCommitEverywhereWithLocale + def initialize(connection: ActiveRecord::Base.connection, **handlers) + @connection = connection + @handlers = handlers + @locale = I18n.locale + end + + def before_committed!(*) + I18n.with_locale(@locale) { @handlers[:before_commit]&.call } + end + + def committed!(*) + I18n.with_locale(@locale) { @handlers[:after_commit]&.call } + end + + def rolledback!(*) + I18n.with_locale(@locale) { @handlers[:after_rollback]&.call } + end +end + +if AfterCommitEverywhere::VERSION == "1.4.0" + AfterCommitEverywhere::Wrap.prepend(AfterCommitEverywhereWithLocale) +else + puts "WARNING: The monkeypatch #{__FILE__} was written for version 1.4.0 of the after_commit_everywhere gem, but you are running #{AfterCommitEverywhere::VERSION}. Please update or remove the monkeypatch." +end diff --git a/config/locales/controllers/en.yml b/config/locales/controllers/en.yml index b8b56ec9838..f1e1c7d4327 100644 --- a/config/locales/controllers/en.yml +++ b/config/locales/controllers/en.yml @@ -124,10 +124,10 @@ en: index: page_subtitle: fandoms users: - contact_abuse: contact Policy & Abuse + contact_abuse: contact our Policy & Abuse team passwords: create: - contact_abuse: contact Policy & Abuse + contact_abuse: contact our Policy & Abuse team reset_blocked_html: Password resets are disabled for that user. For more information, please %{contact_abuse_link}. reset_cooldown_html: You cannot reset your password at this time. Please try again after %{reset_available_time}. send_cooldown_period: @@ -139,8 +139,8 @@ en: other: You may reset your password %{count} more times. user_not_found: We couldn't find an account with that email address or username. Please try again. status: - ban_notice_html: Your account has been banned. You are not permitted to add or edit archive content. Please %{contact_abuse_link} for more information. - suspension_notice_html: Your account has been suspended until %{suspended_until}. You may not add or edit content until your suspension has been resolved. Please %{contact_abuse_link} for more information. + ban_notice_html: Your account has been banned. You are not permitted to post or edit content on AO3. Please check your email or %{contact_abuse_link} for more information. + suspension_notice_html: Your account has been suspended until %{suspended_until}. You cannot post, edit, or delete content until your suspension has ended. Please check your email or %{contact_abuse_link} for more information. works: drafts: page_title: "%{username} - Drafts" diff --git a/config/locales/mailers/en.yml b/config/locales/mailers/en.yml index 16c85b34011..cc03607e132 100644 --- a/config/locales/mailers/en.yml +++ b/config/locales/mailers/en.yml @@ -181,23 +181,23 @@ en: any: Any assignment: html: You have been assigned the following request in the %{link} challenge at the Archive of Our Own! + text: You have been assigned the following request in the "%{collection_title}" challenge (%{collection_url}) at the Archive of Our Own! description: 'Description:' due: 'This assignment is due at:' - html: - footer: You're receiving this email because you signed up for the %{title} challenge. For more information about this challenge and contact information for the moderators, please visit %{footer_link}. - footer_link: the challenge profile page - look_up: You can look up this assignment from %{link}. - look_up_link: your Assignments page + footer: + challenge_profile: the challenge profile page + html: You're receiving this email because you signed up for the %{title} challenge. For more information about this challenge and contact information for the moderators, please visit %{challenge_profile_link}. + text: You're receiving this email because you signed up for the %{title} challenge (%{url}). For more information about this challenge and contact information for the moderators, please visit %{challenge_profile_url}. + look_up: + html: You can look up this assignment from %{your_assignments_link}. + text: You can look up this assignment from your Assignments page at %{your_assignments_url}. + your_assignments: your Assignments page optional_tags: 'Optional Tags:' prompt_url: 'Prompt URL:' prompts: 'Prompts:' recipient: 'Recipient:' recipient_missing: 'None: contact a moderator for help!' subject: "[%{app_name}][%{collection_title}] Your assignment!" - text: - assignment: You have been assigned the following request in the "%{collection_title}" challenge (%{collection_url}) at the Archive of Our Own! - footer: You're receiving this email because you signed up for the %{title} challenge (%{url}). For more information about this challenge and contact information for the moderators, please visit %{profile_url}. - look_up: You can look up this assignment from your Assignments page at %{link}. change_email: changed: html: "%{login}, the email associated with your account has been changed to %{email}" @@ -380,16 +380,15 @@ en: text: If you would like Open Doors to update the redirect to point to your pre-existing work, please delete the imported copy, and contact Open Doors at %{open_doors_link} with your AO3 account name, your account name on the imported archive, and the title and URL of the fanwork you would like the redirect to point to. (If you have multiple works you would like to change the redirects for, you can list these in one email.) uploaded_list: 'The works uploaded include:' invite_increase_notification: - html: - body: + body: + html: one: We just wanted to let you know that you have %{count} new invitation, which can be used to create a new account at the Archive. You can invite a friend at %{invitation_page_link}. other: We just wanted to let you know that you have %{count} new invitations, which can be used to create new accounts at the Archive. You can invite a friend at %{invitation_page_link}. - invitation_page_link_text: your invitations page - subject: "[%{app_name}] New invitations" - text: - body: + text: one: We just wanted to let you know that you have %{count} new invitation, which can be used to create a new account at the Archive. You can invite a friend at your invitations page (%{invitation_page_url}). other: We just wanted to let you know that you have %{count} new invitations, which can be used to create new accounts at the Archive. You can invite a friend at your invitations page (%{invitation_page_url}). + invitation_page_link_text: your invitations page + subject: "[%{app_name}] New invitations" invite_request_declined: main: one: We regret to inform you that your request for a new invitation cannot be fulfilled at this time. diff --git a/config/locales/models/en.yml b/config/locales/models/en.yml index 17a57e8d66c..41f18db878b 100644 --- a/config/locales/models/en.yml +++ b/config/locales/models/en.yml @@ -16,35 +16,15 @@ en: support: Support tag_wrangling: Tag Wrangling translation: Translation - archive_warning: - name_with_colon: - one: 'Warning:' - other: 'Warnings:' - category: - name_with_colon: - one: 'Category:' - other: 'Categories:' chapters/creatorships: base: 'Invalid creator:' pseud_id: Pseud - character: - name_with_colon: - one: 'Character:' - other: 'Characters:' creatorships: base: 'Invalid creator:' pseud_id: Pseud external_work: author: Creator user_defined_tags_count: Fandom, relationship, and character tags - fandom: - name_with_colon: - one: 'Fandom:' - other: 'Fandoms:' - freeform: - name_with_colon: - one: 'Additional Tag:' - other: 'Additional Tags:' gift_exchange: offers_num_allowed: Number of offers allowed per sign-up offers_num_required: Number of offers required per sign-up @@ -55,12 +35,6 @@ en: meta_tag_id: Metatag sub_tag: Subtag sub_tag_id: Subtag - rating: - name_with_colon: 'Rating:' - relationship: - name_with_colon: - one: 'Relationship:' - other: 'Relationships:' role: archivist: Archivist no_resets: No Resets @@ -94,6 +68,7 @@ en: errors: messages: forbidden: "%{value} is not allowed" + numeric_with_optional_hash: 'may begin with an # and otherwise contain only numbers.' models: abuse_report: attributes: @@ -260,5 +235,7 @@ en: closed_ticket: must not be closed. invalid_department: must be in your department. required: must exist and not be spam. + story_parser: + on_archive: URL is for a work on the Archive. Please bookmark it directly instead. subscriptions: deleted: Deleted item diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml index 00d07232965..98b0f754566 100644 --- a/config/locales/views/en.yml +++ b/config/locales/views/en.yml @@ -1598,7 +1598,6 @@ en: make_default: Make this name default name: Name submit: Submit - ticket_footnote: Numbers only. skins: confirm_delete: confirm_html: Are you sure you want to delete the skin "%{skin_title}"? @@ -1748,9 +1747,6 @@ en: submit: Save edit: about_me: About Me - admin: - ticket_number: - numbers_only: Numbers only. browser_title: Edit Profile change_profile_landmark: Change Profile date_of_birth: Date of Birth diff --git a/features/admins/admin_works.feature b/features/admins/admin_works.feature index 39807cbd650..461773a660a 100644 --- a/features/admins/admin_works.feature +++ b/features/admins/admin_works.feature @@ -330,6 +330,19 @@ Feature: Admin Actions for Works, Comments, Series, Bookmarks Then I should see "rolex" And I should not see "This comment has been marked as spam." + Scenario: Moderated comments cannot be approved by admin + Given the moderated work "Moderation" by "author" + And I am logged in as "commenter" + And I post the comment "Test comment" on the work "Moderation" + When I am logged in as a "superadmin" admin + And I view the work "Moderation" + Then I should see "Unreviewed Comments (1)" + And the comment on "Moderation" should be marked as unreviewed + When I follow "Unreviewed Comments (1)" + Then I should see "Test comment" + And I should not see an "Approve All Unreviewed Comments" button + And I should not see an "Approve" button + Scenario: Admin can edit language on works when posting without previewing Given basic languages And I am logged in as "regular_user" diff --git a/features/gift_exchanges/notification_emails.feature b/features/gift_exchanges/notification_emails.feature index c36449ba17f..5aa2a6ab8db 100644 --- a/features/gift_exchanges/notification_emails.feature +++ b/features/gift_exchanges/notification_emails.feature @@ -1,7 +1,7 @@ Feature: Gift Exchange Notification Emails Make sure that gift exchange notification emails are formatted properly - Scenario: Assignment notification emails should be sent to two owners in their respective locales when assignments are generated + Scenario: Assignment sent notification emails should be sent to two owners in their respective locales when assignments are generated Given I have created the tagless gift exchange "Holiday Swap" And I open signups for "Holiday Swap" @@ -148,3 +148,17 @@ Feature: Gift Exchange Notification Emails Then "participant1" should receive 1 email And the notification message to "participant1" should contain the no archive warnings tag + + Scenario: Assignment notifications should be sent to participants in their respective locales + Given the gift exchange "Holiday Swap" is ready for matching + And a locale with translated emails + And the user "myname1" enables translated emails + When I close signups for "Holiday Swap" + And I have generated matches for "Holiday Swap" + And I have sent assignments for "Holiday Swap" + Then "myname1" should receive 1 email + And the email should have "Your assignment!" in the subject + And the email to "myname1" should be translated + And "myname2" should receive 1 email + And the email should have "Your assignment!" in the subject + And the email to "myname2" should be non-translated diff --git a/features/importing/work_import_errors.feature b/features/importing/work_import_errors.feature index c8e67120575..cc084d33b56 100644 --- a/features/importing/work_import_errors.feature +++ b/features/importing/work_import_errors.feature @@ -15,3 +15,12 @@ Feature: Import Works Then I should see "We couldn't successfully import that work, sorry: We couldn't download anything from http://no-content. Please make sure that the URL is correct and complete, and try again." When I go to my works page Then I should see "Drafts (0)" + + Scenario: Cannot import works from the current archive + Given I set up importing + And I fill in "urls" with "https://archiveofourown.org/works/54711364" + And I select "English" from "Choose a language" + And I press "Import" + Then I should see "We couldn't successfully import that work, sorry: URL is for a work on the Archive. Please bookmark it directly instead." + When I go to my works page + Then I should see "Drafts (0)" diff --git a/features/other_a/invite_request.feature b/features/other_a/invite_request.feature index 09f4ab3ad98..6555b5c5a3b 100644 --- a/features/other_a/invite_request.feature +++ b/features/other_a/invite_request.feature @@ -174,3 +174,15 @@ Feature: Invite requests Then "notuser1" should be emailed And the email should have "Additional invitation request declined" in the subject And the email to "notuser1" should be non-translated + + Scenario: Translated email is sent when new invitation is given to registered user + Given a locale with translated emails + And invitations are required + And the user "user1" exists and is activated + And the user "user1" enables translated emails + And all emails have been delivered + When as "user1" I request some invites + And an admin grants the request + Then "user1" should be emailed + And the email should have "New invitations" in the subject + And the email to "user1" should be translated \ No newline at end of file diff --git a/features/other_a/page_title.feature b/features/other_a/page_title.feature index aa1eac9d0da..d192f9ecdb6 100644 --- a/features/other_a/page_title.feature +++ b/features/other_a/page_title.feature @@ -12,22 +12,34 @@ Scenario: user reads a TOS or FAQ page Scenario: Page title should respect user preference Given I am logged in as "author" - And I go to my preferences page - And I fill in "Browser page title format" with "FANDOM - AUTHOR - TITLE" - And I press "Update" - And I post the work "New Story" with fandom "Stargate" + And I go to my preferences page + And I fill in "Browser page title format" with "FANDOM - AUTHOR - TITLE" + And I press "Update" + And I post the work "New Story" with fandom "Stargate" When I view the work "New Story" Then the page title should include "Stargate - author - New Story" Scenario: Page title should change when tags are edited Given I am logged in as "author" - And I post the work "New Story" with fandom "Stargate" + And I post the work "New Story" with fandom "Stargate" When I view the work "New Story" Then the page title should include "Stargate" When I edit the work "New Story" - And I fill in "Fandoms" with "Harry Potter" - And I press "Post" + And I fill in "Fandoms" with "Harry Potter" + And I press "Post" When I view the work "New Story" Then the page title should include "Harry Potter" - And the page title should not include "Stargate" + And the page title should not include "Stargate" + +Scenario: Page title should be informative on the adult content notice page + + Given I am logged in as "author" + And I post the 2 chapter work "New Story" with fandom "Stargate" with rating "Mature" + When I am logged out + And I view the work "New Story" + Then I should see "This work could have adult content" + And the page title should include "New Story - author - Stargate" + When I follow the recent chapter link for the work "New Story" + Then I should see "This work could have adult content" + And the page title should include "New Story - Chapter 2 - author - Stargate" diff --git a/features/other_a/profile_edit.feature b/features/other_a/profile_edit.feature index 1f9e0e6b593..deb3c532e24 100644 --- a/features/other_a/profile_edit.feature +++ b/features/other_a/profile_edit.feature @@ -42,7 +42,7 @@ Scenario: Change details as an admin And I fill in "About Me" with "is it merely thy habit, to talk to dolls?" And I fill in "Ticket ID" with "fine" And I press "Update" - Then I should see "Ticket ID is not a number" + Then I should see "may begin with an # and otherwise contain only numbers." And the field labeled "Ticket ID" should contain "fine" When I fill in "Ticket ID" with "480000" And I press "Update" diff --git a/features/other_a/pseuds.feature b/features/other_a/pseuds.feature index 077c1407e00..37826b9d433 100644 --- a/features/other_a/pseuds.feature +++ b/features/other_a/pseuds.feature @@ -263,9 +263,18 @@ Scenario: Change details as an admin And I fill in "Description" with "I'd probably be removing text." And I fill in "Ticket ID" with "no 💜" And I press "Update" - Then I should see "Ticket ID is not a number" + Then I should see "may begin with an # and otherwise contain only numbers" And the field labeled "Ticket ID" should contain "no 💜" + When I fill in "Ticket ID" with "#4798454#331" + And I press "Update" + Then I should see "may begin with an # and otherwise contain only numbers" + And the field labeled "Ticket ID" should contain "4798454#331" When I fill in "Ticket ID" with "47" + And I press "Update" + Then I should see "Pseud was successfully updated." + When I go to someone's pseuds page + And I follow "Edit alt" + When I fill in "Ticket ID" with "#47" And I press "Update" Then I should see "Pseud was successfully updated." When I go to someone's pseuds page diff --git a/public/help/skins-creating.html b/public/help/skins-creating.html index f20a539a843..26176462ee3 100644 --- a/public/help/skins-creating.html +++ b/public/help/skins-creating.html @@ -112,7 +112,7 @@
URLs

- We allow external image URLs (specified as url('http://somesite.com/my_awesome_image.jpg')) in JPG, GIF, and PNG formats. + We allow external image URLs (specified as url('https://example.com/my_awesome_image.jpg')) in JPG, GIF, and PNG formats. Please note, however, that skins using external images will not be approved for public use.

diff --git a/public/javascripts/mce_editor.js b/public/javascripts/mce_editor.js index b631fee3c61..ca2b778ef5d 100644 --- a/public/javascripts/mce_editor.js +++ b/public/javascripts/mce_editor.js @@ -81,7 +81,7 @@ tinyMCE.init({ editor.on('init change undo redo', function() { editor.save(); $j(editor.targetElm).trigger('change'); - }) + }); } }); diff --git a/public/tolk/reset.css b/public/tolk/reset.css deleted file mode 100644 index f210b720e8a..00000000000 --- a/public/tolk/reset.css +++ /dev/null @@ -1,30 +0,0 @@ -/*------------------------------------------------- -RESET --------------------------------------------------*/ - -body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td { - margin:0; - padding:0; -} -table { - border-collapse:collapse; - border-spacing:0; -} -fieldset,img {border:0;} -address,caption,cite,code,dfn,th,var { - font-style:normal; - font-weight:normal; -} - -h1,h2,h3,h4,h5,h6 { - font-size:100%; - font-weight:normal; -} - -ol,ul {list-style:none;} -caption,th {text-align:left;} -q:before,q:after {content:'';} -abbr,acronym {border:0;} - -img {border: none;} -em em {font-style: normal;} \ No newline at end of file diff --git a/public/tolk/screen.css b/public/tolk/screen.css deleted file mode 100644 index 6edda910ecb..00000000000 --- a/public/tolk/screen.css +++ /dev/null @@ -1,336 +0,0 @@ -/*------------------------------------------------- -Defaults --------------------------------------------------*/ - -acronym, abbr { - font-variant: small-caps; - text-transform: lowercase; - font-weight: bold; -} - -.center {text-align: center;} -.large {font-size: larger;} -.small {font-size: smaller;} -strong {font-weight: bold;} -em {font-style: italic;} - -.clear {clear: both;} -.clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } -.clearfix { display: inline-block; } -.clearfix{ display: block; } - -a {color: #888;} -a:hover {color: #000;} - -/*------------------------------------------------- -Layout --------------------------------------------------*/ - -body { - font-family: "Lucida Sans", "Lucida Grande", "Lucida Sans Unicode", sans-serif; - background: #e5e5e5; - color: #333; - margin: 0; - padding: 0; - font-size: 14px; - line-height: 21px; - text-align: left; -} - -div#container { - margin: 2% 4%; - padding: 25px; - background: #fff; - -webkit-border-radius: 20px; - -moz-border-radius: 20px; - border-radius: 20px; - box-shadow: 0px 0px 15px rgba(00,00,00,0.5); - -moz-box-shadow: 0px 0px 15px rgba(00,00,00,0.5); - -webkit-box-shadow: 0px 0px 15px rgba(00,00,00,0.5); -} - -div#head { - margin: -25px -25px 0; - background: #111; - color: #999; - padding: 25px 25px 20px 15px; - -webkit-border-top-left-radius: 20px; - -webkit-border-top-right-radius: 20px; - -moz-border-radius-topleft: 20px; - -moz-border-radius-topright: 20px; - border-top-left-radius: 20px; - border-top-right-radius: 20px; - font-size: 18px; -} - -div#head a { - color: #2fadcf; - font-weight: bold; - text-decoration: none; -} - -div#head span.home { - -webkit-border-top-left-radius: 10px; - -webkit-border-bottom-left-radius: 10px; - -moz-border-radius-topleft: 10px; - -moz-border-radius-bottomleft: 10px; - border-top-left-radius: 10px; - border-bottom-left-radius: 10px; - background: #333; - padding: 8px 6px 8px 12px; - margin-right: 1px; -} - -div#head span.locale { - color: #fff; - font-weight: bold; - background: #444; - padding: 8px 12px 8px 6px; - -webkit-border-top-right-radius: 10px; - -webkit-border-bottom-right-radius: 10px; - -moz-border-radius-topright: 10px; - -moz-border-radius-bottomright: 10px; - border-top-right-radius: 10px; - border-bottom-right-radius: 10px; -} - -div#head span.locale.empty { - background: #333; - padding-left: 0; - margin-left: -6px; -} - -div#head span.locale em { - font-style: normal; - font-weight: normal; -} - -div#head span.note { - color: #777; - font-size: 13px; - font-weight: normal; - margin-left: 10px; -} - -h2, -h3 { - font-size: 18px; - color: #2fadcf; - margin: 25px 0 10px; -} - -h2 span, -h3 span { - font-size: 13px; - color: #888; -} - -ul.locales { - margin: 25px 0; -} - -ul.locales li { - /*width: 230px;*/ - width: 150px; - display: block; - float: left; - margin: 0 10px 10px 0; - /* background: #fff; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; - border: 1px solid #e5e5e5; - padding: 5px 10px;*/ -} - -ul.locales a { - font-weight: bold; - text-decoration: underline; - color: #2fadcf; -} - -/*ul.locales li:hover { - background: #f5f5f5; - border-color: #ccc; -}*/ - -ul.locales span.missing_translations { - color: #fff; - font-weight: bold; - background: #c00; - font-size: 9px; - padding: 3px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - line-height: 9px; - vertical-align: top; - display: inline-block; -} - -ul.locales span { - font-size: 13px; - color: #666; -} - -div.submit { - background: #f5f5f5; - margin: 25px -25px -25px; - padding: 15px 25px; - border-top: 1px dashed #ccc; - -webkit-border-bottom-right-radius: 20px; - -webkit-border-bottom-left-radius: 20px; - -moz-border-radius-bottomright: 20px; - -moz-border-radius-bottomleft: 20px; - border-bottom-right-radius: 20px; - border-bottom-left-radius: 20px; -} - -span.updated { - font-size: 11px; - padding: 1px; - background: #ffc; - color: #777; - margin-bottom: 10px; - float: right; -} - -div.table_submit { - background: #f5f5f5; - margin: -25px 0 0; - padding: 12px 15px 15px; - text-align: left; -} - -div.translations { - width: 96%; - text-align: center; -} - -span.notice { - background: #ffc; - color: #666; - font-size: 12px; - padding: 2px 5px; - margin: -5px 0 15px; - display: inline-block; -} - -div.original { - color: #999; - margin: 5px 0; - padding: 1px 8px 4px; -} - -div.updated { - background: #ffc; - padding: 1px 8px 4px; -} - -table.translations div.original span.key { - margin: 0 0 -2px; - padding: 0; -} - -table.translations div.updated span.key { - margin: 0 0 -2px; - color: orange !important; - padding: 0; -} - -/*------------------------------------------------- -Translation tables --------------------------------------------------*/ - -table.translations { - margin: 0 0 25px; - width: 100%; - text-align: left; -} - -table.translations td, -table.translations th { - font-size: 14px; - color: #222; - padding: 12px 8px; - border-bottom: 1px solid #e5e5e5; - vertical-align: top; - width: 50%; -} - -table.translations th { - border-bottom-color: #bbb; - font-size: 11px; - font-weight: bold; - text-transform: uppercase; - color: #999; - padding-bottom: 2px; -} - -table.translations textarea.locale { - font-family: "Lucida Sans", "Lucida Grande", "Lucida Sans Unicode", sans-serif; - font-size: 14px; - line-height: 21px; - color: #222; - padding: 1px; - width: 100%; - height: 42px; -} - -table.translations span.key { - color: #aaa; - font-size: 9px; - display: block; -} - -table.translations td.translation { - padding: 10px 8px; -} - -table.translations tr.active td { - background: #edf9fe; -} - -table.translations .highlight { - background-color: yellow; -} - -/*------------------------------------------------- -Pagination --------------------------------------------------*/ - -div.paginate { - margin: 15px auto 20px; - font-size: 12px; - color: #777; -} - -div.paginate a, -div.paginate span { - padding: 2px 6px; - text-decoration: none; -} - -div.paginate a:hover, -div.paginate span.current { - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; - background: #eee; - color: #333; -} - -div.paginate .next_page, -div.paginate .prev_page { - margin: 0 15px; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; - border: 1px solid #bbb; - padding: 4px 8px; -} - -div.paginate .disabled { - color: #ccc; - border-color: #eee; -} diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index b0f209dc798..0ec3abe00ef 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -377,7 +377,7 @@ let!(:collection) { create(:collection, challenge: gift_exchange, challenge_type: "GiftExchange") } let!(:otheruser) { create(:user) } let!(:offer) { create(:challenge_signup, collection: collection, pseud: otheruser.default_pseud) } - let!(:open_assignment) { create(:challenge_assignment, collection: collection, offer_signup: offer) } + let!(:open_assignment) { create(:challenge_assignment, collection: collection, offer_signup: offer, sent_at: Time.current) } # Test the headers it_behaves_like "an email with a valid sender" diff --git a/spec/models/concerns/justifiable_spec.rb b/spec/models/concerns/justifiable_spec.rb index b023e56a6f0..2e68cd2bd48 100644 --- a/spec/models/concerns/justifiable_spec.rb +++ b/spec/models/concerns/justifiable_spec.rb @@ -26,7 +26,7 @@ record.assign_attributes(attributes) expect(record).not_to be_valid - expect(record.errors[:ticket_number]).to contain_exactly("can't be blank", "is not a number") + expect(record.errors[:ticket_number]).to contain_exactly("can't be blank", "may begin with an # and otherwise contain only numbers.") expect(record.ticket_url).to be_nil end diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb index 7aa217d946d..802b0ce4a0c 100644 --- a/test/mailers/previews/user_mailer_preview.rb +++ b/test/mailers/previews/user_mailer_preview.rb @@ -34,6 +34,65 @@ def feedback UserMailer.feedback(feedback.id) end + # Sent by gift exchanges to the participants + # Variant with tag fields set to "Any" and no due date + # URL: /rails/mailers/user_mailer/challenge_assignment_notification_any?sent_at=2025-01-23T20:00 + def challenge_assignment_notification_any + assignment = create(:challenge_assignment, sent_at: (params[:sent_at] ? params[:sent_at].to_time : Time.current)) + + signup = assignment.request_signup + signup.update(pseud: create(:user, :for_mailer_preview).default_pseud) + + # Fill all tag fields with "Any" + prompt = signup.requests.first + TagSet::TAG_TYPES.each do |type| + prompt.send(:"any_#{type}=", true) + end + prompt.title = "This is a title" + prompt.save! + + UserMailer.challenge_assignment_notification(assignment.collection.id, assignment.offering_user.id, assignment.id) + end + + # Sent by gift exchanges to the participants + # Variant with flexible due date, 3 tags per type and all fields filled out + # URL: /rails/mailers/user_mailer/challenge_assignment_notification_filled?sent_at=2025-01-23T20:00&due=2021-12-15T13:45 + def challenge_assignment_notification_filled + assignment = create(:challenge_assignment, sent_at: (params[:sent_at] ? params[:sent_at].to_time : Time.current)) + + challenge = assignment.collection.challenge + challenge.update(assignments_due_at: params[:due] ? params[:due].to_time : Time.current) + + signup = assignment.request_signup + signup.update(pseud: create(:user, :for_mailer_preview).default_pseud) + + # Allow up to 3 tags per type + request_restriction = challenge.request_restriction + TagSet::TAG_TYPES.each do |type| + request_restriction.send(:"#{type}_num_allowed=", 3) + end + request_restriction.save! + + # Tag set with 3 tags per type + tag_set = create(:tag_set, tags: []) + tag_set.archive_warning_tagnames = [ArchiveConfig.WARNING_VIOLENCE_TAG_NAME, ArchiveConfig.WARNING_DEATH_TAG_NAME, ArchiveConfig.WARNING_NONCON_TAG_NAME].join(ArchiveConfig.DELIMITER_FOR_OUTPUT) + tag_set.rating_tagnames = [ArchiveConfig.RATING_EXPLICIT_TAG_NAME, ArchiveConfig.RATING_MATURE_TAG_NAME, ArchiveConfig.RATING_TEEN_TAG_NAME].join(ArchiveConfig.DELIMITER_FOR_OUTPUT) + tag_set.category_tagnames = [ArchiveConfig.CATEGORY_GEN_TAG_NAME, ArchiveConfig.CATEGORY_HET_TAG_NAME, ArchiveConfig.CATEGORY_SLASH_TAG_NAME].join(ArchiveConfig.DELIMITER_FOR_OUTPUT) + %w[fandom character relationship freeform].each do |type| + tag_set.tags += [create(:"canonical_#{type}"), create(:"canonical_#{type}"), create(:"canonical_#{type}")] + end + tag_set.save! + + prompt = signup.requests.first + prompt.tag_set = tag_set + prompt.title = "This is a title" + prompt.url = "https://example.com/" + prompt.optional_tag_set = create(:tag_set, tags: [create(:freeform), create(:freeform), create(:freeform)]) + prompt.save! + + UserMailer.challenge_assignment_notification(assignment.collection.id, assignment.offering_user.id, assignment.id) + end + def claim_notification work = create(:work) creator_id = work.pseuds.first.user.id @@ -54,6 +113,12 @@ def change_email UserMailer.change_email(user.id, old_email, new_email) end + def invite_increase_notification + user = create(:user, :for_mailer_preview) + total = params[:total] || 1 + UserMailer.invite_increase_notification(user.id, total.to_i) + end + private def creatorship_notification_data(creation_type)