From 3514d4d1da4f0772a80b1d8dee17b1933c21b1a8 Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:51:23 -0800 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A5=97=20`Marketplace`:=20Spec=20for?= =?UTF-8?q?=20`Product#tags`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - https://github.com/zinc-collective/convene/issues/2189 Lil' quick sketch through the workflow. Only interesting things of note here are I am using `scenario` syntax (which feels better to me, and was validated by @ExMember in antoher context yesterday.) I am also doing something "weird" where I overload the `visit` method to rely on our `Record#location` method to interject a `polymorphic_path` call; saving us the burden of including the `polymorphic_path` call on our own every time we use `visit`; but adding the burden of a layer of indirection. I also did this with `within`, because omg that awkward `"##{dom_id(model)"` drives me nuts every time I type it. It makes me want to scream. I can't stand it. --- .../marketplace/product_tags_system_spec.rb | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 spec/furniture/marketplace/product_tags_system_spec.rb diff --git a/spec/furniture/marketplace/product_tags_system_spec.rb b/spec/furniture/marketplace/product_tags_system_spec.rb new file mode 100644 index 000000000..1954ce1ab --- /dev/null +++ b/spec/furniture/marketplace/product_tags_system_spec.rb @@ -0,0 +1,46 @@ +require "rails_helper" + +describe "Product Tags", type: :system do + let(:space) { create(:space, :with_entrance, :with_members) } + let(:marketplace) { create(:marketplace, :ready_for_shopping, room: space.entrance) } + + before do + sign_in(space.members.first, space) + end + + scenario "Adding Tags to a Product" do # rubocop:disable RSpec/Capybara/FeatureMethods,RSpec/ExampleLength + muffins = create(:marketplace_product, marketplace:, name: "Mazin' Muffins", description: "Buttery corn muffins") + + visit(marketplace) + click_link("Tags") + + click_link("Add Tag") + + fill_in("Label", with: "🚫🌾 Gluten Free") + + click_button("Create") + + click_link("Products") + within(muffins) do + click_link("⚙️ Edit") + end + + visit(marketplace) + + within(muffins) do + expect(page).to have_content("🚫🌾 Gluten Free") + end + end + + def visit(object_or_path) + if object_or_path.respond_to?(:location) + super(polymorphic_path(object_or_path.location)) + else + super + end + end + + def within(model, *, **, &block) + page.within("##{dom_id(model)}", *, **, &block) + end +end From 2c17ad23f8d180dfef776a280d36a9d1f7b1dcb1 Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Thu, 8 Feb 2024 08:38:23 -0800 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=A8=20`Marketplace`:=20Adds=20`Produc?= =?UTF-8?q?t#tags`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Products can now be tagged with things like `Gluten Free` or `Vegan`, and will show up as such under Product listings --- app/furniture/marketplace/bazaar.rb | 1 + app/furniture/marketplace/breadcrumbs.rb | 10 ++++++ app/furniture/marketplace/locales/en.yml | 5 +++ .../marketplace/management_component.html.erb | 1 + app/furniture/marketplace/marketplace.rb | 2 ++ app/furniture/marketplace/policy.rb | 3 +- app/furniture/marketplace/product.rb | 3 ++ .../product/title_component.html.erb | 4 +++ app/furniture/marketplace/product_policy.rb | 2 +- app/furniture/marketplace/product_tag.rb | 8 +++++ .../marketplace/products/_form.html.erb | 2 ++ app/furniture/marketplace/routes.rb | 1 + app/furniture/marketplace/tag.rb | 12 +++++++ app/furniture/marketplace/tag_policy.rb | 32 +++++++++++++++++++ app/furniture/marketplace/tags/_form.html.erb | 6 ++++ app/furniture/marketplace/tags/index.html.erb | 20 ++++++++++++ app/furniture/marketplace/tags/new.html.erb | 2 ++ app/furniture/marketplace/tags_controller.rb | 31 ++++++++++++++++++ ...8025354_create_marketplace_product_tags.rb | 17 ++++++++++ db/schema.rb | 19 +++++++++++ .../marketplace/product_tags_system_spec.rb | 3 ++ 21 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 app/furniture/marketplace/product_tag.rb create mode 100644 app/furniture/marketplace/tag.rb create mode 100644 app/furniture/marketplace/tag_policy.rb create mode 100644 app/furniture/marketplace/tags/_form.html.erb create mode 100644 app/furniture/marketplace/tags/index.html.erb create mode 100644 app/furniture/marketplace/tags/new.html.erb create mode 100644 app/furniture/marketplace/tags_controller.rb create mode 100644 db/migrate/20240208025354_create_marketplace_product_tags.rb diff --git a/app/furniture/marketplace/bazaar.rb b/app/furniture/marketplace/bazaar.rb index 2f4362604..eee8c4763 100644 --- a/app/furniture/marketplace/bazaar.rb +++ b/app/furniture/marketplace/bazaar.rb @@ -4,6 +4,7 @@ class Marketplace class Bazaar < ::Space has_many :marketplaces, through: :rooms, source: :gizmos, inverse_of: :bazaar, class_name: "Marketplace" has_many :tax_rates, inverse_of: :bazaar, dependent: :destroy + has_many :tags, inverse_of: :bazaar, dependent: :destroy def space becomes(Space) diff --git a/app/furniture/marketplace/breadcrumbs.rb b/app/furniture/marketplace/breadcrumbs.rb index c26cb14ab..b09a1e3ff 100644 --- a/app/furniture/marketplace/breadcrumbs.rb +++ b/app/furniture/marketplace/breadcrumbs.rb @@ -104,6 +104,16 @@ link t("marketplace.stripe_accounts.show.link_to"), marketplace.location(child: :stripe_account) end +crumb :marketplace_tags do |marketplace| + parent :edit_marketplace, marketplace + link(t("marketplace.tags.index.link_to"), marketplace.location(child: :tags)) +end + +crumb :new_marketplace_tag do |tag| + parent :marketplace_tags, tag.marketplace + link t("marketplace.tags.new.link_to"), marketplace.location(:new, child: :tag) +end + crumb :marketplace_tax_rates do |marketplace| parent :edit_marketplace, marketplace link t("marketplace.tax_rates.index.link_to"), marketplace.location(child: :tax_rates) diff --git a/app/furniture/marketplace/locales/en.yml b/app/furniture/marketplace/locales/en.yml index a8c0aa18a..4da9047a0 100644 --- a/app/furniture/marketplace/locales/en.yml +++ b/app/furniture/marketplace/locales/en.yml @@ -136,3 +136,8 @@ en: payment_settings: index: link_to: "Payment Settings" + tags: + index: + link_to: "Tags" + new: + link_to: "Add Tag" diff --git a/app/furniture/marketplace/management_component.html.erb b/app/furniture/marketplace/management_component.html.erb index 5c7b32a85..76d802a9a 100644 --- a/app/furniture/marketplace/management_component.html.erb +++ b/app/furniture/marketplace/management_component.html.erb @@ -21,6 +21,7 @@ <%= link_to_child(:notification_methods, icon: :bell_alert) if policy(marketplace.notification_methods).index? %> <%= link_to_child(:flyer, icon: :qr_code) if policy(marketplace.flyer).show? %> <%= link_to_child(:vendor_representatives, icon: :building_storefront) if policy(marketplace.vendor_representatives).index? %> + <%= link_to_child(:tags, icon: :tag) if policy(marketplace.tags).index? %> <% end %> <% end %> diff --git a/app/furniture/marketplace/marketplace.rb b/app/furniture/marketplace/marketplace.rb index 0cd4a250a..1031296b2 100644 --- a/app/furniture/marketplace/marketplace.rb +++ b/app/furniture/marketplace/marketplace.rb @@ -9,6 +9,8 @@ class Marketplace < Furniture # @todo replace with through :bazaar has_many :tax_rates, inverse_of: :marketplace + has_many :tags, through: :bazaar + has_many :products, inverse_of: :marketplace, dependent: :destroy has_many :carts, inverse_of: :marketplace, dependent: :destroy has_many :orders, inverse_of: :marketplace diff --git a/app/furniture/marketplace/policy.rb b/app/furniture/marketplace/policy.rb index bbad7025c..f0912237f 100644 --- a/app/furniture/marketplace/policy.rb +++ b/app/furniture/marketplace/policy.rb @@ -2,7 +2,7 @@ class Marketplace class Policy < ApplicationPolicy def create? return true if current_person.operator? - return true if current_person.member_of?(marketplace.space) + return true if current_person.member_of?(space) return true if shopper&.person.blank? && !current_person.authenticated? @@ -23,6 +23,7 @@ def marketplace object.marketplace if object.respond_to?(:marketplace) end + delegate :space, to: :marketplace module SpecFactories def self.included(spec) diff --git a/app/furniture/marketplace/product.rb b/app/furniture/marketplace/product.rb index 93e2dd6d2..be06f3411 100644 --- a/app/furniture/marketplace/product.rb +++ b/app/furniture/marketplace/product.rb @@ -26,6 +26,9 @@ class Product < Record has_many :ordered_products, inverse_of: :product, dependent: :destroy has_many :orders, -> { checked_out }, through: :ordered_products, inverse_of: :products + has_many :product_tags, inverse_of: :product, dependent: :destroy + has_many :tags, through: :product_tags, inverse_of: :products + has_many :product_tax_rates, inverse_of: :product, dependent: :destroy has_many :tax_rates, through: :product_tax_rates, inverse_of: :products diff --git a/app/furniture/marketplace/product/title_component.html.erb b/app/furniture/marketplace/product/title_component.html.erb index 9f21e96af..b5626e5b9 100644 --- a/app/furniture/marketplace/product/title_component.html.erb +++ b/app/furniture/marketplace/product/title_component.html.erb @@ -2,3 +2,7 @@ <%- if servings.present? %>

Serves <%= servings %>

<%- end %> + +<%- if product.tags.present? %> +

(<%= product.tags.pluck(:label).join(", ") %>)

+<%- end %> diff --git a/app/furniture/marketplace/product_policy.rb b/app/furniture/marketplace/product_policy.rb index ef63c0979..3b8d09636 100644 --- a/app/furniture/marketplace/product_policy.rb +++ b/app/furniture/marketplace/product_policy.rb @@ -4,7 +4,7 @@ class Marketplace class ProductPolicy < Policy alias_method :product, :object def permitted_attributes(_params = nil) - %i[name description price_cents price_currency price photo restore servings] + [tax_rate_ids: []] + %i[name description price_cents price_currency price photo restore servings] + [tag_ids: [], tax_rate_ids: []] end def update? diff --git a/app/furniture/marketplace/product_tag.rb b/app/furniture/marketplace/product_tag.rb new file mode 100644 index 000000000..368ddacd3 --- /dev/null +++ b/app/furniture/marketplace/product_tag.rb @@ -0,0 +1,8 @@ +class Marketplace + class ProductTag < Record + self.table_name = :marketplace_product_tags + + belongs_to :product, inverse_of: :product_tags + belongs_to :tag, inverse_of: :product_tags + end +end diff --git a/app/furniture/marketplace/products/_form.html.erb b/app/furniture/marketplace/products/_form.html.erb index c4e3f705b..4a38ab9db 100644 --- a/app/furniture/marketplace/products/_form.html.erb +++ b/app/furniture/marketplace/products/_form.html.erb @@ -5,6 +5,8 @@ <%= render "number_field", { attribute: :servings, form: f, min: 0, step: 1} %> <%= render "money_field", { attribute: :price, form: f, min: 0, step: 0.01} %> + <%= render "collection_check_boxes", { attribute: :tag_ids, collection: bazaar.tags, value_method: :id, + text_method: :label, form: f} %> <%= render "collection_check_boxes", { attribute: :tax_rate_ids, collection: bazaar.tax_rates, value_method: :id, text_method: :label, form: f} %> <%- if product.photo.present? %>
diff --git a/app/furniture/marketplace/routes.rb b/app/furniture/marketplace/routes.rb index d8bb9061d..52ae8e6e0 100644 --- a/app/furniture/marketplace/routes.rb +++ b/app/furniture/marketplace/routes.rb @@ -16,6 +16,7 @@ def self.append_routes(router) router.resources :products router.resource :stripe_account, only: [:show, :new, :create] router.resources :stripe_events + router.resources :tags router.resources :tax_rates router.resources :vendor_representatives router.resources :payment_settings, only: [:index] diff --git a/app/furniture/marketplace/tag.rb b/app/furniture/marketplace/tag.rb new file mode 100644 index 000000000..709a893fd --- /dev/null +++ b/app/furniture/marketplace/tag.rb @@ -0,0 +1,12 @@ +class Marketplace + class Tag < Record + self.table_name = "marketplace_tags" + + belongs_to :bazaar, inverse_of: :tags + has_many :product_tags, inverse_of: :tag, dependent: :destroy + has_many :products, through: :product_tags, inverse_of: :tags + + attr_accessor :marketplace + location(parent: :marketplace) + end +end diff --git a/app/furniture/marketplace/tag_policy.rb b/app/furniture/marketplace/tag_policy.rb new file mode 100644 index 000000000..cd43c1be4 --- /dev/null +++ b/app/furniture/marketplace/tag_policy.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Marketplace + class TagPolicy < Policy + alias_method :tag, :object + def space + tag.bazaar + end + + def permitted_attributes(_params = nil) + %i[label] + end + + def update? + return false unless current_person.authenticated? + + super + end + + alias_method :create?, :update? + + def show? + true + end + + class Scope < ApplicationScope + def resolve + scope.all + end + end + end +end diff --git a/app/furniture/marketplace/tags/_form.html.erb b/app/furniture/marketplace/tags/_form.html.erb new file mode 100644 index 000000000..acd3ab69e --- /dev/null +++ b/app/furniture/marketplace/tags/_form.html.erb @@ -0,0 +1,6 @@ +<%= form_with(model: tag.location) do |form| %> + +<%= render "text_field", attribute: :label, form: form %> + +<%= form.submit %> +<%- end %> diff --git a/app/furniture/marketplace/tags/index.html.erb b/app/furniture/marketplace/tags/index.html.erb new file mode 100644 index 000000000..61832ff4c --- /dev/null +++ b/app/furniture/marketplace/tags/index.html.erb @@ -0,0 +1,20 @@ +<%- breadcrumb :marketplace_tags, marketplace %> + +<%= render CardComponent.new do |card| %> + + <%- marketplace.tags.each do |tag| %> + <%- tag.marketplace = marketplace %> +
+ + <%= tag.label %> + +
+ <%- end %> + + <%- card.with_footer(variant: :action_bar) do %> + <%- new_tag = bazaar.tags.new %> + <%- if policy(new_tag).create? %> + <%= link_to t("marketplace.tags.new.link_to"), marketplace.location(:new, child: :tag), class: "button w-full" %> + <%- end %> + <%- end %> +<%- end %> diff --git a/app/furniture/marketplace/tags/new.html.erb b/app/furniture/marketplace/tags/new.html.erb new file mode 100644 index 000000000..d5e9383ef --- /dev/null +++ b/app/furniture/marketplace/tags/new.html.erb @@ -0,0 +1,2 @@ +<%- breadcrumb :new_marketplace_tag, mtag %> +<%= render "form", tag: mtag %> diff --git a/app/furniture/marketplace/tags_controller.rb b/app/furniture/marketplace/tags_controller.rb new file mode 100644 index 000000000..8e25ffb8a --- /dev/null +++ b/app/furniture/marketplace/tags_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class Marketplace + class TagsController < Controller + # Apparently, `tag` is used inside of `turbo_frame_tag`, so if we define a + # helper_method named `tag` our method gets called when it really shouldn't + # be... so `mtag` it is. For now. + expose :mtag, scope: -> { tags }, model: Tag + expose :tags, -> { policy_scope(bazaar.tags.create_with(marketplace: marketplace)) } + + def new + authorize(mtag) + end + + def create + if authorize(mtag).save + redirect_to marketplace.location(child: :tags) + else + render :new + end + end + + def index + skip_authorization + end + + def mtag_params + policy(Tag).permit(params.require(:tag)) + end + end +end diff --git a/db/migrate/20240208025354_create_marketplace_product_tags.rb b/db/migrate/20240208025354_create_marketplace_product_tags.rb new file mode 100644 index 000000000..4e1fcfaad --- /dev/null +++ b/db/migrate/20240208025354_create_marketplace_product_tags.rb @@ -0,0 +1,17 @@ +class CreateMarketplaceProductTags < ActiveRecord::Migration[7.1] + def change + create_table :marketplace_tags, id: :uuid do |t| + t.references :bazaar, type: :uuid + t.string :label + + t.timestamps + end + + create_table :marketplace_product_tags, id: :uuid do |t| + t.references :product, type: :uuid, foreign_key: {to_table: :marketplace_products} + t.references :tag, type: :uuid, foreign_key: {to_table: :marketplace_tags} + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 378aa23c1..c1fdcaeb2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -202,6 +202,15 @@ t.index ["shopper_id"], name: "index_marketplace_orders_on_shopper_id" end + create_table "marketplace_product_tags", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "product_id" + t.uuid "tag_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["product_id"], name: "index_marketplace_product_tags_on_product_id" + t.index ["tag_id"], name: "index_marketplace_product_tags_on_tag_id" + end + create_table "marketplace_product_tax_rates", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "tax_rate_id" t.uuid "product_id" @@ -231,6 +240,14 @@ t.index ["person_id"], name: "index_marketplace_shoppers_on_person_id", unique: true end + create_table "marketplace_tags", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "bazaar_id" + t.string "label" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["bazaar_id"], name: "index_marketplace_tags_on_bazaar_id" + end + create_table "marketplace_tax_rates", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.float "tax_rate" t.string "label" @@ -337,6 +354,8 @@ add_foreign_key "marketplace_notification_methods", "furnitures", column: "marketplace_id" add_foreign_key "marketplace_orders", "marketplace_delivery_areas", column: "delivery_area_id" add_foreign_key "marketplace_orders", "marketplace_shoppers", column: "shopper_id" + add_foreign_key "marketplace_product_tags", "marketplace_products", column: "product_id" + add_foreign_key "marketplace_product_tags", "marketplace_tags", column: "tag_id" add_foreign_key "marketplace_product_tax_rates", "marketplace_products", column: "product_id" add_foreign_key "marketplace_product_tax_rates", "marketplace_tax_rates", column: "tax_rate_id" add_foreign_key "marketplace_products", "furnitures", column: "marketplace_id" diff --git a/spec/furniture/marketplace/product_tags_system_spec.rb b/spec/furniture/marketplace/product_tags_system_spec.rb index 1954ce1ab..a4b642844 100644 --- a/spec/furniture/marketplace/product_tags_system_spec.rb +++ b/spec/furniture/marketplace/product_tags_system_spec.rb @@ -25,6 +25,9 @@ click_link("⚙️ Edit") end + check("🚫🌾 Gluten Free") + click_button("Save") + visit(marketplace) within(muffins) do From d5f0cb075b08f383c7caaca3e8c4b7e2ba5ecb39 Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Wed, 6 Mar 2024 18:34:06 -0800 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20`Tag`=20labels=20are=20case=20i?= =?UTF-8?q?nsensitive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/furniture/marketplace/tag.rb | 2 ++ spec/furniture/marketplace/tag_spec.rb | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 spec/furniture/marketplace/tag_spec.rb diff --git a/app/furniture/marketplace/tag.rb b/app/furniture/marketplace/tag.rb index 709a893fd..9d8e98db2 100644 --- a/app/furniture/marketplace/tag.rb +++ b/app/furniture/marketplace/tag.rb @@ -6,6 +6,8 @@ class Tag < Record has_many :product_tags, inverse_of: :tag, dependent: :destroy has_many :products, through: :product_tags, inverse_of: :tags + validates :label, uniqueness: {case_sensitive: false, scope: :bazaar_id} + attr_accessor :marketplace location(parent: :marketplace) end diff --git a/spec/furniture/marketplace/tag_spec.rb b/spec/furniture/marketplace/tag_spec.rb new file mode 100644 index 000000000..82d0d9849 --- /dev/null +++ b/spec/furniture/marketplace/tag_spec.rb @@ -0,0 +1,5 @@ +require "rails_helper" + +RSpec.describe Marketplace::Tag, type: :model do + it { is_expected.to validate_uniqueness_of(:label).case_insensitive.scoped_to(:bazaar_id) } +end