diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index 0cb802cb..11d371df 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -28,8 +28,7 @@ class UserController < ApplicationController end def update - user_attributes = params.require(:user).permit(:display_name, :motivation_header, :website, :show_foreign_themes, :location, :bio, - :profile_picture_x, :profile_picture_y, :profile_picture_w, :profile_picture_h, + user_attributes = params.require(:user).permit(:show_foreign_themes, :profile_picture_x, :profile_picture_y, :profile_picture_w, :profile_picture_h, :profile_header_x, :profile_header_y, :profile_header_w, :profile_header_h, :profile_picture, :profile_header) if current_user.update_attributes(user_attributes) text = t('flash.user.update.text') @@ -41,6 +40,17 @@ class UserController < ApplicationController end redirect_to edit_user_profile_path end + + def update_profile + profile_attributes = params.require(:profile).permit(:display_name, :motivation_header, :website, :location, :description) + + if current_user.profile.update_attributes(profile_attributes) + flash[:success] = t('flash.user.update.text') + else + flash[:error] = t('flash.user.update.error') + end + redirect_to edit_user_profile_path + end # endregion # region Privacy settings diff --git a/app/helpers/application_helper/graph_methods.rb b/app/helpers/application_helper/graph_methods.rb index 137bcf73..79ebe241 100644 --- a/app/helpers/application_helper/graph_methods.rb +++ b/app/helpers/application_helper/graph_methods.rb @@ -14,11 +14,11 @@ module ApplicationHelper::GraphMethods # @param user [User] def user_opengraph(user) opengraph_meta_tags({ - 'og:title': user.safe_name, + 'og:title': user.profile.safe_name, 'og:type': 'profile', 'og:image': full_profile_picture_url(user), 'og:url': show_user_profile_url(user.screen_name), - 'og:description': user.bio, + 'og:description': user.profile.description, 'og:site_name': APP_CONFIG['site_name'], 'profile:username': user.screen_name }) @@ -29,8 +29,8 @@ module ApplicationHelper::GraphMethods meta_tags({ 'twitter:card': 'summary', 'twitter:site': '@retrospring', - 'twitter:title': user.motivation_header.presence || "Ask me anything!", - 'twitter:description': "Ask #{user.safe_name} anything on Retrospring", + 'twitter:title': user.profile.motivation_header.presence || "Ask me anything!", + 'twitter:description': "Ask #{user.profile.safe_name} anything on Retrospring", 'twitter:image': full_profile_picture_url(user) }) end @@ -38,7 +38,7 @@ module ApplicationHelper::GraphMethods # @param answer [Answer] def answer_opengraph(answer) opengraph_meta_tags({ - 'og:title': "#{answer.user.safe_name} answered: #{answer.question.content}", + 'og:title': "#{answer.user.profile.safe_name} answered: #{answer.question.content}", 'og:type': 'article', 'og:image': full_profile_picture_url(answer.user), 'og:url': show_user_answer_url(answer.user.screen_name, answer.id), diff --git a/app/helpers/user_helper.rb b/app/helpers/user_helper.rb index 3abe244b..ab35c452 100644 --- a/app/helpers/user_helper.rb +++ b/app/helpers/user_helper.rb @@ -3,7 +3,7 @@ module UserHelper # @return [String] The user name def user_screen_name(user, anonymous: false, url: true, link_only: false) return APP_CONFIG['anonymous_name'] if user.nil? || anonymous - name = user.display_name.blank? ? user.screen_name : user.display_name + name = user.profile.display_name.blank? ? user.screen_name : user.profile.display_name if url link = show_user_profile_path(user.screen_name) return link if link_only diff --git a/app/models/profile.rb b/app/models/profile.rb new file mode 100644 index 00000000..df75ce61 --- /dev/null +++ b/app/models/profile.rb @@ -0,0 +1,29 @@ +class Profile < ApplicationRecord + belongs_to :user + + attr_readonly :user_id + + validates :display_name, length: { maximum: 50 } + validates :location, length: { maximum: 72 } + validates :description, length: { maximum: 200 } + + before_save do + unless website.blank? + self.website = if website.match %r{\Ahttps?://} + website + else + "http://#{website}" + end + end + end + + def display_website + website.match(/https?:\/\/([A-Za-z.\-0-9]+)\/?(?:.*)/i)[1] + rescue NoMethodError + website + end + + def safe_name + display_name.presence || user.screen_name + end +end diff --git a/app/models/services/tumblr.rb b/app/models/services/tumblr.rb index d6631b9f..8af15e1d 100644 --- a/app/models/services/tumblr.rb +++ b/app/models/services/tumblr.rb @@ -32,7 +32,7 @@ class Services::Tumblr < Service asker = if answer.question.author_is_anonymous? APP_CONFIG['anonymous_name'] else - answer.question.user.display_name.blank? ? answer.question.user.screen_name : answer.question.user.display_name + answer.question.user.profile.display_name.blank? ? answer.question.user.screen_name : answer.question.user.profile.display_name end client.text( self.uid, diff --git a/app/models/user.rb b/app/models/user.rb index ac6bf38d..5e86fc31 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -45,6 +45,7 @@ class User < ApplicationRecord has_many :subscriptions, dependent: :destroy has_many :totp_recovery_codes, dependent: :destroy + has_one :profile, dependent: :destroy has_one :theme, dependent: :destroy SCREEN_NAME_REGEX = /\A[a-zA-Z0-9_]{1,16}\z/ @@ -57,22 +58,11 @@ class User < ApplicationRecord validates :email, fake_email: true validates :screen_name, presence: true, format: { with: SCREEN_NAME_REGEX }, uniqueness: { case_sensitive: false }, screen_name: true - validates :display_name, length: { maximum: 50 } - validates :bio, length: { maximum: 200 } - mount_uploader :profile_picture, ProfilePictureUploader, mount_on: :profile_picture_file_name process_in_background :profile_picture mount_uploader :profile_header, ProfileHeaderUploader, mount_on: :profile_header_file_name process_in_background :profile_header - before_save do - self.website = if website.match %r{\Ahttps?://} - website - else - "http://#{website}" - end unless website.blank? - end - # when a user has been deleted, all reports relating to the user become invalid before_destroy do rep = Report.where(target_id: self.id, type: 'Reports::User') @@ -84,6 +74,10 @@ class User < ApplicationRecord end end + after_create do + Profile.create(user_id: id) if Profile.where(user_id: id).count.zero? + end + def login=(login) @login = login end @@ -169,12 +163,6 @@ class User < ApplicationRecord comment.smiles.pluck(:user_id).include? self.id end - def display_website - website.match(/https?:\/\/([A-Za-z.\-0-9]+)\/?(?:.*)/i)[1] - rescue NoMethodError - website - end - def comment(answer, content) Comment.create!(user: self, answer: answer, content: content) end @@ -253,10 +241,6 @@ class User < ApplicationRecord !self.export_processing end - def safe_name - self.display_name.presence || self.screen_name - end - # %w[admin moderator].each do |m| # define_method(m) { raise "not allowed: #{m}" } # define_method(m+??) { raise "not allowed: #{m}?"} diff --git a/app/views/application/_questionbox.haml b/app/views/application/_questionbox.haml index 23af423e..6decb371 100644 --- a/app/views/application/_questionbox.haml +++ b/app/views/application/_questionbox.haml @@ -1,9 +1,9 @@ .card .card-header - - if user.motivation_header.blank? + - if user.profile.motivation_header.blank? = t 'views.questionbox.title' - else - = user.motivation_header + = user.profile.motivation_header .card-body - if user.banned? .text-center diff --git a/app/views/discover/_userbox.haml b/app/views/discover/_userbox.haml index 8ae7567f..a34869a5 100644 --- a/app/views/discover/_userbox.haml +++ b/app/views/discover/_userbox.haml @@ -6,12 +6,12 @@ %img.avatar-md.mr-2{ src: u.profile_picture.url(:medium) } .media-body %h6.media-heading.answerbox__question-user - - if u.display_name.blank? + - if u.profile.display_name.blank? %a{ href: show_user_profile_path(u.screen_name) } = u.screen_name - else %a{ href: show_user_profile_path(u.screen_name) } - = u.display_name + = u.profile.display_name %span.text-muted= u.screen_name %p.answerbox__question-text - case type diff --git a/app/views/moderation/_userbox.haml b/app/views/moderation/_userbox.haml index 0b604f6d..7685622b 100644 --- a/app/views/moderation/_userbox.haml +++ b/app/views/moderation/_userbox.haml @@ -3,9 +3,9 @@ .card-body %img.userbox__avatar{ src: user.profile_picture.url(:small) } %a.profile__name{ href: show_user_profile_path(user.screen_name) } - - unless user.display_name.blank? + - unless user.profile.display_name.blank? .profile__display-name - = user.display_name + = user.profile.display_name .profile__screen-name = user.screen_name .row diff --git a/app/views/settings/_data.haml b/app/views/settings/_data.haml index 98b80195..929800ab 100644 --- a/app/views/settings/_data.haml +++ b/app/views/settings/_data.haml @@ -13,31 +13,31 @@ %p.font-weight-bold.mb-0 Display name %p.text-muted - - if current_user.display_name.blank? + - if current_user.profile.display_name.blank? None set! - else - = current_user.display_name + = current_user.profile.display_name %p.font-weight-bold.mb-0 Bio %p.text-muted - - if current_user.bio.blank? + - if current_user.profile.description.blank? None set! - else - = current_user.bio + = current_user.profile.description %p.font-weight-bold.mb-0 Location %p.text-muted - - if current_user.location.blank? + - if current_user.profile.location.blank? None set! - else - = current_user.location + = current_user.profile.location %p.font-weight-bold.mb-0 Website %p.text-muted - - if current_user.website.blank? + - if current_user.profile.website.blank? None set! - else - = current_user.website + = current_user.profile.website .col-md-6.col-sm-6.col-xs-12 %h4 Pictures %p.font-weight-bold.mb-0 Profile picture diff --git a/app/views/settings/_profile.haml b/app/views/settings/_profile.haml index 0ddfe141..ea87af87 100644 --- a/app/views/settings/_profile.haml +++ b/app/views/settings/_profile.haml @@ -2,8 +2,6 @@ .card-body = bootstrap_form_for(current_user, url: { action: :edit }, html: { multipart: true }, method: :patch) do |f| - = f.text_field :display_name, label: t('views.settings.profile.displayname') - .media#profile-picture-media .pull-left %img.avatar-lg.mr-3{ src: current_user.profile_picture.url(:medium) } @@ -38,14 +36,6 @@ %button.btn.btn-inverse#cropper-header-zoom-in{ type: :button } %i.fa.fa-search-plus - = f.text_field :motivation_header, label: t('views.settings.profile.motivation'), placeholder: t('views.settings.profile.placeholder.motivation') - - = f.text_field :website, label: t('views.settings.profile.website'), placeholder: 'https://example.com' - - = f.text_field :location, label: t('views.settings.profile.location'), placeholder: t('views.settings.profile.placeholder.location') - - = f.text_area :bio, label: t('views.settings.profile.bio'), placeholder: t('views.settings.profile.placeholder.bio') - = f.check_box :show_foreign_themes, label: 'Render other user themes when visiting their profile' - %i[profile_picture_x profile_picture_y profile_picture_w profile_picture_h].each do |attrib| @@ -55,3 +45,19 @@ = f.hidden_field attrib, id: attrib = f.submit t('views.actions.save'), class: 'btn btn-primary' + +.card + .card-body + = bootstrap_form_for(current_user.profile, url: { action: :update_profile }, html: { multipart: true }, method: :patch) do |f| + + = f.text_field :display_name, label: t('views.settings.profile.displayname') + + = f.text_field :motivation_header, label: t('views.settings.profile.motivation'), placeholder: t('views.settings.profile.placeholder.motivation') + + = f.text_field :website, label: t('views.settings.profile.website'), placeholder: 'https://example.com' + + = f.text_field :location, label: t('views.settings.profile.location'), placeholder: t('views.settings.profile.placeholder.location') + + = f.text_area :description, label: t('views.settings.profile.bio'), placeholder: t('views.settings.profile.placeholder.bio') + + = f.submit t('views.actions.save'), class: 'btn btn-primary' diff --git a/app/views/shared/_sidebar.haml b/app/views/shared/_sidebar.haml index 69a534d1..508bfb87 100644 --- a/app/views/shared/_sidebar.haml +++ b/app/views/shared/_sidebar.haml @@ -3,9 +3,9 @@ .card-body %img.userbox__avatar{ src: current_user.profile_picture.url(:small) } .profile__name - - unless current_user.display_name.blank? + - unless current_user.profile.display_name.blank? .profile__display-name - = current_user.display_name + = current_user.profile.display_name .profile__screen-name = current_user.screen_name - unless @list.nil? diff --git a/app/views/shared/_userbox.haml b/app/views/shared/_userbox.haml index a7325486..8ae9e144 100644 --- a/app/views/shared/_userbox.haml +++ b/app/views/shared/_userbox.haml @@ -4,9 +4,9 @@ .card-body %img.userbox__avatar{ src: user.profile_picture.url(:small) } %a.profile__name{ href: show_user_profile_path(user.screen_name) } - - unless user.display_name.blank? + - unless user.profile.display_name.blank? .profile__display-name - = user.display_name + = user.profile.display_name .profile__screen-name = user.screen_name = render 'user/actions', user: user, type: type diff --git a/app/views/user/_profile.haml b/app/views/user/_profile.haml index 235fe788..ae9e809b 100644 --- a/app/views/user/_profile.haml +++ b/app/views/user/_profile.haml @@ -2,9 +2,9 @@ %img.profile__avatar{ src: user.profile_picture.url(:large) } .card-body .profile__name - - unless user.display_name.blank? + - unless user.profile.display_name.blank? .profile__display-name - = user.display_name + = user.profile.display_name .profile__screen-name = user.screen_name .profile__badge-container @@ -24,15 +24,15 @@ %span.badge.badge-success %i.fa.fa-fw.fa-users = t 'views.user.title.moderator' - - unless user.bio.blank? + - unless user.profile.description.blank? .profile__biography - = markdown user.bio - - unless user.website.blank? + = markdown user.profile.description + - unless user.profile.website.blank? .profile__website %i.fa.fa-fw.fa-globe - %a{ href: user.website }= user.display_website - - unless user.location.blank? + %a{ href: user.profile.website }= user.profile.display_website + - unless user.profile.location.blank? .profile__location %i.fa.fa-fw.fa-location-arrow - = user.location + = user.profile.location = render 'user/actions', user: user, type: :follower diff --git a/app/workers/export_worker.rb b/app/workers/export_worker.rb index 29d95dc3..74af60b0 100644 --- a/app/workers/export_worker.rb +++ b/app/workers/export_worker.rb @@ -2,7 +2,7 @@ require 'exporter' class ExportWorker include Sidekiq::Worker - sidekiq_options queue: :export, retry: false + sidekiq_options queue: :export, retry: 0 # @param user_id [Integer] the user id def perform(user_id) diff --git a/config/routes.rb b/config/routes.rb index dc67cee9..35d23bee 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -62,6 +62,7 @@ Rails.application.routes.draw do match '/settings/profile', to: 'user#edit', via: 'get', as: :edit_user_profile match '/settings/profile', to: 'user#update', via: 'patch', as: :update_user_profile + match '/settings/profile_info', to: 'user#update_profile', via: 'patch', as: :update_user_profile_info match '/settings/theme', to: 'user#edit_theme', via: 'get', as: :edit_user_theme match '/settings/theme', to: 'user#update_theme', via: 'patch', as: :update_user_theme diff --git a/db/migrate/20211219153054_create_profiles.rb b/db/migrate/20211219153054_create_profiles.rb new file mode 100644 index 00000000..d6480af9 --- /dev/null +++ b/db/migrate/20211219153054_create_profiles.rb @@ -0,0 +1,24 @@ +class CreateProfiles < ActiveRecord::Migration[5.2] + def change + create_table :profiles do |t| + t.references :user, index: true, foreign_key: true + t.string :display_name, length: 50 + t.string :description, length: 200, null: false, default: '' + t.string :location, length: 72, null: false, default: '' + t.string :website, null: false, default: '' + t.string :motivation_header, null: false, default: '' + + t.timestamps + end + + transaction do + execute 'INSERT INTO profiles (user_id, display_name, description, location, website, motivation_header, created_at, updated_at) SELECT users.id as user_id, users.display_name, users.bio as description, users.location, users.website, users.motivation_header, users.created_at, users.updated_at FROM users;' + + remove_column :users, :display_name + remove_column :users, :bio + remove_column :users, :location + remove_column :users, :website + remove_column :users, :motivation_header + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 3f9aed55..eb8141ef 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.define(version: 2021_08_11_133004) do +ActiveRecord::Schema.define(version: 2021_12_19_153054) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -116,6 +116,18 @@ ActiveRecord::Schema.define(version: 2021_08_11_133004) do t.datetime "updated_at" end + create_table "profiles", force: :cascade do |t| + t.bigint "user_id" + t.string "display_name" + t.string "description", default: "", null: false + t.string "location", default: "", null: false + t.string "website", default: "", null: false + t.string "motivation_header", default: "", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_profiles_on_user_id" + end + create_table "questions", id: :bigint, default: -> { "gen_timestamp_id('questions'::text)" }, force: :cascade do |t| t.string "content" t.boolean "author_is_anonymous" @@ -296,4 +308,5 @@ ActiveRecord::Schema.define(version: 2021_08_11_133004) do t.index ["user_id"], name: "index_users_roles_on_user_id" end + add_foreign_key "profiles", "users" end diff --git a/lib/exporter.rb b/lib/exporter.rb index f84ce038..4a3c0c18 100644 --- a/lib/exporter.rb +++ b/lib/exporter.rb @@ -34,17 +34,21 @@ class Exporter private def collect_user_info - %i(answered_count asked_count ban_reason banned_until bio comment_smiled_count commented_count - confirmation_sent_at confirmed_at created_at crop_h crop_h_h crop_h_w crop_h_x crop_h_y - crop_w crop_x crop_y current_sign_in_at current_sign_in_ip display_name email follower_count friend_count - id last_sign_in_at last_sign_in_ip locale location motivation_header permanently_banned + %i(answered_count asked_count ban_reason banned_until comment_smiled_count commented_count + confirmation_sent_at confirmed_at created_at profile_header profile_header_h profile_header_w profile_header_x profile_header_y + profile_picture_w profile_picture_h profile_picture_x profile_picture_y current_sign_in_at current_sign_in_ip display_name follower_count friend_count + id last_sign_in_at last_sign_in_ip locale permanently_banned privacy_allow_anonymous_questions privacy_allow_public_timeline privacy_allow_stranger_answers - privacy_show_in_search profile_header_content_type profile_header_file_name profile_header_file_size - profile_header_updated_at profile_picture_content_type profile_picture_file_name profile_picture_file_size - profile_picture_updated_at screen_name show_foreign_themes sign_in_count smiled_count updated_at website).each do |f| + privacy_show_in_search profile_header_file_name profile_picture_file_name + screen_name show_foreign_themes sign_in_count smiled_count updated_at).each do |f| @obj[f] = @user.send f end + @obj[:profile] = {} + %i(display_name motivation_header website location description).each do |f| + @obj[:profile][f] = @user.profile.send f + end + EXPORT_ROLES.each do |role| @obj[role] = @user.has_role?(role) end @@ -228,11 +232,16 @@ class Exporter def user_stub(user) uobj = {} - %i(answered_count asked_count bio comment_smiled_count commented_count created_at display_name follower_count - friend_count id location motivation_header permanently_banned screen_name smiled_count website).each do |f| + %i(answered_count asked_count comment_smiled_count commented_count created_at follower_count + friend_count id permanently_banned screen_name smiled_count).each do |f| uobj[f] = user.send f end + uobj[:profile] = {} + %i(display_name motivation_header website location description).each do |f| + uobj[:profile][f] = user.profile.send f + end + EXPORT_ROLES.each do |role| uobj[role] = user.has_role?(role) end diff --git a/spec/controllers/user_controller_spec.rb b/spec/controllers/user_controller_spec.rb index d3789066..8635d9d3 100644 --- a/spec/controllers/user_controller_spec.rb +++ b/spec/controllers/user_controller_spec.rb @@ -43,6 +43,28 @@ describe UserController, type: :controller do end end + describe "#update_profile" do + subject { patch :update_profile, params: { profile: profile_params } } + let(:profile_params) do + { + display_name: 'sneaky cune' + } + end + + context "user signed in" do + before(:each) { sign_in user } + + it "updates the user's profile" do + expect { subject }.to change{ user.profile.reload.display_name }.to('sneaky cune') + end + + it "redirects to the edit_user_profile page" do + subject + expect(response).to redirect_to(:edit_user_profile) + end + end + end + describe "#update" do subject { patch :update, params: { user: header_params } } let(:header_params) do diff --git a/spec/factories/user.rb b/spec/factories/user.rb index ddf58cb9..c50145a3 100644 --- a/spec/factories/user.rb +++ b/spec/factories/user.rb @@ -6,16 +6,20 @@ FactoryBot.define do sequence(:email) { |i| "#{i}#{Faker::Internet.email}" } password { 'P4s5w0rD' } confirmed_at { Time.now.utc } - display_name { Faker::Name.name } transient do roles { [] } + profile { { display_name: Faker::Name.name } } end after(:create) do |user, evaluator| evaluator.roles.each do |role| user.add_role role end + + evaluator.profile.each do |key, value| + user.profile.public_send(:"#{key}=", value) + end end end -end +end \ No newline at end of file diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index fa6f8b4e..3e27b935 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -55,8 +55,8 @@ describe ApplicationHelper, :type => :helper do describe "#user_opengraph" do context "sample user" do let(:user) { FactoryBot.create(:user, - display_name: 'Cunes', - bio: 'A bunch of raccoons in a trenchcoat.', + profile: { display_name: 'Cunes', + description: 'A bunch of raccoons in a trenchcoat.' }, screen_name: 'raccoons') } subject { user_opengraph(user) } @@ -79,12 +79,12 @@ EOS describe "#user_twitter_card" do context "sample user" do let(:user) { FactoryBot.create(:user, - display_name: '', - bio: 'A bunch of raccoons in a trenchcoat.', + profile: { + display_name: '', + description: 'A bunch of raccoons in a trenchcoat.'}, screen_name: 'raccoons') } subject { user_twitter_card(user) } - it 'should generate a matching OpenGraph structure for a user' do expect(subject).to eq(<<-EOS.chomp) diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb index 31663a9d..d49046ca 100644 --- a/spec/support/factory_bot.rb +++ b/spec/support/factory_bot.rb @@ -1,3 +1,4 @@ + # frozen_string_literal: true require "factory_bot_rails"