diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 9834213e1..31511217e 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -57,3 +57,4 @@ //= require vertices //= require watchlists //= require turbolinks +//= require search_tags \ No newline at end of file diff --git a/app/assets/javascripts/search_tags.js b/app/assets/javascripts/search_tags.js new file mode 100644 index 000000000..2aa798305 --- /dev/null +++ b/app/assets/javascripts/search_tags.js @@ -0,0 +1,19 @@ +$(document).on("turbolinks:load", function () { + $("#search_all_tags").change(evt => toggleSearchAllTags(evt)); +}); + +/** + * Dynamically enable/disable the OR/AND buttons in the media search form. + * If the user has decided to search for media regardless of the tags, + * i.e. they enable the "all" (tags) button, we disable the "OR/AND" buttons + * as it is pointless to search for media that references *all* available tags + * at once. + */ +function toggleSearchAllTags(evt) { + const searchAllTags = evt.target.checked; + if (searchAllTags) { + $("#search_tag_operator_or").prop("checked", true); + } + $("#search_tag_operator_or").prop("disabled", searchAllTags); + $("#search_tag_operator_and").prop("disabled", searchAllTags); +} diff --git a/app/controllers/erdbeere_controller.rb b/app/controllers/erdbeere_controller.rb index 23c78601e..f47ec1daa 100644 --- a/app/controllers/erdbeere_controller.rb +++ b/app/controllers/erdbeere_controller.rb @@ -8,7 +8,7 @@ def current_ability end def show_example - response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/examples/#{params[:id]}") + response = Faraday.get(ENV.fetch("ERDBEERE_API") + "/examples/#{params[:id]}") @content = if response.status == 200 JSON.parse(response.body)["embedded_html"] else @@ -17,7 +17,7 @@ def show_example end def show_property - response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/properties/#{params[:id]}") + response = Faraday.get(ENV.fetch("ERDBEERE_API") + "/properties/#{params[:id]}") @content = if response.status == 200 JSON.parse(response.body)["embedded_html"] @@ -28,7 +28,7 @@ def show_property def show_structure params[:id] - response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/structures/#{params[:id]}") + response = Faraday.get(ENV.fetch("ERDBEERE_API") + "/structures/#{params[:id]}") @content = if response.status == 200 JSON.parse(response.body)["embedded_html"] else @@ -51,7 +51,7 @@ def cancel_edit_tags def display_info @id = params[:id] @sort = params[:sort] - response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + + response = Faraday.get(ENV.fetch("ERDBEERE_API") + "/#{@sort.downcase.pluralize}/#{@id}/view_info") @content = JSON.parse(response.body) if response.status != 200 @@ -87,7 +87,7 @@ def update_tags end def fill_realizations_select - response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/structures/") + response = Faraday.get("#{ENV.fetch("ERDBEERE_API")}/structures/") @tag = Tag.find_by(id: params[:id]) hash = JSON.parse(response.body) @structures = hash["data"].map do |d| @@ -102,7 +102,7 @@ def fill_realizations_select end def find_example - response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/find?#{find_params.to_query}") + response = Faraday.get("#{ENV.fetch("ERDBEERE_API")}/find?#{find_params.to_query}") @content = if response.status == 200 JSON.parse(response.body)["embedded_html"] else diff --git a/app/controllers/lectures_controller.rb b/app/controllers/lectures_controller.rb index d108fbbc0..a7196ea79 100644 --- a/app/controllers/lectures_controller.rb +++ b/app/controllers/lectures_controller.rb @@ -217,7 +217,7 @@ def edit_structures def search_examples if @lecture.structure_ids.any? - response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/search") + response = Faraday.get("#{ENV.fetch("ERDBEERE_API")}/search") @form = JSON.parse(response.body)["embedded_html"] # rubocop:disable Style/StringConcatenation @form.gsub!("token_placeholder", @@ -402,7 +402,7 @@ def eager_load_stuff def set_erdbeere_data @structure_ids = @lecture.structure_ids - response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/structures") + response = Faraday.get("#{ENV.fetch("ERDBEERE_API")}/structures") response_hash = if response.status == 200 JSON.parse(response.body) else diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 23fd99052..a8efdbd2c 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -228,21 +228,6 @@ def search results = search.results @total = search.total - # in the case of a search with tag_operator 'or', we - # execute two searches and merge the results, where media - # with the selected tags are now shown at the front of the list - if (search_params[:tag_operator] == "or") \ - && (search_params[:all_tags] == "0") \ - && (search_params[:fulltext].size >= 2) - params["search"]["all_tags"] = "1" - search_no_tags = Medium.search_by(search_params, params[:page]) - search_no_tags.execute - results_no_tags = search_no_tags.results - results = (results + results_no_tags).uniq - @total = results.size - params["search"]["all_tags"] = "0" - end - if filter_media search_arel = Medium.where(id: results.pluck(:id)) visible_search_results = current_user.filter_visible_media(search_arel) diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index bc9152b78..939f2d56b 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -9,12 +9,12 @@ def verify_captcha return true unless ENV["USE_CAPTCHA_SERVICE"] begin - uri = URI.parse(ENV.fetch("CAPTCHA_VERIFY_URL", nil)) + uri = URI.parse(ENV.fetch("CAPTCHA_VERIFY_URL")) data = { message: params["frc-captcha-solution"], - application_token: ENV.fetch("CAPTCHA_APPLICATION_TOKEN", nil) } + application_token: ENV.fetch("CAPTCHA_APPLICATION_TOKEN") } header = { "Content-Type": "text/json" } http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true if ENV["CAPTCHA_VERIFY_URL"].include?("https") + http.use_ssl = true if ENV.fetch("CAPTCHA_VERIFY_URL").include?("https") request = Net::HTTP::Post.new(uri.request_uri, header) request.body = data.to_json @@ -70,9 +70,9 @@ def after_sign_up_path_for(_resource) private def check_registration_limit - timeframe = ((ENV["MAMPF_REGISTRATION_TIMEFRAME"] || 15).to_i.minutes.ago..) + timeframe = (ENV.fetch("MAMPF_REGISTRATION_TIMEFRAME", 15).to_i.minutes.ago..) num_new_registrations = User.where(confirmed_at: nil, created_at: timeframe).count - max_registrations = (ENV["MAMPF_MAX_REGISTRATION_PER_TIMEFRAME"] || 40).to_i + max_registrations = ENV.fetch("MAMPF_MAX_REGISTRATION_PER_TIMEFRAME", 40).to_i return if num_new_registrations <= max_registrations # Current number of new registrations is too high diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 50f6b333b..f267191e3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -16,7 +16,7 @@ def current_lecture def host if Rails.env.production? # rubocop:disable Style/StringConcatenation - ENV.fetch("MEDIA_SERVER", nil) + "/" + ENV.fetch("INSTANCE_NAME", nil) + ENV.fetch("MEDIA_SERVER") + "/" + ENV.fetch("INSTANCE_NAME") # rubocop:enable Style/StringConcatenation else "" @@ -29,7 +29,7 @@ def host # the actual media server. # This is used for the download buttons for videos and manuscripts. def download_host - Rails.env.production? ? ENV.fetch("DOWNLOAD_LOCATION", nil) : "" + Rails.env.production? ? ENV.fetch("DOWNLOAD_LOCATION") : "" end # Returns the full title on a per-page basis. diff --git a/app/mailers/exception_handler/exception_mailer.rb b/app/mailers/exception_handler/exception_mailer.rb index 84db08e96..f56a25431 100644 --- a/app/mailers/exception_handler/exception_mailer.rb +++ b/app/mailers/exception_handler/exception_mailer.rb @@ -5,7 +5,7 @@ class ExceptionMailer < ApplicationMailer # Defaults default subject: I18n.t("exception.exception", - host: ENV.fetch("URL_HOST", nil)) + host: ENV.fetch("URL_HOST")) default from: ExceptionHandler.config.email default template_path: "exception_handler/mailers" # => http://stackoverflow.com/a/18579046/1143732 diff --git a/app/models/medium.rb b/app/models/medium.rb index 0fe6faaa5..645371915 100644 --- a/app/models/medium.rb +++ b/app/models/medium.rb @@ -301,9 +301,6 @@ def self.search_by(search_params, _page) search_params[:all_terms] = "1" if search_params[:all_terms].blank? search_params[:all_teachers] = "1" if search_params[:all_teachers].blank? search_params[:term_ids].push("0") if search_params[:term_ids].present? - if search_params[:all_tags] == "1" && search_params[:tag_operator] == "and" - search_params[:tag_ids] = Tag.pluck(:id) - end user = User.find_by(id: search_params[:user_id]) search = Sunspot.new_search(Medium) search.build do @@ -336,15 +333,12 @@ def self.search_by(search_params, _page) with(:release_state, search_params[:access]) end end - if !search_params[:all_tags] == "1" && - !search_params[:tag_operator] == "or" && (search_params[:tag_ids]) - if search_params[:tag_operator] == "or" || search_params[:all_tags] == "1" - search.build do - with(:tag_ids).any_of(search_params[:tag_ids]) - end - else - search.build do + if search_params[:all_tags] == "0" && search_params[:tag_ids].any? + search.build do + if search_params[:tag_operator] == "and" with(:tag_ids).all_of(search_params[:tag_ids]) + else + with(:tag_ids).any_of(search_params[:tag_ids]) end end end diff --git a/app/models/user_cleaner.rb b/app/models/user_cleaner.rb index 877760420..8a5763e53 100644 --- a/app/models/user_cleaner.rb +++ b/app/models/user_cleaner.rb @@ -3,9 +3,9 @@ class UserCleaner attr_accessor :imap, :email_dict, :hash_dict def login - @imap = Net::IMAP.new(ENV.fetch("IMAPSERVER", nil), port: 993, ssl: true) - @imap.authenticate("LOGIN", ENV.fetch("PROJECT_EMAIL_USERNAME", nil), - ENV.fetch("PROJECT_EMAIL_PASSWORD", nil)) + @imap = Net::IMAP.new(ENV.fetch("IMAPSERVER"), port: 993, ssl: true) + @imap.authenticate("LOGIN", ENV.fetch("PROJECT_EMAIL_USERNAME"), + ENV.fetch("PROJECT_EMAIL_PASSWORD")) end def logout @@ -15,7 +15,7 @@ def logout def search_emails_and_hashes @email_dict = {} @hash_dict = {} - @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX", nil)) + @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX")) # Mails containing multiple email addresses (Subject: "Undelivered Mail Returned to Sender") @imap.search(["SUBJECT", "Undelivered Mail Returned to Sender"]).each do |message_id| @@ -89,11 +89,11 @@ def send_hashes end def delete_ghosts - @hash_dict.each do |mail, hash| - u = User.find_by(email: mail, ghost_hash: hash) - move_mail(@email_dict[mail]) if u.present? && @email_dict.present? - u.destroy! if u&.generic? - end + # @hash_dict.each do |mail, hash| + # u = User.find_by(email: mail, ghost_hash: hash) + # move_mail(@email_dict[mail]) if u.present? && @email_dict.present? + # u.destroy! if u&.generic? + # end end def move_mail(message_ids, attempt = 0) @@ -103,7 +103,7 @@ def move_mail(message_ids, attempt = 0) return if attempt > 3 begin - @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX", nil)) + @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX")) @imap.move(message_ids, "Other Users/mampf/handled_bounces") rescue Net::IMAP::BadResponseError move_mail(message_ids, attempt + 1) @@ -111,14 +111,15 @@ def move_mail(message_ids, attempt = 0) end def clean! - login - search_emails_and_hashes - return if @email_dict.blank? - - send_hashes - sleep(10) - search_emails_and_hashes - delete_ghosts - logout + # TODO: Implement new user cleaner logic + # login + # search_emails_and_hashes + # return if @email_dict.blank? + + # send_hashes + # sleep(10) + # search_emails_and_hashes + # delete_ghosts + # logout end end diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 2cb17d595..2e47ea0f0 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -33,13 +33,13 @@ target: :_blank)), class: "d-inline", for: "dsgvo-consent" %> <%= f.hidden_field :locale, value: I18n.locale %> - <% if ENV['USE_CAPTCHA_SERVICE']%> + <% if ENV["USE_CAPTCHA_SERVICE"]%>

-

+

<%end %>
- <%= f.submit t('.sign_up'), class: 'btn btn-primary', id: 'register-user', disabled:ENV['USE_CAPTCHA_SERVICE'] %> + <%= f.submit t('.sign_up'), class: 'btn btn-primary', id: 'register-user', disabled:ENV.fetch("USE_CAPTCHA_SERVICE") %>
<% end %> diff --git a/app/views/exception_handler/mailers/new_exception.html.erb b/app/views/exception_handler/mailers/new_exception.html.erb index 93b3b9b89..0ebfbbe16 100644 --- a/app/views/exception_handler/mailers/new_exception.html.erb +++ b/app/views/exception_handler/mailers/new_exception.html.erb @@ -1,5 +1,5 @@ <%= t('exception.exception_report', - host: ENV['URL_HOST']) %> + host: ENV.fetch("URL_HOST")) %> <%= @exception.response %> (<%= @exception.status %>) diff --git a/app/views/main/start/_media_search.html.erb b/app/views/main/start/_media_search.html.erb index b836981b2..2a0f2dcfa 100644 --- a/app/views/main/start/_media_search.html.erb +++ b/app/views/main/start/_media_search.html.erb @@ -62,7 +62,8 @@ <%= f.radio_button :tag_operator, 'or', checked: true, - class: 'form-check-input' %> + class: 'form-check-input', + disabled: true %> <%= f.label :tag_operator, t('basics.OR'), value: 'or', @@ -71,7 +72,8 @@
<%= f.radio_button :tag_operator, 'and', - class: 'form-check-input' %> + class: 'form-check-input', + disabled: true %> <%= f.label :tag_operator, t('basics.AND'), value: 'and', diff --git a/app/views/media/catalog/_search_form.html.erb b/app/views/media/catalog/_search_form.html.erb index d831fae4e..8d554400b 100644 --- a/app/views/media/catalog/_search_form.html.erb +++ b/app/views/media/catalog/_search_form.html.erb @@ -105,7 +105,8 @@ <%= f.radio_button :tag_operator, 'or', checked: true, - class: 'form-check-input' %> + class: 'form-check-input', + disabled: true %> <%= f.label :tag_operator, t('basics.OR'), value: 'or', @@ -114,7 +115,8 @@
<%= f.radio_button :tag_operator, 'and', - class: 'form-check-input' %> + class: 'form-check-input', + disabled: true %> <%= f.label :tag_operator, t('basics.AND'), value: 'and', diff --git a/app/views/media/get_statistics.coffee b/app/views/media/statistics.coffee similarity index 100% rename from app/views/media/get_statistics.coffee rename to app/views/media/statistics.coffee diff --git a/app/workers/interaction_saver.rb b/app/workers/interaction_saver.rb index f6c95faf1..344d53830 100644 --- a/app/workers/interaction_saver.rb +++ b/app/workers/interaction_saver.rb @@ -2,8 +2,8 @@ class InteractionSaver include Sidekiq::Worker def perform(session_id, full_path, referrer, study_participant) - referrer_url = if referrer.to_s.include?(ENV["URL_HOST"]) - referrer.to_s.remove(ENV["URL_HOST"]) + referrer_url = if referrer.to_s.include?(ENV.fetch("URL_HOST")) + referrer.to_s.remove(ENV.fetch("URL_HOST")) .remove("https://").remove("http://") end Interaction.create(session_id: Digest::SHA2.hexdigest(session_id).first(10), diff --git a/config/application.rb b/config/application.rb index 15040fd01..b5e4e0f1e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -24,7 +24,7 @@ class Application < Rails::Application # the framework and any gems in your application. config.exception_handler = { # sends exception emails to a listed email (string // "you@email.com") - email: ENV.fetch("ERROR_EMAIL", nil), + email: ENV.fetch("ERROR_EMAIL"), # All keys interpolated as strings, so you can use # symbols, strings or integers where necessary diff --git a/config/environments/production.rb b/config/environments/production.rb index 805b5c66e..ae9c17db1 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -55,13 +55,13 @@ config.log_tags = [:request_id] # Use a different cache store in production. - config.cache_store = :mem_cache_store, ENV.fetch("MEMCACHED_SERVER", nil) + config.cache_store = :mem_cache_store, ENV.fetch("MEMCACHED_SERVER") # Use a real queuing backend for Active Job (and separate queues per environment) # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "mampf_#{Rails.env}" config.action_mailer.perform_caching = false - config.action_mailer.default_url_options = { protocol: "https", host: ENV.fetch("URL_HOST", nil) } + config.action_mailer.default_url_options = { protocol: "https", host: ENV.fetch("URL_HOST") } config.action_mailer.delivery_method = :smtp config.action_mailer.perform_deliveries = true @@ -69,9 +69,10 @@ config.action_mailer.default(charset: "utf-8") config.action_mailer.smtp_settings = { - address: ENV.fetch("MAILSERVER", nil), + address: ENV.fetch("MAILSERVER"), port: 25, - domain: ENV.fetch("MAILSERVER", nil) + user_name: ENV.fetch("MAMPF_EMAIL_USERNAME"), + password: ENV.fetch("MAMPF_EMAIL_PASSWORD") } # Ignore bad email addresses and do not raise email delivery errors. diff --git a/config/initializers/default_setting.rb b/config/initializers/default_setting.rb index 5fd3bb048..a9c860b45 100644 --- a/config/initializers/default_setting.rb +++ b/config/initializers/default_setting.rb @@ -1,11 +1,11 @@ class DefaultSetting - ERDBEERE_LINK = ENV.fetch("ERDBEERE_SERVER", nil) - MUESLI_LINK = ENV.fetch("MUESLI_SERVER", nil) - PROJECT_EMAIL = ENV.fetch("PROJECT_EMAIL", nil) - FEEDBACK_EMAIL = ENV.fetch("FEEDBACK_EMAIL", nil) - PROJECT_NOTIFICATION_EMAIL = ENV.fetch("PROJECT_NOTIFICATION_EMAIL", nil) - BLOG_LINK = ENV.fetch("BLOG", nil) - URL_HOST_SHORT = ENV.fetch("URL_HOST_SHORT", nil) + ERDBEERE_LINK = ENV.fetch("ERDBEERE_SERVER") + MUESLI_LINK = ENV.fetch("MUESLI_SERVER") + PROJECT_EMAIL = ENV.fetch("PROJECT_EMAIL") + FEEDBACK_EMAIL = ENV.fetch("FEEDBACK_EMAIL") + PROJECT_NOTIFICATION_EMAIL = ENV.fetch("PROJECT_NOTIFICATION_EMAIL") + BLOG_LINK = ENV.fetch("BLOG") + URL_HOST_SHORT = ENV.fetch("URL_HOST_SHORT") RESEARCHGATE_LINK = "https://www.researchgate.net/project/MaMpf-Mathematische-Medienplattform".freeze TOUR_LINK = "https://mampf.blog/ueber-mampf/".freeze RESOURCES_LINK = "https://mampf.blog/ressourcen-fur-editorinnen/".freeze diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index c3b179a48..d900ea888 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -12,7 +12,7 @@ # note that it will be overwritten if you use your own mailer class # with default "from" parameter. config.mailer_sender = if Rails.env.production? - ENV.fetch("FROM_ADDRESS", nil) + ENV.fetch("FROM_ADDRESS") else "please-change-me-at-config-initializers-devise@example.com" end diff --git a/config/initializers/thredded.rb b/config/initializers/thredded.rb index 75f4539e3..30b0ae4a3 100644 --- a/config/initializers/thredded.rb +++ b/config/initializers/thredded.rb @@ -190,4 +190,4 @@ # # add in (must install separate gem (under development) as well): # Thredded.notifiers = [Thredded::EmailNotifier.new, -# Thredded::PushoverNotifier.new(ENV.fetch("PUSHOVER_APP_ID", nil))] +# Thredded::PushoverNotifier.new(ENV.fetch("PUSHOVER_APP_ID"))] diff --git a/db/migrate/20240116180000_fix_unread_comments_inconsistencies.rb b/db/migrate/20240116180000_fix_unread_comments_inconsistencies.rb index 74ef88755..9d1a0eb19 100644 --- a/db/migrate/20240116180000_fix_unread_comments_inconsistencies.rb +++ b/db/migrate/20240116180000_fix_unread_comments_inconsistencies.rb @@ -10,45 +10,80 @@ # the unread_comments flag. class FixUnreadCommentsInconsistencies < ActiveRecord::Migration[7.0] def up - num_fixed_users = 0 + # 🛑 This migration contains a bug, please don't use it again. Instead, + # refer to the migration in 20240215100000_fix_unread_comments_inconsistencies_again.rb. + # + # 💡 Explanation of the bug: + # The bug resides within the method `user_unread_comments?`. It was supposed + # to reflect the logic of the method `comments` in app/controllers/main_controller.rb: + # + # def comments + # @media_comments = current_user.subscribed_media_with_latest_comments_not_by_creator + # @media_comments.select! do |m| + # (Reader.find_by(user: current_user, thread: m[:thread]) + # &.updated_at || 1000.years.ago) < m[:latest_comment].created_at && + # m[:medium].visible_for_user?(current_user) + # end + # @media_array = Kaminari.paginate_array(@media_comments) + # .page(params[:page]).per(10) + # end + # + # The method `user_unread_comments?` in this migration, however, does not + # reflect the logic of the method `comments` in app/controllers/main_controller.rb + # precisely. Consider what happens when for each reader instance, the + # check reader.updated_at < latest_thread_comment_time is false. In this case, + # we don't encounter an early "return true" and therefore go on with the second + # part. There, we want to check if there are any *unseen* media. What we actually do + # is to check if there are any media with comments (that are not by the creator + # and that are visible to the user). But we missed the part where we check if + # the user has already seen these comments, i.e. an additional reader query + # is missing. This is why suddenly, many more people encounter the original + # issue after having run this migration. + # + # This migration took ~40 minutes to run for ~7000 users at 2024-02-15, 0:40. + raise ActiveRecord::IrreversibleMigration + + # For archive reasons, here is the original code: - User.find_each do |user| - had_user_unread_comments = user.unread_comments # boolean - has_user_unread_comments = user_unread_comments?(user) + # num_fixed_users = 0 - has_flag_changed = (had_user_unread_comments != has_user_unread_comments) - user.update(unread_comments: has_user_unread_comments) if has_flag_changed - num_fixed_users += 1 if has_flag_changed - end + # User.find_each do |user| + # had_user_unread_comments = user.unread_comments # boolean + # has_user_unread_comments = user_unread_comments?(user) - Rails.logger.warn { "Ran through #{User.count} users (unread comments flag)" } - Rails.logger.warn { "Fixed #{num_fixed_users} users (unread comments flag)" } + # has_flag_changed = (had_user_unread_comments != has_user_unread_comments) + # user.update(unread_comments: has_user_unread_comments) if has_flag_changed + # num_fixed_users += 1 if has_flag_changed + # end + + # Rails.logger.warn { "Ran through #{User.count} users (unread comments flag)" } + # Rails.logger.warn { "Fixed #{num_fixed_users} users (unread comments flag)" } end # Checks and returns whether the user has unread comments. - def user_unread_comments?(user) - # Check for unread comments -- directly via Reader - readers = Reader.where(user: user) - readers.each do |reader| - thread = Commontator::Thread.find_by(id: reader.thread_id) - next if thread.nil? - - latest_thread_comment_by_any_user = thread.comments.max_by(&:created_at) - next if latest_thread_comment_by_any_user.blank? - - latest_thread_comment_time = latest_thread_comment_by_any_user.created_at - has_user_unread_comments = reader.updated_at < latest_thread_comment_time - - return true if has_user_unread_comments - end - - # User might still have unread comments but no related Reader objects - # -> Check for unread comments -- via Media - unseen_media = user.subscribed_media_with_latest_comments_not_by_creator.select do |m| - m[:medium].visible_for_user?(user) - end - unseen_media.present? - end + # def user_unread_comments?(user) + # # Check for unread comments -- directly via Reader + # readers = Reader.where(user: user) + # readers.each do |reader| + # thread = Commontator::Thread.find_by(id: reader.thread_id) + # next if thread.nil? + + # latest_thread_comment_by_any_user = thread.comments.max_by(&:created_at) + # next if latest_thread_comment_by_any_user.blank? + + # latest_thread_comment_time = latest_thread_comment_by_any_user.created_at + # has_user_unread_comments = reader.updated_at < latest_thread_comment_time + + # return true if has_user_unread_comments + # end + + # # User might still have unread comments but no related Reader objects + # # -> Check for unread comments -- via Media + # unseen_media = user.subscribed_media_with_latest_comments_not_by_creator.select do |m| + # m[:medium].visible_for_user?(user) + # end + # unseen_media.present? + # end def down raise ActiveRecord::IrreversibleMigration diff --git a/db/migrate/20240215100000_fix_unread_comments_inconsistencies_again.rb b/db/migrate/20240215100000_fix_unread_comments_inconsistencies_again.rb new file mode 100644 index 000000000..02420a74d --- /dev/null +++ b/db/migrate/20240215100000_fix_unread_comments_inconsistencies_again.rb @@ -0,0 +1,45 @@ +# Fixes the unread_comments flag for all users. Unintended behavior was +# introduced in pull request #515. Behavior fixed in #585 +# A migration was introduced in #587, but it turned out it contained a bug. +# This migration here is a fix for the migration in #587. +# +# This migration is generally *not* idempotent since users might have interacted +# with the website since the migration was run and thus they will probably have +# different unread comments flags as the ones at the time of the migration. +# +# This migration is not reversible as we don't store the previous state of +# the unread_comments flag. +class FixUnreadCommentsInconsistenciesAgain < ActiveRecord::Migration[7.0] + def up + num_fixed_users = 0 + + User.find_each do |user| + had_user_unread_comments = user.unread_comments # boolean + has_user_unread_comments = user_unread_comments?(user) + + has_flag_changed = (had_user_unread_comments != has_user_unread_comments) + user.update(unread_comments: has_user_unread_comments) if has_flag_changed + num_fixed_users += 1 if has_flag_changed + end + + Rails.logger.warn { "Ran through #{User.count} users (unread comments flag again)" } + Rails.logger.warn { "Fixed #{num_fixed_users} users (unread comments flag again)" } + end + + # Checks and returns whether the user has unread comments. + def user_unread_comments?(user) + # see the method "comments" in app/controllers/main_controller.rb + unseen_media = user.subscribed_media_with_latest_comments_not_by_creator + unseen_media.select! do |m| + (Reader.find_by(user: user, thread: m[:thread]) + &.updated_at || 1000.years.ago) < m[:latest_comment].created_at && + m[:medium].visible_for_user?(user) + end + + unseen_media.present? + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/schema.rb b/db/schema.rb index 893ebcec9..423121de5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_01_25_180000) do +ActiveRecord::Schema[7.0].define(version: 2024_02_15_100000) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index d7dd28f20..c1856e773 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -81,5 +81,36 @@ RUN bundle install RUN yarn install --production=true COPY --chown=app:app . /usr/src/app + +# The command ". ./docker-dummy.env" will source our dummy docker env file. +# So why do we need this? +# +# Well, (deeply inhales), Rails needs to boot entirely to run the +# `assets:precompile` task. Therefore, it also needs to access the env variables +# to correctly start the initializers. +# +# However (after a long ime researching), docker compose does not seem to offer +# an easy solution to have an env file from the host machine available during +# the build step (Dockerfile) and not just during the run time of the container. +# Note that the env file is indeed available on our host, just not in the build +# context, the latter being the MaMpf github repo that docker compose pulls from. +# +# Even with volumes and bind mounts it's not working properly ("file not found"). +# In the end, we found a solution that suggests to use the new docker buildkit +# to allow for multiple build contexts. Yet, we explicitly set DOCKER_BUILDKIT=0 +# to use the old buildkit since the new one always gives a moby-related ssh error. +# And even if this worked, it is not entirely clear if this is even working +# with docker compose or just with docker (sigh). +# +# That's why, in the end, we decided to leverage our already-existing dummy env +# file and source it here in the Dockerfile just to have the precompile task run +# successfully (this task doesn't even rely on the actual values, so despite +# being a hack, it should be fine). +# +# I've written down more details in this question on StackOverflow: +# https://stackoverflow.com/q/78098380/ +COPY ./docker/production/docker.env ./docker-dummy.env + RUN cp -r $(bundle info --path sidekiq)/web/assets /usr/src/app/public/sidekiq && \ - SECRET_KEY_BASE="$(bundle exec rails secret)" DB_ADAPTER=nulldb bundle exec rails assets:precompile + set -o allexport && . ./docker-dummy.env && set +o allexport && \ + SECRET_KEY_BASE="$(bundle exec rails secret)" DB_ADAPTER=nulldb bundle exec rails assets:precompile diff --git a/docker/production/docker.env b/docker/production/docker.env index b9588f0eb..a214c5523 100644 --- a/docker/production/docker.env +++ b/docker/production/docker.env @@ -1,4 +1,5 @@ -GIT_BRANCH=production +# Instance variables +GIT_BRANCH=main COMPOSE_PROJECT_NAME=mampf INSTANCE_NAME=mampf @@ -10,22 +11,32 @@ MUESLI_SERVER=https://muesli.mathi.uni-heidelberg.de ERDBEERE_API=https://erdbeere.mathi.uni-heidelberg.de/api/v1 MEMCACHED_SERVER=cache -# Email is send using a mailserver without authentication. Specify how to connect here +# Email FROM_ADDRESS=mampf@mathi.uni-heidelberg.de MAILSERVER=mail.mathi.uni-heidelberg.de PROJECT_EMAIL=mampf@mathi.uni-heidelberg.de -ERROR_EMAIL=mampf-error@mathi.uni-heidelberg.de +PROJECT_NOTIFICATION_EMAIL=notificationmail MAILID_DOMAIN=mathi.uni-heidelberg.de +ERROR_EMAIL=mampf-error@mathi.uni-heidelberg.de IMAPSERVER=mail.mathi.uni-heidelberg.de PROJECT_EMAIL_USERNAME=creativeusername PROJECT_EMAIL_PASSWORD=secretsecret -PROJECT_EMAIL_MAILBOX=Other Users/mampf +PROJECT_EMAIL_MAILBOX="Other Users/mampf" +MAMPF_EMAIL_USERNAME=secret +MAMPF_EMAIL_PASSWORD=secret FEEDBACK_EMAIL=mampf-feedback-mail -FEEDBACK_EMAIL_USERNAME=creative-feedback-username -FEEDBACK_EMAIL_PASSWORD=creative-feedback-password +FEEDBACK_EMAIL_USERNAME=secret +FEEDBACK_EMAIL_PASSWORD=secret # Due to CORS constraints, some urls are proxied to the media server DOWNLOAD_LOCATION=https://mampf.mathi.uni-heidelberg.de/mediaforward +REWRITE_ENABLED=1 + +# Captcha Server +USE_CAPTCHA_SERVICE=1 +CAPTCHA_VERIFY_URL=https://captcha2go.mathi.uni-heidelberg.de/v1/verify +CAPTCHA_PUZZLE_URL=https://captcha2go.mathi.uni-heidelberg.de/v1/puzzle +CAPTCHA_APPLICATION_TOKEN=secret # Upload folder MEDIA_PATH=/private/media @@ -38,14 +49,14 @@ PRODUCTION_DATABASE_INTERACTIONS=mampf_interactions PRODUCTION_DATABASE_HOST=db PRODUCTION_DATABASE_USERNAME=mampf PRODUCTION_DATABASE_PASSWORD=supersecret -PRODUCTION_DATABASE_PORT=5432 -PRODUCTION_DATABASE_URL='postgresql://mampf:supersecret@db:5432/mampf' +PRODUCTION_DATABASE_PORT=port +PRODUCTION_DATABASE_URL='postgresql://mampf:supersecret@db:port/mampf' # Rails configuration # change RAILS_ENV to production for a production deployment RAILS_ENV=production -RAILS_MASTER_KEY= -SECRET_KEY_BASE= +RAILS_MASTER_KEY=secret +SECRET_KEY_BASE=secret URL_HOST=mampf.mathi.uni-heidelberg.de URL_HOST_SHORT=http://mampf.media diff --git a/eslint.config.mjs b/eslint.config.mjs index f037398b8..38122f670 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -82,5 +82,9 @@ export default [ ...globals.node, }, }, + linterOptions: { + // see https://github.com/Splines/eslint-plugin-erb/releases/tag/v2.0.1 + reportUnusedDisableDirectives: "off", + }, }, ]; diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake index d7aa4dd01..067cd8ac2 100644 --- a/lib/tasks/db.rake +++ b/lib/tasks/db.rake @@ -33,7 +33,7 @@ namespace :db do desc "Dumps the database to backups" task dump: :environment do - dump_fmt = ensure_format(ENV.fetch("format", nil)) + dump_fmt = ensure_format(ENV.fetch("format")) dump_sfx = suffix_for_format(dump_fmt) backup_dir = backup_directory(Rails.env, create: true) full_path = nil @@ -56,10 +56,10 @@ namespace :db do namespace :dump do desc "Dumps a specific table to backups" task table: :environment do - table_name = ENV.fetch("table", nil) + table_name = ENV.fetch("table") if table_name.present? - dump_fmt = ensure_format(ENV.fetch("format", nil)) + dump_fmt = ensure_format(ENV.fetch("format")) dump_sfx = suffix_for_format(dump_fmt) backup_dir = backup_directory(Rails.env, create: true) full_path = nil @@ -92,7 +92,7 @@ namespace :db do desc "Restores the database from a backup using PATTERN" task restore: :environment do - pattern = ENV.fetch("pattern", nil) + pattern = ENV.fetch("pattern") if pattern.present? file = nil diff --git a/yarn.lock b/yarn.lock index b58fa340d..dd7907d62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3237,9 +3237,9 @@ escape-string-regexp@^4.0.0: integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== eslint-plugin-erb@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-erb/-/eslint-plugin-erb-2.0.0.tgz#34ef70ff7f04987f3f385a5d498ce4992b504bb7" - integrity sha512-yLfDeaVjY5PzoBR9mk1hRIlOZ/LuzIi8CSZi2DYsRWVY0WIQLRL/D6372OFwhyJomqsnT17V6XRhi6rbeumUhw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-erb/-/eslint-plugin-erb-2.0.1.tgz#ed4b98e2c06221510ff46e43743236a11c0d2a71" + integrity sha512-d0vvv0QNH2MGl69ey89hAKtrfllgB7gSVU85q+eb+/u5OmbUuOtE1QttBZbIoGFnBoDIiTYl+bXaPbqWKB/r5w== eslint-scope@^4.0.3: version "4.0.3"