Skip to content

Conversation

@giladshanan
Copy link
Contributor

@giladshanan giladshanan commented Mar 6, 2018

Release Notes

Add a list of the user's 10 most recently purchased products to the user's home page.

Screenshots

screen shot 2018-03-16 at 1 58 11 pm

Context

Allow users to access frequently used products faster.

Refactorings

  • Rename sorted scope on Facilitiy model to alphabetized
  • Add MostRecentlyUsedSearcher service object for Products and Facilities
  • Refactor public_calendar view helper to handle non-specific facility context

@giladshanan giladshanan requested review from jhanggi and meara March 6, 2018 16:09
@meara meara added the NU label Mar 6, 2018
Copy link
Contributor

@meara meara left a comment

Choose a reason for hiding this comment

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

Hey Gilad--great start! Let me know if you have any questions about the comments I left you--we can also go through this together or pair a bit if that's helpful.

Also, can you add the redmine ticket number to the PR title? We usually do [#123456] pull request title as the format.

facility_downcase: facility
facilities_downcase: facilities

Product: "!activerecord.models.product.one!"
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need these i18n fields for products. The reason we have them for facility is because Dartmouth uses "Resource" instead of "Facility" to refer to the facility model.

facilities:
index:
recently_used: Recently Used !Facilities!
recently_purchased: Recently Purchased !Products!
Copy link
Contributor

Choose a reason for hiding this comment

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

Per the ticket, I think this text should be "Recently Used Products"--you don't need to use the !Products! notation.

scope :not_archived, -> { where(is_archived: false) }
scope :mergeable_into_order, -> { not_archived.where(type: mergeable_types) }
scope :in_active_facility, -> { joins(:facility).where(facilities: { is_active: true }) }
scope :recently_purchased, -> { joins(order_details: :order).order('ordered_at DESC').distinct.limit(10) }
Copy link
Contributor

Choose a reason for hiding this comment

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

Does .order(ordered_at: :desc) work here? If so, it would be better to use that format to take advantage of ActiveRecord.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That syntax results in a Unknown column 'products.orders' error, but I think I figured out how to do it with .merge


- if @products.any?
%h4= text(".recently_purchased")
%table.table.table-striped.table-hover.product_list
Copy link
Contributor

Choose a reason for hiding this comment

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

I definitely get why you're using a table here; however, we don't typically use tables when listing products in this type of view, so it kind of breaks the style idiom for users, if that makes sense.

I would suggest modeling this after https://github.com/tablexi/nucore-open/blob/master/app/views/facilities/_product_list.html.haml#L7 -- that's what we use in facilities/show, so the link locations and style will match what the user expects.

%tr
%td
- if product.is_a?(Instrument)
= link_to product.name, [product.facility, product, :schedule]
Copy link
Contributor

Choose a reason for hiding this comment

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

So this link location, and the link on line 38, are actually linking to the admin views for the product. We want to link to the ordering views (see the product list partial I linked above), since this feature is for regular users trying to buy things, not admins wanting to manage products.

@meara
Copy link
Contributor

meara commented Mar 6, 2018

Oh also, one more small note: I think we want the "Recently Used Products" to appear at the top of the page, above the list of all facilities.

@giladshanan giladshanan changed the title add links to 10 most recently purchased products to home page [#137627] add links to 10 most recently purchased products to home page Mar 6, 2018
@giladshanan
Copy link
Contributor Author

@jhanggi can you give this a look-through when you have a chance? @meara and I both worked on it so it would be good to have another pair of eyes on it.

scope :not_archived, -> { where(is_archived: false) }
scope :mergeable_into_order, -> { not_archived.where(type: mergeable_types) }
scope :in_active_facility, -> { joins(:facility).where(facilities: { is_active: true }) }
scope :recently_purchased, ->(user) { joins(order_details: :order).merge(Order.where(user: user, state: :purchased).group(:product_id).order('MAX(orders.ordered_at) DESC')).limit(10) }
Copy link
Contributor

Choose a reason for hiding this comment

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

We're not still using this, right?

@meara
Copy link
Contributor

meara commented Mar 9, 2018

Also @giladshanan looks like you probably want to run rubocop -a on that new spec file.

Copy link
Contributor

@jhanggi jhanggi left a comment

Choose a reason for hiding this comment

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

Overall, great work. I like the separate object and the queries are definitely nicer now.

def recently_used_facilities(limit = 5)
return [] unless user

Facility.active.joins(products: { order_details: :order }).merge(Order.for_user(user).group("orders.facility_id").order('MAX(orders.ordered_at) DESC')).limit(limit)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a good way to add some line breaks in here to make the line not so long and easier to read?

Also, use single quotes. And can you say group(orders: :facility_id)? (It’s very likely that doesn’t work, but would probably be cleaner)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So group(orders: :facility_id) doesn't work (the group method doesn't accept hash arguments) but group(:id) does the trick.

Copy link
Contributor

Choose a reason for hiding this comment

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

I struggle with understanding why group(:id) works. Might just be how the two scopes get merged. I think I would lean towards keeping the string in that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@meara it looks like Oracle doesn't like the new AR queries so we're gonna have to break it up into two steps like it was before.


.alert.alert-info= text(".welcome")

- if @products.any?
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it might be nicer to call this @recent_products.

end
end

end No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

You use sublime, right? I would recommend adding the "ensure_newline_at_eof_on_save": true and ”trim_trailing_white_space_on_save": true settings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call, thanks!


- if @products.any?
%h4= text(".recently_purchased")
.product_list{ class: @products.first.class.model_name.to_s.pluralize.downcase }
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason we can’t use the product_list partial itself?

Part of it may be related to sorting, and if that’s the case, we could pull the sorting out of that and do it in facilities/show, though to be honest, I think we should sort the products alphabetically. I think it’ll be easier to use that way.

If we do decide to decide to leave this here, there’s probably some cleanup we can do: I don’t think the classes here actually do anything.

It might also be nice to add the facility name. But I could go either way.

Also, as noted in the ticket, we want to show recent facilities first, then products.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The product_list partial expects a current_facility, which we don't have in this context. Is there an easy workaround that would let us use the partial anyway?

Sorting alphabetically seems fine to me. What do you mean about adding the facility name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add the facility name to the product link? Something like: Product name (Facility name)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So facilities have a sorted scope and products have an alphabetized scope, they do very similar things. I lean towards alphabetized and sorted is only used in a handful of places. Do we want to change this or leave it alone?

@giladshanan
Copy link
Contributor Author

giladshanan commented Mar 12, 2018

screen shot 2018-03-12 at 10 43 40 am

Alphabetized + facility name

Copy link
Contributor

@jhanggi jhanggi left a comment

Choose a reason for hiding this comment

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

Very minor comments.

# GET /facilities
def index
@facilities = Facility.active.sorted
@recently_used_facilities = MostRecentlyUsedSearcher.new(acting_user).recently_used_facilities.sorted
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we create an aliased scope alphabetized for facilities? I do like that better than sorted. Probably want to leave sorted around for now because other stuff probably uses it. I'd be fine if we did that separately, though.

%ul
- @recent_products.each do |product|
%li
= link_to "#{product.name}" + " - #{product.facility}", facility_product_path(product.facility, product)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpicky, but I would do this all in one string: `"#{product.name} - #{product.facility}"

@giladshanan
Copy link
Contributor Author

@meara @jhanggi It looks like .sorted was only used in a handful of places so I went ahead and added the change here. I also included a commit that makes a few adjustments to the product_list partial so that we can use it here. If we go that route we won't get the facility name in the list. I wasn't sure what your thoughts were on that so I figured I'd include it here so you can give me some feedback.

Copy link
Contributor

@meara meara left a comment

Choose a reason for hiding this comment

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

This generally looks good, just a few small notes/questions.

Also, can you attach a screenshot of what it looks like post switch to product list partial?

@facilities = Facility.active.alphabetized
@recently_used_facilities = MostRecentlyUsedSearcher.new(acting_user).recently_used_facilities.alphabetized
@active_tab = "home"
@recent_products = MostRecentlyUsedSearcher.new(acting_user).recently_used_products.alphabetized
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we sure that including the alphabetized scope here doesn't affect which products are selected as recently used? I'm a little leery of adding additional sorting in here because of the way active record chains scopes. Can we test this?

Copy link
Contributor Author

@giladshanan giladshanan Mar 13, 2018

Choose a reason for hiding this comment

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

The alphabetized scope adds a secondary column to sort by, in this case the end of the query looks like this: ORDER BY MAX(orders.ordered_at) DESC, lower(products.name). Testing it out it doesn't seem to change the order at all. In fact, if you take a closer look at the first screenshot above, you'll see the products are not listed in alpha order. The ordering in the two more recent screenshots comes from the partial, which uses Array#sort on the products collection returned by @recent_products.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we ditch the alphabetized scope for products since it isn't doing much?

Copy link
Contributor

Choose a reason for hiding this comment

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

I was just about to suggest that 😀 . Leaving it in seems misleading, since the actual ordering is provided by the Array#sort in the partial.

It might be nice to pull the sort out of the partial and into the controller (and add a comment on why we're doing the sort in ruby and not in SQL)--you'd need to make sure you made the change for everywhere the partial is used, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, it looks like this would mean putting 10 .sorts in the facilities/show view. Is there another way to clarify intent here? Maybe a code comment in the facilities controller where we call the MostRecentlyUsedSearcher's?

- products.sort.each do |product|
%li{ class: product.class.model_name.to_s.downcase }
= public_calendar_link(product)
- if current_facility.present?
Copy link
Contributor

Choose a reason for hiding this comment

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

Does public_calendar_link require the facility? I think we can leave that in, I don't think having the calendar icon appear hurts anything, and it might be useful.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, if we can include it here, I say we do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The public_calendar_link does require a facility, it links to facility_instrument_public_schedule_path. There's a way to make it work though. I'll put it in my next commit with the other changes so you can let me know what you think.

- if current_facility.present?
%h3= product_list_title products, local_assigns[:title_extra]

- if products.first.class.model_name == "TimedService"
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the TimedService note ought to be nested under the if current_facility.present? as well. Try adding a Timed Service product type to see how it shows up.

= builder.text_field :quantity, value: 0, class: "product_quantity", index: nil
= builder.hidden_field :product_id, value: product.id, index: nil
= link_to product.name + (product.is_hidden? ? ' (hidden)' : ''), facility_product_path(current_facility, product)
= link_to product.name + (product.is_hidden? ? ' (hidden)' : ''), facility_product_path(current_facility || product.facility, product)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can just be product.facility, rather than using the or.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also we should check for N+1s here.

Copy link
Contributor

@jhanggi jhanggi left a comment

Choose a reason for hiding this comment

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

Couple minor style notes, but otherwise I agree with all Meara's comments

.joins(products: { order_details: :order })
.merge(Order.for_user(user)
.group(:id)
.order('MAX(orders.ordered_at) DESC'))
Copy link
Contributor

Choose a reason for hiding this comment

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

Use double quotes for strings.

.joins(order_details: :order)
.merge(Order.for_user(user)
.group(:id)
.order('MAX(orders.ordered_at) DESC'))
Copy link
Contributor

Choose a reason for hiding this comment

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

Double quotes

- products.sort.each do |product|
%li{ class: product.class.model_name.to_s.downcase }
= public_calendar_link(product)
- if current_facility.present?
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, if we can include it here, I say we do it.

@giladshanan
Copy link
Contributor Author

screen shot 2018-03-13 at 2 10 28 pm

Partial view

@giladshanan
Copy link
Contributor Author

screen shot 2018-03-13 at 2 10 49 pm

Partial view with calendar (still working on the other suggested changes, but this is how it looks)

Copy link
Contributor

@meara meara left a comment

Choose a reason for hiding this comment

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

Getting so close!


def public_calendar_options(product)
if current_facility.show_instrument_availability?
if current_facility&.show_instrument_availability?
Copy link
Contributor

Choose a reason for hiding this comment

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

I would switch this to current_facility&.show_instrument_availability? || !current_facility && product.facility.show_instrument_availability?, which is kind of gross, but ensures that we avoid N+1s in the facility context by using current_facility, and when we're in the home page context it will display with colors or not as appropriate (which is what show_instrument_availability? is for).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

does current_facility&.show_instrument_availability? || product.facility.show_instrument_availability? do the trick?

Copy link
Contributor

Choose a reason for hiding this comment

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

The issue is we only want to call product.facility if current_facility is not present, rather than any time current_facility&.show_instrument_availability? evaluates to false. Does that make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, that does make sense, thanks.

def recently_used_products(limit = 10)
return [] unless user

Product.active.in_active_facility
Copy link
Contributor

Choose a reason for hiding this comment

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

I would put includes(:facility) explicitly here, rather than in the in_active_facility scope.

= builder.text_field :quantity, value: 0, class: "product_quantity", index: nil
= builder.hidden_field :product_id, value: product.id, index: nil
= link_to product.name + (product.is_hidden? ? ' (hidden)' : ''), facility_product_path(current_facility, product)
= link_to product.name + (product.is_hidden? ? ' (hidden)' : ''), facility_product_path(product.facility, product)
Copy link
Contributor

@meara meara Mar 13, 2018

Choose a reason for hiding this comment

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

On second thought, I think what you had with current_facility || product.facility is better, because it lets us avoid all the includes we would need in the facility-specific context to prevent N+1s.

Specifically on this page: https://github.com/tablexi/nucore-open/blob/master/app/views/facilities/show.html.haml

Copy link
Contributor

@jhanggi jhanggi left a comment

Choose a reason for hiding this comment

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

Had some thoughts about how we're doing the sorting, but we can deal with that later.

@recently_used_facilities = MostRecentlyUsedSearcher.new(acting_user).recently_used_facilities.alphabetized
@active_tab = "home"
# recent products get sorted alphabetically in the product_list partial
@recent_products = MostRecentlyUsedSearcher.new(acting_user).recently_used_products.includes(:facility)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any downside to adding the alphabetization here, even if it's still re-sorted in the partial. We don't need to do it now, but I think it would be nice to not do the sort in the product_list partial (facilities/show is kind of nasty already, but could defiitely be cleaned up), but that can be for another time.

Copy link
Contributor

@meara meara left a comment

Choose a reason for hiding this comment

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

👍 pending circle

@giladshanan
Copy link
Contributor Author

🙌

@giladshanan giladshanan merged commit dc3e3a8 into master Mar 16, 2018
@giladshanan giladshanan deleted the 137627-last-ten-unhidden-products branch March 16, 2018 19:32
meara added a commit that referenced this pull request Mar 20, 2018
…d in user (#1455)

Release Notes
Fix error on recently used products/facilities when there is no logged in user, follow up to #1429.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants