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"