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 35 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 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?

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've added organization_id to the tags table as requested, so an organization can create/edit/delete tags independently of them being used to tag something.

Only remaining question is whether the tags table should have a type or not. We can either wait until that is a requirement in the future or I can add it to this PR. Up to y'all

Copy link
Collaborator

Choose a reason for hiding this comment

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

We discussed during the meeting. Tags should have a type. Taggings should not need to have one since it has the taggable_id / taggable_type for the polymorphic association.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 0cd6971.

@@ -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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 8b7242d.

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 Outdated 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 374dea1.

@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.

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.

Minor quibble with a name.

@cielf this looks good to me now - do you want to take one more look on the QA side since the implementation has changed?

has_many :taggings, as: :taggable, dependent: :destroy
has_many :tags, through: :taggings, source: :tag

scope :by_tags, ->(tags) { left_joins(:tags).where(tags: {name: tags}) }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we name the parameter tag_names? It was a bit confusing till I saw where it was used.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 4561a63.

@cielf
Copy link
Collaborator

cielf commented Jan 22, 2025

@dorner Yup -- will try to take a look at it tomorrow.

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.

Gave it another test. I don't know if I checked this out the first time, but if you have an error, the tags are not showing properly.
Screenshot 2025-01-22 at 10 53 11 AM
You can't put in the tags.
And, if you then try to save, you get a 500 (unless you clear the tags field)

@coalest
Copy link
Collaborator Author

coalest commented Jan 22, 2025

Whoops, that was working at some point.

I just pushed commits to fix the issue with corresponding request specs.

@cielf It is ready for testing again.

@coalest coalest requested a review from cielf January 22, 2025 20:04
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.

I seem to be stopping as soon as a notice a problem. This time...

Any other field, if you click to a different field, the information stays.

Not so with tags.

So, for example, if you type in your tag and hit submit without hitting enter first, you lose it. Or if you type in your tag, notice a problem in the date and go to fix that, the tag disappears.

Is that something we can reasonably address?

@coalest
Copy link
Collaborator Author

coalest commented Jan 23, 2025

@cielf Fixed in my last commit. That is configurable by select2, so I just had to set selectOnClose to true.

Here are the other configuration options for this dropdown in case you're curious: https://select2.org/configuration/options-api.

@coalest coalest requested a review from cielf January 23, 2025 21:29
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.

Hmm, Try this sequence. New product drive, don't enter a name, but enter the start date. In tags: "tagname1", hit enter, "tagname2", hit enter, "tagname3" then click save without hitting enter.
What I'm seeing in this case is that the tags go away.

@coalest
Copy link
Collaborator Author

coalest commented Jan 24, 2025

I think what you might be experiencing is that new tags (ones that haven't yet been saved to a product drive) don't persist after a validation failure. That's because when the page re-renders the tag dropdown options are created from all currently existing product drive tags. Which since the validation failed, that product drive tag didn't get created and therefore doesn't appear any more.

Is this considered an edge case? Or should i fix it?

To fix it, I see two options:

  1. Just create the tags regardless of whether the product drive validation passes. Pro -> easy fix. Con -> could lead to unwanted tags created.
  2. Merge the tags that were attempted to be created with the currently existing tags. Pro -> tags dont get created unless product drive update succeeds. Con -> a little bit messier logic in the controller.

@cielf
Copy link
Collaborator

cielf commented Jan 24, 2025

Well, I don't think 1/ is any worse than if they had typed in those tags on a successful product drive creation, so I'm good with that. Honestly, the most likely error is someone mistyping a end date.

@coalest
Copy link
Collaborator Author

coalest commented Jan 24, 2025

@cielf Ok in that case, I just pushed a commit that implemented option 1. Ready for testing again.

@cielf
Copy link
Collaborator

cielf commented Jan 26, 2025

LGTM. @dorner do you want to take a quick look at the latter changes?

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