diff --git a/Gemfile b/Gemfile index c20323811..b09b258fd 100644 --- a/Gemfile +++ b/Gemfile @@ -100,3 +100,5 @@ group :development do gem 'spring', '~> 4.0' gem 'web-console', '~> 4.2' end + +gem 'maintenance_tasks', '~> 2.1.1' diff --git a/Gemfile.lock b/Gemfile.lock index 52d4d8c7f..47bccbcc9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -164,6 +164,8 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.1) + job-iteration (1.3.6) + activejob (>= 5.2) jquery-rails (4.5.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -189,6 +191,12 @@ GEM net-imap net-pop net-smtp + maintenance_tasks (2.1.1) + actionpack (>= 6.0) + activejob (>= 6.0) + activerecord (>= 6.0) + job-iteration (~> 1.3.6) + railties (>= 6.0) marcel (1.0.4) matrix (0.4.2) memory_profiler (1.0.0) @@ -415,6 +423,7 @@ DEPENDENCIES jquery-rails (~> 4.5.0) letter_opener_web (~> 2.0) listen (~> 3.7) + maintenance_tasks (~> 2.1.1) memory_profiler (~> 1.0) minitest (~> 5.16.0) minitest-ci (~> 3.4.0) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index bcd8aa87e..1b4aec6f0 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -355,25 +355,24 @@ def edit_profile render layout: 'without_sidebar' end - def validate_profile_website(profile_params) - uri = profile_params[:website] + def cleaned_profile_websites(profile_params) + sites = profile_params[:user_websites_attributes] - if URI.parse(uri).instance_of?(URI::Generic) - # URI::Generic indicates the user didn't include a protocol, so we'll add one now so that it can be - # parsed correctly in the view later on. - profile_params[:website] = "https://#{uri}" + sites.transform_values do |w| + w.merge({ label: w[:label].presence, url: w[:url].presence }) end - rescue URI::InvalidURIError - profile_params.delete(:website) - flash[:danger] = 'Invalid profile website link.' end def update_profile - profile_params = params.require(:user).permit(:username, :profile_markdown, :website, :twitter, :discord) - profile_params[:twitter] = profile_params[:twitter].delete('@') + profile_params = params.require(:user).permit(:username, + :profile_markdown, + :website, + :discord, + :twitter, + user_websites_attributes: [:id, :label, :url]) - if profile_params[:website].present? - validate_profile_website(profile_params) + if profile_params[:user_websites_attributes].present? + profile_params[:user_websites_attributes] = cleaned_profile_websites(profile_params) end @user = current_user @@ -389,8 +388,14 @@ def update_profile end end - profile_rendered = helpers.post_markdown(:user, :profile_markdown) - if @user.update(profile_params.merge(profile: profile_rendered)) + if params[:user][:profile_markdown].present? + profile_rendered = helpers.post_markdown(:user, :profile_markdown) + profile_params = profile_params.merge(profile: profile_rendered) + end + + status = @user.update(profile_params) + + if status flash[:success] = 'Your profile details were updated.' redirect_to user_path(current_user) else diff --git a/app/models/user.rb b/app/models/user.rb index 46a27df1a..d585a9818 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -27,6 +27,8 @@ class User < ApplicationRecord has_many :comment_threads_locked, class_name: 'CommentThread', foreign_key: :locked_by_id, dependent: :nullify has_many :category_filter_defaults, dependent: :destroy has_many :filters, dependent: :destroy + has_many :user_websites, dependent: :destroy + accepts_nested_attributes_for :user_websites belongs_to :deleted_by, required: false, class_name: 'User' validates :username, presence: true, length: { minimum: 3, maximum: 50 } @@ -43,7 +45,7 @@ class User < ApplicationRecord scope :active, -> { where(deleted: false) } scope :deleted, -> { where(deleted: true) } - after_create :send_welcome_tour_message + after_create :send_welcome_tour_message, :ensure_websites def self.list_includes includes(:posts, :avatar_attachment) @@ -61,6 +63,12 @@ def trust_level community_user.trust_level end + # Checks whether this user is the same as a given user + # @param [User] user user to compare with + def same_as?(user) + id == user.id + end + # This class makes heavy use of predicate names, and their use is prevalent throughout the codebase # because of the importance of these methods. # rubocop:disable Naming/PredicateName @@ -130,6 +138,18 @@ def website_domain website.nil? ? website : URI.parse(website).hostname end + def valid_websites_for + user_websites.where.not(url: [nil, '']).order(position: :asc) + end + + def ensure_websites + pos = user_websites.size + while pos < UserWebsite::MAX_ROWS + pos += 1 + UserWebsite.create(user_id: id, position: pos) + end + end + def is_moderator is_global_moderator || community_user&.is_moderator || is_admin || community_user&.privilege?('mod') || false end diff --git a/app/models/user_website.rb b/app/models/user_website.rb new file mode 100644 index 000000000..6b50d909f --- /dev/null +++ b/app/models/user_website.rb @@ -0,0 +1,6 @@ +class UserWebsite < ApplicationRecord + belongs_to :user + default_scope { order(:position) } + + MAX_ROWS = 3 +end diff --git a/app/tasks/maintenance/initialize_user_websites_task.rb b/app/tasks/maintenance/initialize_user_websites_task.rb new file mode 100644 index 000000000..80a91b948 --- /dev/null +++ b/app/tasks/maintenance/initialize_user_websites_task.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Maintenance + class InitializeUserWebsitesTask < MaintenanceTasks::Task + def collection + User.all + end + + def process(user) + unless user.user_websites.exists?(position: 1) + if user.website.present? + UserWebsite.create!(user_id: user.id, position: 1, label: 'website', url: user.website) + else + UserWebsite.create!(user_id: user.id, position: 1) + end + end + + unless user.user_websites.exists?(position: 2) + if user.twitter.present? + UserWebsite.create!(user_id: user.id, position: 2, label: 'Twitter', + url: "https://twitter.com/#{user.twitter}") + else + UserWebsite.create!(user_id: user.id, position: 2) + end + end + + # This check *should* be superfluous, but just in case... + unless user.user_websites.exists?(position: 3) + UserWebsite.create!(user_id: user.id, position: 3) + end + end + end +end diff --git a/app/views/users/edit_profile.html.erb b/app/views/users/edit_profile.html.erb index 4dbe1d2da..654a2ef29 100644 --- a/app/views/users/edit_profile.html.erb +++ b/app/views/users/edit_profile.html.erb @@ -46,26 +46,29 @@ <% end %>
-Extra fields -- your web site, GitHub profile, social-media usernames, whatever you want. Only values that begin with "http" are rendered as links.
+- <% if @user.website.present? %> - <% unless !user_signed_in? && !@user.community_user.privilege?('unrestricted') %> - - - <%= link_to @user.website_domain, @user.website, rel: 'nofollow', - 'aria-label': "Visit website of #{rtl_safe_username(@user)} at #{@user.website_domain}" %> - - <% end %> - <% end %> - <% if @user.twitter.present? %> - - <%= link_to @user.twitter, "https://twitter.com/#{@user.twitter}", - 'aria-label': "Visit twitter account of #{rtl_safe_username(@user)}" %> - - <% end %> - <% if @user.discord.present? %> - - <%= @user.discord %> - - <% end %> -
<% effective_profile = raw(sanitize(@user.profile&.strip || '', scrubber: scrubber)) %> <% if effective_profile.blank? %>A quiet enigma. We don't know anything about <%= rtl_safe_username(@user) %> yet.
- <% elsif !user_signed_in? && !@user.community_user.privilege?('unrestricted') %> - <%= sanitize(effective_profile, attributes: %w()) %> - <% else %> + <% elsif !user_signed_in? && !@user.community_user.privilege?('unrestricted') %> + <%= sanitize(effective_profile, attributes: %w()) %> + <% else %> <%= effective_profile %> <% end %>Extra fields
+<%= w.label %> | ++ <% if w.url[0,4] == 'http' %> + <%= link_to w.url, w.url, rel: 'nofollow' %> + <% else %> + <%= w.url %> + <% end %> + | +
+ <% if @user.discord.present? %> + + <%= @user.discord %> + + <% end %> +
+User since <%= @user.created_at %> | +User since <%= @user.created_at %> |