diff --git a/.database_consistency.todo.yml b/.database_consistency.todo.yml index 4fcdc664..3436b2d2 100644 --- a/.database_consistency.todo.yml +++ b/.database_consistency.todo.yml @@ -3,3 +3,6 @@ Profile: self_ref: MissingIndexChecker: enabled: false + uuid: + ColumnPresenceChecker: + enabled: false \ No newline at end of file diff --git a/Gemfile b/Gemfile index a4593102..1d8462e4 100644 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,7 @@ gem "avo", ">= 3.2.1" # Other gem "bootsnap", require: false +gem "draper" gem "inline_svg" gem "puma", ">= 5.0" gem "ransack" diff --git a/Gemfile.lock b/Gemfile.lock index 4c10addf..f06d8b0d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -58,6 +58,10 @@ GEM globalid (>= 0.3.6) activemodel (7.1.3.4) activesupport (= 7.1.3.4) + activemodel-serializers-xml (1.0.2) + activemodel (> 5.x) + activesupport (> 5.x) + builder (~> 3.1) activerecord (7.1.3.4) activemodel (= 7.1.3.4) activesupport (= 7.1.3.4) @@ -166,6 +170,13 @@ GEM diff-lcs (1.5.1) docile (1.4.0) dotenv (3.1.2) + draper (4.0.2) + actionpack (>= 5.0) + activemodel (>= 5.0) + activemodel-serializers-xml (>= 1.0) + activesupport (>= 5.0) + request_store (>= 1.0) + ruby2_keywords drb (2.2.1) dry-configurable (1.2.0) dry-core (~> 1.0, < 2) @@ -375,6 +386,8 @@ GEM regexp_parser (2.9.2) reline (0.5.9) io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) rexml (3.3.2) strscan rouge (4.3.0) @@ -436,6 +449,7 @@ GEM rubocop (~> 1.61) rubocop-rspec (~> 3, >= 3.0.1) ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) ruby_parser (3.21.0) racc (~> 1.5) sexp_processor (~> 4.16) @@ -546,6 +560,7 @@ DEPENDENCIES database_consistency debug dotenv + draper erb_lint factory_bot_rails faker diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index f78eec4d..7df05e4c 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -1,6 +1,8 @@ class ProfilesController < ApplicationController + before_action :set_profile, only: :show + def show - @profile = current_profile + @profile = @profile.decorate end def edit @@ -13,7 +15,7 @@ def update if @profile.save remove_profile_image_if_requested - redirect_to profile_path, notice: t("controllers.profiles.update.success") + redirect_to profile_path(@profile.uuid), notice: t("controllers.profiles.update.success") else render :edit, status: :unprocessable_entity end @@ -21,6 +23,15 @@ def update private + def set_profile + @profile = + if params[:uuid] == current_profile.uuid + current_profile + else + Profile.public_profiles.find_by!(uuid: params[:uuid]) + end + end + def current_profile current_user.profile || current_user.build_profile end diff --git a/app/decorators/profile_decorator.rb b/app/decorators/profile_decorator.rb new file mode 100644 index 00000000..b40b701b --- /dev/null +++ b/app/decorators/profile_decorator.rb @@ -0,0 +1,13 @@ +class ProfileDecorator < Draper::Decorator + delegate_all + + def svg_qr_code(options = {}) + RQRCode::QRCode.new(profile_url).as_svg(options) + end + + private + + def profile_url + Rails.application.routes.url_helpers.profile_url(object.uuid) + end +end diff --git a/app/models/profile.rb b/app/models/profile.rb index 8c651f01..ff992931 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -13,6 +13,7 @@ # name :string # profileable_type :string not null # twitter_url :string +# uuid :string # created_at :datetime not null # updated_at :datetime not null # profileable_id :integer not null @@ -20,6 +21,7 @@ # Indexes # # index_profiles_on_profileable (profileable_type,profileable_id) +# index_profiles_on_uuid (uuid) UNIQUE # class Profile < ApplicationRecord has_one_attached :image @@ -29,4 +31,16 @@ class Profile < ApplicationRecord has_one :speaker, through: :self_ref, source: :profileable, source_type: "Speaker" belongs_to :profileable, polymorphic: true + + validates :uuid, uniqueness: true, presence: true + + before_validation :set_uuid + + scope :public_profiles, -> { where(is_public: true) } + + private + + def set_uuid + self.uuid ||= SecureRandom.uuid + end end diff --git a/app/models/user.rb b/app/models/user.rb index 13b65e45..5e81d33b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -36,6 +36,8 @@ class User < ApplicationRecord after_create_commit { create_profile! } + delegate :uuid, to: :profile, allow_nil: true + def self.ransackable_attributes(_auth_object = nil) %w[email] end diff --git a/app/views/layouts/_bottom_navbar.html.erb b/app/views/layouts/_bottom_navbar.html.erb index c8272f81..b9ee1cb6 100644 --- a/app/views/layouts/_bottom_navbar.html.erb +++ b/app/views/layouts/_bottom_navbar.html.erb @@ -7,8 +7,8 @@ <%= inline_svg_tag "clock.svg", class: nav_icon_class_for([root_path]) %> My Schedule - - <%= inline_svg_tag "avatar_no_fill.svg", class: nav_icon_class_for([profile_path, edit_profile_path]) %> + + <%= inline_svg_tag "avatar_no_fill.svg", class: nav_icon_class_for([profile_path(current_user.uuid), edit_profile_path]) %> Profile diff --git a/app/views/layouts/_flash_message.html.erb b/app/views/layouts/_flash_message.html.erb index dfdca38d..4496e81e 100644 --- a/app/views/layouts/_flash_message.html.erb +++ b/app/views/layouts/_flash_message.html.erb @@ -2,4 +2,4 @@
<%= message %>
- \ No newline at end of file + diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 55186fe5..d6d9c069 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -24,7 +24,7 @@ <% end %> <% flash.each do |type, message| %> <% if message.present? && message.is_a?(String) %> - <%= render partial: "layouts/flash_message", locals: { message: message }%> + <%= render partial: "layouts/flash_message", locals: { message: message } %> <% end %> <% end %> <%= yield %> diff --git a/app/views/main/index.html.erb b/app/views/main/index.html.erb index f2d623dc..1c27d579 100644 --- a/app/views/main/index.html.erb +++ b/app/views/main/index.html.erb @@ -1,2 +1,2 @@

Homepage

-<%= link_to "Profile", profile_path %> +<%= link_to "Profile", profile_path(current_user.uuid) %> diff --git a/app/views/profiles/show.html.erb b/app/views/profiles/show.html.erb index feacac86..30b54193 100644 --- a/app/views/profiles/show.html.erb +++ b/app/views/profiles/show.html.erb @@ -55,6 +55,7 @@ + <%= render inline: @profile.svg_qr_code(module_size: 4) %>
<%= link_to 'Edit Profile', edit_profile_path, class: "text-red font-bold rounded-sm underline italic text-lg" %>
diff --git a/config/environments/development.rb b/config/environments/development.rb index c5d02975..4456214e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -33,6 +33,8 @@ config.cache_store = :null_store end + Rails.application.routes.default_url_options[:host] = "localhost:3000" + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local @@ -82,4 +84,7 @@ # Raise error when a before_action's only/except options reference missing actions config.action_controller.raise_on_missing_callback_actions = true + + # Allow ngrok hosts + config.hosts << /[a-z0-9.\-]+\.ngrok\.io/ end diff --git a/config/environments/test.rb b/config/environments/test.rb index 3fb3f577..3db62bc4 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -34,6 +34,8 @@ # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false + Rails.application.routes.default_url_options[:host] = "localhost:3000" + # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 3860f659..435cf643 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -11,6 +11,6 @@ # end # These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym "RESTful" -# end +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym "QR" +end diff --git a/config/routes.rb b/config/routes.rb index ce6e82a3..71ad11f7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,8 +10,9 @@ resource :registration, only: [:new, :create] resource :session, only: [:new, :create, :destroy] resource :password, only: [:edit, :update] - resource :profile, only: [:edit, :update, :show] resource :password_reset, only: [:new, :create, :edit, :update] do get :post_submit end + resources :profiles, only: [:show], param: :uuid + resource :profile, only: [:edit, :update] end diff --git a/config/tailwind.config.js b/config/tailwind.config.js index d55ac0b6..249f911a 100644 --- a/config/tailwind.config.js +++ b/config/tailwind.config.js @@ -10,7 +10,7 @@ module.exports = { theme: { extend: { boxShadow: { - 'simple': '0px 0px 25px 0px rgba(0, 0, 0, 0.50)' + simple: '0px 0px 25px 0px rgba(0, 0, 0, 0.50)' }, fontFamily: { sans: ['Roboto', ...defaultTheme.fontFamily.sans] @@ -31,7 +31,7 @@ module.exports = { blue: '#0A4E6B', 'green-dark': '#62C554', 'green-light': '#D8F1D4' - }, + } } }, plugins: [ diff --git a/db/migrate/20240710213305_add_profile_uuid_to_profile.rb b/db/migrate/20240710213305_add_profile_uuid_to_profile.rb new file mode 100644 index 00000000..40f2d070 --- /dev/null +++ b/db/migrate/20240710213305_add_profile_uuid_to_profile.rb @@ -0,0 +1,6 @@ +class AddProfileUuidToProfile < ActiveRecord::Migration[7.1] + def change + add_column :profiles, :uuid, :string + add_index :profiles, :uuid, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 42eee9a4..3790f247 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -111,7 +111,9 @@ t.integer "profileable_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "uuid" t.index ["profileable_type", "profileable_id"], name: "index_profiles_on_profileable" + t.index ["uuid"], name: "index_profiles_on_uuid", unique: true end create_table "speakers", force: :cascade do |t| diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb new file mode 100644 index 00000000..205f44d9 --- /dev/null +++ b/spec/controllers/profiles_controller_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ProfilesController, type: :controller do + let(:user) { create(:user, :with_profile) } + let(:profile) { user.profile } + let(:other_profile) { create(:profile, :with_user) } + + before do + sign_in(user) + end + + describe "GET #show" do + context "when the profile is yourself" do + it "returns a success response" do + get :show, params: {uuid: profile.uuid} + expect(response).to have_http_status(:success) + end + end + + context "when the profile is public" do + before do + other_profile.update!(is_public: true) + end + + it "returns a success response" do + get :show, params: {uuid: other_profile.uuid} + expect(response).to have_http_status(:success) + end + end + + context "when the profile is not public" do + it "raises a RecordNotFound error" do + expect { get :show, params: {uuid: other_profile.uuid} }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/decorators/profile_decorator_spec.rb b/spec/decorators/profile_decorator_spec.rb new file mode 100644 index 00000000..cb285cf2 --- /dev/null +++ b/spec/decorators/profile_decorator_spec.rb @@ -0,0 +1,11 @@ +require "rails_helper" + +RSpec.describe ProfileDecorator do + let(:profile) { create(:profile, :with_user).decorate } + + describe "#svg_qr_code" do + it "generates a QR code SVG" do + expect(profile.svg_qr_code).to include("