Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature tags step 1 #4884

Open
wants to merge 24 commits into
base: main
Choose a base branch
from

Conversation

coalest
Copy link
Collaborator

@coalest coalest commented Dec 22, 2024

Resolves #4820

Description

  • Add the ability to tag product drives. In a manner that will be expandable to tagging other entities.
  • When adding a tag, the current tags should be shown for selection, but another can be specified.
  • Add tagging to the product drive search.
  • tags are organization-specific
  • tagging entity will have (tag name, org id), entity type and entity id
  • the tagging filtration will show the tags for that kind of thing (in this case product drives) for that organization

Type of change

  • New feature

How Has This Been Tested?

Added new tests

Screenshots

Screenshot from 2025-01-07 16-44-42
Screenshot from 2025-01-07 16-44-20
Screenshot from 2025-01-07 16-45-00

@coalest coalest force-pushed the 4820-feature-tags-step-1 branch from 7fc06ef to 50bea61 Compare December 22, 2024 12:02
@coalest coalest force-pushed the 4820-feature-tags-step-1 branch from a0c0bb1 to a5c8808 Compare January 6, 2025 17:30
@coalest coalest changed the title WIP: Feature tags step 1 Feature tags step 1 Jan 6, 2025
@coalest
Copy link
Collaborator Author

coalest commented Jan 7, 2025

Requesting a review from you @cielf first as I want to make sure this implementation is functionally correct.

@coalest coalest requested a review from cielf January 7, 2025 15:46
@cielf cielf requested a review from dorner January 7, 2025 23:12
@cielf
Copy link
Collaborator

cielf commented Jan 7, 2025

I'm including in @dorner for parallel - and I might not get to it til the end of the week.

@@ -44,7 +45,7 @@ def end_date_is_bigger_of_end_date
return if start_date.nil? || end_date.nil?

if end_date < start_date
errors.add(:end_date, 'End date must be after the start date')
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes this duplicate text issue:

Screenshot from 2025-01-07 10-16-28

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated change, so I can move it to another PR if needed.

Comment on lines +161 to +162
# More concise test ("should") matchers
gem "shoulda-matchers", "~> 6.2"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this gem because otherwise rails g model fails because shoulda-matcher is needed for factory bot to create the spec factory files, so we need shoulda-matcher in the dev environment as well.

Copy link
Collaborator

@cielf cielf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good at first go-through.

@dorner
Copy link
Collaborator

dorner commented Jan 10, 2025

Sorry for the delay - I keep leaving this to last in my reviews because I have to think about it the most, and I always end up without enough time to address it :) Hopefully soon!

Copy link
Collaborator

@dorner dorner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So happy this is getting started! I think we need to tweak the solution a bit though.

@@ -66,9 +69,9 @@ def show

def update
@product_drive = current_organization.product_drives.find(params[:id])
if @product_drive.update(product_drive_params)
@product_drive.attributes = product_drive_params.merge(tags: tags_from_params)
if @product_drive.save
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this pattern is different than in create?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why i did that. Fixed in dbe77d7 to be more consistent with other update actions.

private

def set_organization_id(tagging)
tagging.organization_id ||= organization_id
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the join table need an organization_id?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this method is pretty broadly named for something that's going to be in a model... maybe rename it so it's specific to tagging?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there might be a modelling issue here. It's the Tag that should belong to the organization, not the Tagging. If we want to show only product drive tags, that should be an enum on the Tag record, not the Tagging. Tagging should be a simple join table between a "taggable" and a Tag.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either the tags table or the taggings join table need a foreign key reference to an organization. Functionally I think either would work. To me, it makes more sense that a tagging belongs to an organization rather than a tag. Because whoever is doing the tagging will belong to a certain organization so a tagging without an organization doesn't really make sense, whereas a tag doesn't necessarily need to belong to an organization it could be used by multiple organizations. If taggings references the organization, then tags can be referenced between organizations and this reduces the number of tag records. Adding type to the tag model also means more duplicates if for example there is a donation tag and a product drive tag both use the same tag name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it doesn't really matter but out of curiosity I checked and it looks like the acts_as_taggable_on gem also uses a similar schema.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I see what you're trying to achieve with this implementation, but I don't think it squares up with my thoughts.

My main concern is opening up the ability for tags to be a managed resource. In other words, organizations should be able to do things like rename tags, archive tags, or create tags in advance (e.g. to present a curated list).

If we limit ownership to the join table, that means that we can only work with tags as they are defined in relation to actual existing objects. So things like renaming would actually mean somehow creating a new tag and doing a "find/replace" on every taggable in the database. Ensuring an existing tag doesn't show up in a list of possible tags would be very difficult if not impossible.

In general we also want to try to keep things multitenant - organizations shouldn't share records if at all possible. This is why they don't all have the same Item list.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I didn't know what was meant by organizations being able to manage tags. If those are the requirements, then this for sure is wrong schema.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will organizations be able to create tags of a certain type without it being used? Or change the type of a tag?

If so I guess type should go on the tags table

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the first case is a reasonable one.

@cielf do you want to weigh in - am I right in my assumptions here?

Copy link
Collaborator

@cielf cielf Jan 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok -- I'm going to muse a bit here. We definitely need to discuss this more before making any changes based on these musings.

What's in my head, here, certainly wasn't explicit in the issue. Nor was the idea of typed tags.

We want the tags to be a tool the banks can use for identifying groups of things that belong together that we haven't otherwise grouped for them.

The use case that prompted this is a "campaign" for product drives - the idea of linking product drives with a common theme, like Mothers' Day or Christmas. I could certainly see having distributions associated with either of those examples.

I can also imagine banks using this to tag things associated with particular grants -- purchases, eligible partners or partner groups, special items, distributions.

So, from a user's point of view, why do we have type associated with the tags, at least, yet?

@@ -68,6 +68,9 @@ class Organization < ApplicationRecord
has_many :purchases
has_many :requests
has_many :storage_locations
has_many :taggings
has_many :product_drive_tags, -> { merge(Tagging.by_type("ProductDrive")).distinct },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here this should be a simple has_many instead of :through

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the refactor from 413a6e0 what you meant?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope - see above. I meant moving organization_id to tags.

class Tag < ApplicationRecord
has_many :taggings, dependent: :destroy

scope :alphabetized, -> { order(:name) }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...and this should have the by_type scope instead of Tagging.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved by_type scope to Tag in 413a6e0.

@@ -103,6 +107,7 @@
<td><%= product_drive.name %></td>
<td><%= product_drive.start_date.strftime("%m-%d-%Y") %></td>
<td><%= product_drive.end_date&.strftime("%m-%d-%Y") %></td>
<td><%= product_drive.tags.map(&:name).sort.join(", ") %></td>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a method that should live on the Taggable concern.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Fixed in 99f4f97.

db/migrate/20241221180952_create_tags.rb Show resolved Hide resolved
FactoryBot.define do
factory :tag do
name { "Holidays" }
initialize_with { Tag.find_or_create_by(name:) }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not the biggest fan of quietly changing this from "create new tag" to "find existing tag"... certainly not in the base factory. I'd rather have helper methods to work with this if needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, I wanted to avoid issues from tags being created in tests with non unique names, but this wasn't the right way to achieve that.

Fixed in efd015f.

@@ -47,6 +47,23 @@
expect(response.body).not_to include(product_drive_path(product_drive_five.id))
end

it "allows filtering by tag" do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a shared example?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean a shared example for filtering product drives? Or a shared example for filtering different resources by tags?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems premature to me to create a shared example for filtering by tags, since only product drives have tags.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the whole point of the tag feature is to make it a generic way to attach tags to anything - we know we're not going to limit it to product drives.

Copy link
Collaborator Author

@coalest coalest Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok then I will make it a shared example.

@coalest coalest requested a review from dorner January 16, 2025 11:28
@coalest
Copy link
Collaborator Author

coalest commented Jan 19, 2025

@dorner Refactored organization_id to the tags table.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature] Tags - Step 1
3 participants