Merge pull request #10772 from Matt-Yorkley/view-caching

Locale-aware Fragment Caching
This commit is contained in:
Filipe
2023-05-10 11:18:08 +01:00
committed by GitHub
32 changed files with 797 additions and 616 deletions

View File

@@ -18,6 +18,8 @@ module Admin
end
end
ContentConfig.updated_at = Time.zone.now
flash[:success] =
t(:successfully_updated, resource: I18n.t('admin.contents.edit.your_content'))

View File

@@ -68,4 +68,14 @@ module ApplicationHelper
wicked_pdf_stylesheet_pack_tag(source)
end
end
def cache_with_locale(key = nil, options = {}, &block)
cache(cache_key_with_locale(key, I18n.locale), options) do
yield(block)
end
end
def cache_key_with_locale(key, locale)
Array.wrap(key) + [locale.to_s, I18nDigests.for_locale(locale)]
end
end

View File

@@ -83,4 +83,19 @@ class ContentConfiguration < Spree::Preferences::Configuration
# User Guide
preference :user_guide_link, :string, default: 'https://guide.openfoodnetwork.org/'
# ContentConfig Caching
preference :updated_at_timestamp, :integer, default: Time.zone.today.to_time.to_i
def updated_at
Time.zone.at updated_at_timestamp
end
def updated_at=(time)
self.updated_at_timestamp = time.to_i
end
def cache_key
"ContentConfig:#{updated_at_timestamp}"
end
end

View File

@@ -1 +1,2 @@
%img.spinner{ src: image_pack_path("spinning-circles.svg"), style: "max-width: 100%" }
= cache do
%img.spinner{ src: image_pack_path("spinning-circles.svg"), style: "max-width: 100%" }

View File

@@ -1,8 +1,9 @@
#tagline
.row
.small-12.text-center.columns
%h1
%img{src: image_pack_path("logo-white-notext.png"), title: Spree::Config.site_name}
%br/
%a.button.transparent{href: "/shops"}
= t :home_shop
= cache_with_locale "sitename:#{Spree::Config.site_name}" do
#tagline
.row
.small-12.text-center.columns
%h1
%img{src: image_pack_path("logo-white-notext.png"), title: Spree::Config.site_name}
%br/
%a.button.transparent{href: "/shops"}
= t :home_shop

View File

@@ -1,91 +1,92 @@
.row.active_table_row{"ng-if" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : open()}"}
.columns.small-12.fat.text-center{"ng-show" => "open() && shopfront_loading"}
%p.fullwidth
= render partial: "components/spinner"
= cache_with_locale do
.row.active_table_row{"ng-if" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : open()}"}
.columns.small-12.fat.text-center{"ng-show" => "open() && shopfront_loading"}
%p.fullwidth
= render partial: "components/spinner"
.columns.small-12.medium-7.large-7.fat{"ng-show" => "open() && !shopfront_loading"}
/ Will add in long description available once clean up HTML formatting producer.long_description
%div{"ng-if" => "::producer.description"}
%label
= t :producers_about
%img.right.show-for-medium-up{"ng-src" => "{{::producer.logo}}" }
%p.text-small{ "ng-bind" => "::producer.description"}
%div.show-for-medium-up{"ng-if" => "::producer.description.length==0"}
%label &nbsp;
%img.right.show-for-medium-up{"ng-src" => "{{::producer.logo}}" }
.columns.small-12.medium-7.large-7.fat{"ng-show" => "open() && !shopfront_loading"}
/ Will add in long description available once clean up HTML formatting producer.long_description
%div{"ng-if" => "::producer.description"}
%label
= t :producers_about
%img.right.show-for-medium-up{"ng-src" => "{{::producer.logo}}" }
%p.text-small{ "ng-bind" => "::producer.description"}
%div.show-for-medium-up{"ng-if" => "::producer.description.length==0"}
%label &nbsp;
%img.right.show-for-medium-up{"ng-src" => "{{::producer.logo}}" }
.columns.small-12.medium-5.large-5.fat{"ng-show" => "open() && !shopfront_loading"}
.columns.small-12.medium-5.large-5.fat{"ng-show" => "open() && !shopfront_loading"}
%div{"ng-if" => "::producer.supplied_taxons"}
%label
= t :producers_buy
%p.trans-sentence
%div
%span.fat-taxons{"ng-repeat" => "taxon in producer.supplied_taxons"}
%span{"ng-bind" => "::taxon.name"}
%div
%span.fat-properties{"ng-repeat" => "property in producer.supplied_properties"}
%span{"ng-bind" => "property.presentation"}
%div{"ng-if" => "::producer.supplied_taxons"}
%label
= t :producers_buy
%p.trans-sentence
%div
%span.fat-taxons{"ng-repeat" => "taxon in producer.supplied_taxons"}
%span{"ng-bind" => "::taxon.name"}
%div
%span.fat-properties{"ng-repeat" => "property in producer.supplied_properties"}
%span{"ng-bind" => "property.presentation"}
%div.show-for-medium-up{"ng-if" => "producer.supplied_taxons.length==0"}
&nbsp;
%div.show-for-medium-up{"ng-if" => "producer.supplied_taxons.length==0"}
&nbsp;
%div{"ng-if" => "::producer.email_address || producer.website || producer.phone"}
%label
= t :producers_contact
%div{"ng-if" => "::producer.email_address || producer.website || producer.phone"}
%label
= t :producers_contact
%p.word-wrap{"ng-if" => "::producer.phone"}
= t :producers_contact_phone
%span{"ng-bind" => "::producer.phone"}
%p.word-wrap{"ng-if" => "::producer.phone"}
= t :producers_contact_phone
%span{"ng-bind" => "::producer.phone"}
%p.word-wrap{"ng-if" => "::producer.whatsapp_phone"}
%a{"ng-href" => "{{::producer.whatsapp_url}}", target: "_blank"}
%img{ src: image_pack_path("social-logos/whatsapp.svg") }
%span{"ng-bind" => "::producer.whatsapp_phone"}
%p.word-wrap{"ng-if" => "::producer.whatsapp_phone"}
%a{"ng-href" => "{{::producer.whatsapp_url}}", target: "_blank"}
%img{ src: image_pack_path("social-logos/whatsapp.svg") }
%span{"ng-bind" => "::producer.whatsapp_phone"}
%p.word-wrap{"ng-if" => "::producer.email_address"}
%a{"ng-href" => "{{::producer.email_address | stripUrl}}", target: "_blank", mailto: true}
%span.obfuscatedEmail.email{"ng-bind" => "::producer.email_address | stripUrl"}
%p.word-wrap{"ng-if" => "::producer.email_address"}
%a{"ng-href" => "{{::producer.email_address | stripUrl}}", target: "_blank", mailto: true}
%span.obfuscatedEmail.email{"ng-bind" => "::producer.email_address | stripUrl"}
%p.word-wrap{"ng-if" => "::producer.website"}
%a{"ng-href" => "http://{{::producer.website | stripUrl}}", target: "_blank" }
%span{"ng-bind" => "::producer.website | stripUrl"}
%p.word-wrap{"ng-if" => "::producer.website"}
%a{"ng-href" => "http://{{::producer.website | stripUrl}}", target: "_blank" }
%span{"ng-bind" => "::producer.website | stripUrl"}
%div{"ng-if" => "::producer.twitter || producer.facebook || producer.linkedin || producer.instagram"}
%label
= t :producers_social
.follow-icons
%span{"ng-if" => "::producer.twitter"}
%a{"ng-href" => "http://twitter.com/{{::producer.twitter}}", target: "_blank"}
%i.ofn-i_041-twitter
%div{"ng-if" => "::producer.twitter || producer.facebook || producer.linkedin || producer.instagram"}
%label
= t :producers_social
.follow-icons
%span{"ng-if" => "::producer.twitter"}
%a{"ng-href" => "http://twitter.com/{{::producer.twitter}}", target: "_blank"}
%i.ofn-i_041-twitter
%span{"ng-if" => "::producer.facebook"}
%a{"ng-href" => "http://{{::producer.facebook | stripUrl}}", target: "_blank"}
%i.ofn-i_044-facebook
%span{"ng-if" => "::producer.facebook"}
%a{"ng-href" => "http://{{::producer.facebook | stripUrl}}", target: "_blank"}
%i.ofn-i_044-facebook
%span{"ng-if" => "::producer.linkedin"}
%a{"ng-href" => "http://{{::producer.linkedin | stripUrl}}", target: "_blank"}
%i.ofn-i_042-linkedin
%span{"ng-if" => "::producer.linkedin"}
%a{"ng-href" => "http://{{::producer.linkedin | stripUrl}}", target: "_blank"}
%i.ofn-i_042-linkedin
%span{"ng-if" => "::producer.instagram"}
%a{"ng-href" => "http://instagram.com/{{::producer.instagram}}", target: "_blank"}
%i.ofn-i_043-instagram
%span{"ng-if" => "::producer.instagram"}
%a{"ng-href" => "http://instagram.com/{{::producer.instagram}}", target: "_blank"}
%i.ofn-i_043-instagram
.row.active_table_row.pad-top{"ng-if" => "open() && producer.hubs && !shopfront_loading"}
.columns.small-12{"ng-if" => "producer.hubs.length > 0"}
.row
.columns.small-12.fat
%div{"ng-if" => "::producer.name"}
%label
= t :producers_buy_at_html, enterprise: '<span class="turquoise" ng-bind="::producer.name"></span>'.html_safe
%div.show-for-medium-up{"ng-if" => "::!producer.name"}
&nbsp;
.row.cta-container
.columns.small-12
%a.cta-hub{"ng-repeat" => "hub in producer.hubs | orderBy:'-active'",
"ng-href" => "{{::hub.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined }}",
"ng-class" => "::{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"}
%i.ofn-i_068-shop-reversed{"ng-if" => "::hub.active"}
%i.ofn-i_068-shop-reversed{"ng-if" => "::!hub.active"}
.hub-name{"ng-bind" => "::hub.name"}
.button-address{"ng-bind" => "::[hub.address.city, hub.address.state_name] | printArray"}
.row.active_table_row.pad-top{"ng-if" => "open() && producer.hubs && !shopfront_loading"}
.columns.small-12{"ng-if" => "producer.hubs.length > 0"}
.row
.columns.small-12.fat
%div{"ng-if" => "::producer.name"}
%label
= t :producers_buy_at_html, enterprise: '<span class="turquoise" ng-bind="::producer.name"></span>'.html_safe
%div.show-for-medium-up{"ng-if" => "::!producer.name"}
&nbsp;
.row.cta-container
.columns.small-12
%a.cta-hub{"ng-repeat" => "hub in producer.hubs | orderBy:'-active'",
"ng-href" => "{{::hub.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined }}",
"ng-class" => "::{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"}
%i.ofn-i_068-shop-reversed{"ng-if" => "::hub.active"}
%i.ofn-i_068-shop-reversed{"ng-if" => "::!hub.active"}
.hub-name{"ng-bind" => "::hub.name"}
.button-address{"ng-bind" => "::[hub.address.city, hub.address.state_name] | printArray"}

View File

@@ -1,115 +1,119 @@
%footer
.footer-global
.row
.small-12.columns.text-center
.logo
%img{src: image_pack_path("logo-white-notext.png") }
.row
.small-12.medium-8.medium-offset-2.columns.text-center
.alert-box
= render 'shared/register_call'
= cache_with_locale "global" do
.row
.small-12.columns.text-center
.logo
%img{src: image_pack_path("logo-white-notext.png") }
.row
.small-12.medium-8.medium-offset-2.columns.text-center
.alert-box
= render 'shared/register_call'
.footer-local
.row
.small-12.medium-2.medium-offset-2.columns.text-center
%p.secure-icon
%i.ofn-i_017-locked
.small-12.medium-6.columns.text-center
%p.text-big.secure-text
= t '.footer_secure'
%p.secure-text
= t '.footer_secure_text'
.small-12.medium-2.columns
= cache_with_locale "local" do
.row
.small-12.medium-2.medium-offset-2.columns.text-center
%p.secure-icon
%i.ofn-i_017-locked
.small-12.medium-6.columns.text-center
%p.text-big.secure-text
= t '.footer_secure'
%p.secure-text
= t '.footer_secure_text'
.small-12.medium-2.columns
.row
.small-12.medium-8.medium-offset-2.columns.text-center
%hr.hr-light
%br
.row
.small-12.medium-8.medium-offset-2.columns.text-center
%hr.hr-light
%br
.row
.small-6.medium-3.medium-offset-2.columns.text-left
// This is the instance-managed set of links:
%h4
= t '.footer_contact_headline'
- if show_social_icons?
%p.social-icons
- if ContentConfig.footer_facebook_url.present?
%a{href: ContentConfig.footer_facebook_url}
%i.ofn-i_044-facebook
- if ContentConfig.footer_twitter_url.present?
%a{href: ContentConfig.footer_twitter_url}
%i.ofn-i_041-twitter
- if ContentConfig.footer_instagram_url.present?
%a{href: ContentConfig.footer_instagram_url}
%i.ofn-i_043-instagram
- if ContentConfig.footer_linkedin_url.present?
%a{href: ContentConfig.footer_linkedin_url}
%i.ofn-i_042-linkedin
- if ContentConfig.footer_googleplus_url.present?
%a{href: ContentConfig.footer_googleplus_url}
%i.ofn-i_046-g
- if ContentConfig.footer_pinterest_url.present?
%a{href: ContentConfig.footer_pinterest_url}
%i.ofn-i_045-pintrest
- if ContentConfig.footer_email.present?
= cache_with_locale ContentConfig.cache_key do
.row
.small-6.medium-3.medium-offset-2.columns.text-left
// This is the instance-managed set of links:
%h4
= t '.footer_contact_headline'
- if show_social_icons?
%p.social-icons
- if ContentConfig.footer_facebook_url.present?
%a{href: ContentConfig.footer_facebook_url}
%i.ofn-i_044-facebook
- if ContentConfig.footer_twitter_url.present?
%a{href: ContentConfig.footer_twitter_url}
%i.ofn-i_041-twitter
- if ContentConfig.footer_instagram_url.present?
%a{href: ContentConfig.footer_instagram_url}
%i.ofn-i_043-instagram
- if ContentConfig.footer_linkedin_url.present?
%a{href: ContentConfig.footer_linkedin_url}
%i.ofn-i_042-linkedin
- if ContentConfig.footer_googleplus_url.present?
%a{href: ContentConfig.footer_googleplus_url}
%i.ofn-i_046-g
- if ContentConfig.footer_pinterest_url.present?
%a{href: ContentConfig.footer_pinterest_url}
%i.ofn-i_045-pintrest
- if ContentConfig.footer_email.present?
%p
%a{href: ContentConfig.footer_email.reverse, mailto: true, target: '_blank'}
= t '.footer_contact_email'
= render_markdown(ContentConfig.footer_links_md).html_safe
.small-6.medium-3.columns.text-left
%h4
= t '.footer_nav_headline'
%p
%a{href: ContentConfig.footer_email.reverse, mailto: true, target: '_blank'}
= t '.footer_contact_email'
= render_markdown(ContentConfig.footer_links_md).html_safe
%a{href: "/shops"}
= t :label_shops
%p
%a{href: "/map"}
= t :label_map
%p
%a{href: "/producers"}
= t :label_producers
%p
%a{href: "/groups"}
= t :label_groups
%p
%a{href: ContentConfig.footer_about_url}
= t :label_about
.small-12.medium-2.columns.text-left
%h4
= t '.footer_join_headline'
%p
= t '.footer_join_body'
%a{href: "/sell"}
= t '.footer_join_cta'
.small-6.medium-3.columns.text-left
%h4
= t '.footer_nav_headline'
%p
%a{href: "/shops"}
= t :label_shops
%p
%a{href: "/map"}
= t :label_map
%p
%a{href: "/producers"}
= t :label_producers
%p
%a{href: "/groups"}
= t :label_groups
%p
%a{href: ContentConfig.footer_about_url}
= t :label_about
.medium-2.columns.text-center
/ Placeholder
.small-12.medium-2.columns.text-left
%h4
= t '.footer_join_headline'
%p
= t '.footer_join_body'
%a{href: "/sell"}
= t '.footer_join_cta'
.row
.small-12.medium-8.medium-offset-2.columns.text-center
%hr.hr-light
%br
.medium-2.columns.text-center
/ Placeholder
.row
.small-12.medium-8.medium-offset-2.columns.text-center
%hr.hr-light
%br
.row.legal
.small-12.medium-3.medium-offset-2.columns.text-left
%a{href: main_app.root_path}
%img{src: ContentConfig.url_for(:footer_logo), width: "220"}
.small-12.medium-5.columns.text-left
%p.text-small
= t '.footer_legal_call'
= link_to_platform_terms
&#124;
= t '.footer_legal_visit'
%a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank"} GitHub
%p.text-small
= t('.footer_legal_text_html', content_license: link_to('CC BY-SA 3.0', 'https://creativecommons.org/licenses/by-sa/3.0/'), code_license: link_to('AGPL 3', 'https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)' ))
%p.text-small
- if Spree::Config.privacy_policy_url.present?
= t('.footer_data_text_with_privacy_policy_html', cookies_policy: cookies_policy_link.html_safe, privacy_policy: privacy_policy_link.html_safe)
- else
= t('.footer_data_text_without_privacy_policy_html', cookies_policy: cookies_policy_link.html_safe)
.medium-2.columns.text-center
/ Placeholder
= cache_with_locale [ContentConfig.cache_key, TermsOfServiceFile.current_url, Spree::Config.privacy_policy_url] do
.row.legal
.small-12.medium-3.medium-offset-2.columns.text-left
%a{href: main_app.root_path}
%img{src: ContentConfig.url_for(:footer_logo), width: "220"}
.small-12.medium-5.columns.text-left
%p.text-small
= t '.footer_legal_call'
= link_to_platform_terms
&#124;
= t '.footer_legal_visit'
%a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank"} GitHub
%p.text-small
= t('.footer_legal_text_html', content_license: link_to('CC BY-SA 3.0', 'https://creativecommons.org/licenses/by-sa/3.0/'), code_license: link_to('AGPL 3', 'https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)' ))
%p.text-small
- if Spree::Config.privacy_policy_url.present?
= t('.footer_data_text_with_privacy_policy_html', cookies_policy: cookies_policy_link.html_safe, privacy_policy: privacy_policy_link.html_safe)
- else
= t('.footer_data_text_without_privacy_policy_html', cookies_policy: cookies_policy_link.html_safe)
.medium-2.columns.text-center
/ Placeholder

View File

@@ -1,12 +1,13 @@
.alert-cta
%h6
-# Please forgive the hard-coded link:
-# The more elegant 'registration_path' resolves to /signup due to spree_auth_device > config > routes.rb
-# This is one of several possible fixes. Long-term, we'd like to bring the accounts page into OFN.
-# View the discussion here: https://github.com/openfoodfoundation/openfoodnetwork/pull/3174
%a{href: "/register", target: "_blank"}
= t '.selling_on_ofn'
&nbsp;
%strong
= t '.register'
%i.ofn-i_054-point-right
= cache_with_locale do
.alert-cta
%h6
-# Please forgive the hard-coded link:
-# The more elegant 'registration_path' resolves to /signup due to spree_auth_device > config > routes.rb
-# This is one of several possible fixes. Long-term, we'd like to bring the accounts page into OFN.
-# View the discussion here: https://github.com/openfoodfoundation/openfoodnetwork/pull/3174
%a{href: "/register", target: "_blank"}
= t '.selling_on_ofn'
&nbsp;
%strong
= t '.register'
%i.ofn-i_054-point-right

View File

@@ -1,8 +1,9 @@
%span.cart-span{"ng-controller" => "CartCtrl", "ng-class" => "{ dirty: Cart.dirty || Cart.empty(), 'pure-dirty': Cart.dirty }"}
%a#cart.icon{"ng-click" => "toggleCartSidebar()"}
%span
= t '.cart'
%span.count
%img{ src: image_pack_path("menu/icn-cart.svg") }
= cache_with_locale do
%span.cart-span{"ng-controller" => "CartCtrl", "ng-class" => "{ dirty: Cart.dirty || Cart.empty(), 'pure-dirty': Cart.dirty }"}
%a#cart.icon{"ng-click" => "toggleCartSidebar()"}
%span
{{ Cart.total_item_count() }}
= t '.cart'
%span.count
%img{ src: image_pack_path("menu/icn-cart.svg") }
%span
{{ Cart.total_item_count() }}

View File

@@ -1,38 +1,40 @@
.expanding-sidebar.cart-sidebar{ng: {controller: 'CartCtrl', class: "{'shown': showCartSidebar}"}}
.background{ng: {click: 'toggleCartSidebar()'}}
.sidebar
.cart-header
%span.title{"ng-show" => "Cart.line_items.length == 1"}
= t('.items_in_cart_singular', num: "{{ Cart.total_item_count() }}")
%span.title{"ng-show" => "Cart.line_items.length > 1"}
= t('.items_in_cart_plural', num: "{{ Cart.total_item_count() }}")
%a.close{ng: {click: 'toggleCartSidebar()'}}
= t('.close')
%i.ofn-i_009-close
= cache_with_locale "cart-header" do
.cart-header
%span.title{"ng-show" => "Cart.line_items.length == 1"}
= t('.items_in_cart_singular', num: "{{ Cart.total_item_count() }}")
%span.title{"ng-show" => "Cart.line_items.length > 1"}
= t('.items_in_cart_plural', num: "{{ Cart.total_item_count() }}")
%a.close{ng: {click: 'toggleCartSidebar()'}}
= t('.close')
%i.ofn-i_009-close
.cart-content
%table
%tr.product-cart{"ng-repeat" => "line_item in Cart.line_items", "id" => "cart-variant-{{ line_item.variant.id }}"}
%td.image
%img{'ng-src' => '{{ line_item.variant.thumb_url }}'}
%td
%span {{ line_item.variant.extended_name | truncate: max_characters }}
%br
%span.options-text {{ line_item.variant.options_text | truncate: max_characters }}
%td.text-right
%span.quantity {{ line_item.quantity }}
%td
.total-price.text-right {{ line_item.total_price | localizeCurrency }}
.unit-price
%div{:style => "margin-right: 5px"}
%question-mark-with-tooltip{"question-mark-with-tooltip" => "_",
"question-mark-with-tooltip-append-to-body" => "true",
"question-mark-with-tooltip-placement" => "top",
"question-mark-with-tooltip-animation" => true,
key: "'js.shopfront.unit_price_tooltip'",
context: "'cart-sidebar'"}
.options-text
{{ line_item.variant.unit_price_price | localizeCurrency }}&nbsp;/&nbsp;{{ line_item.variant.unit_price_unit }}
= cache_with_locale "cart-table" do
%table
%tr.product-cart{"ng-repeat" => "line_item in Cart.line_items", "id" => "cart-variant-{{ line_item.variant.id }}"}
%td.image
%img{'ng-src' => '{{ line_item.variant.thumb_url }}'}
%td
%span {{ line_item.variant.extended_name | truncate: max_characters }}
%br
%span.options-text {{ line_item.variant.options_text | truncate: max_characters }}
%td.text-right
%span.quantity {{ line_item.quantity }}
%td
.total-price.text-right {{ line_item.total_price | localizeCurrency }}
.unit-price
%div{:style => "margin-right: 5px"}
%question-mark-with-tooltip{"question-mark-with-tooltip" => "_",
"question-mark-with-tooltip-append-to-body" => "true",
"question-mark-with-tooltip-placement" => "top",
"question-mark-with-tooltip-animation" => true,
key: "'js.shopfront.unit_price_tooltip'",
context: "'cart-sidebar'"}
.options-text
{{ line_item.variant.unit_price_price | localizeCurrency }}&nbsp;/&nbsp;{{ line_item.variant.unit_price_unit }}
.cart-empty{"ng-show" => "Cart.line_items.length == 0"}
%p
@@ -42,15 +44,16 @@
= t('.take_me_shopping')
.sidebar-footer{"ng-show" => "Cart.line_items.length > 0"}
%p.cart-total
%strong
= t 'total'
{{ Cart.total() | localizeCurrency }}
= cache_with_locale "cart-footer" do
%p.cart-total
%strong
= t 'total'
{{ Cart.total() | localizeCurrency }}
%div.fullwidth
%a.edit-cart.button.large.dark.left{href: main_app.cart_path, "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }"}
%div{ ng: { if: "Cart.dirty" } }= t(:cart_updating)
%div{ ng: { if: "!Cart.dirty && Cart.empty()" } }= t(:cart_empty)
%div{ ng: { if: "!Cart.dirty && !Cart.empty()" } }= t('.edit_cart')
%a.checkout.button.large.bright.right{href: main_app.checkout_path, "ng-disabled" => "Cart.dirty || Cart.empty()"}
= t '.checkout'
%div.fullwidth
%a.edit-cart.button.large.dark.left{href: main_app.cart_path, "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }"}
%div{ ng: { if: "Cart.dirty" } }= t(:cart_updating)
%div{ ng: { if: "!Cart.dirty && Cart.empty()" } }= t(:cart_empty)
%div{ ng: { if: "!Cart.dirty && !Cart.empty()" } }= t('.edit_cart')
%a.checkout.button.large.bright.right{href: main_app.checkout_path, "ng-disabled" => "Cart.dirty || Cart.empty()"}
= t '.checkout'

View File

@@ -1,8 +1,9 @@
%li.language-switcher.has-dropdown.not-click
%a{href: '#', class: "top-bar--menu-item-with-icon"}
%i.ofn-i_071-globe
%span= t 'language_name'
%ul.dropdown
- OpenFoodNetwork::I18nConfig.locale_options.each do |l|
%li
= link_to t('language_name', locale: l), main_app.locale_path(l.to_s)
= cache_with_locale OpenFoodNetwork::I18nConfig.locale_options do
%li.language-switcher.has-dropdown.not-click
%a{href: '#', class: "top-bar--menu-item-with-icon"}
%i.ofn-i_071-globe
%span= t 'language_name'
%ul.dropdown
- OpenFoodNetwork::I18nConfig.locale_options.each do |l|
%li
= link_to t('language_name', locale: l), main_app.locale_path(l.to_s)

View File

@@ -1,27 +1,30 @@
%nav.top-bar.show-for-large-up
%section.top-bar-section
%ul.nav-logo
%li.ofn-logo
%a{href: main_logo_link(@white_label_distributor)}
- if @white_label_logo&.variable?
= image_tag @white_label_distributor.white_label_logo_url(:default)
- else
%img{src: ContentConfig.url_for(:logo)}
%li.powered-by
%img{src: '/favicon.ico'}
%span
= t 'powered_by'
%a{href: '/'}
= t 'title'
= cache_with_locale [@white_label_distributor, ContentConfig.cache_key] do
%li.ofn-logo
%a{href: main_logo_link(@white_label_distributor)}
- if @white_label_logo&.variable?
= image_tag @white_label_distributor.white_label_logo_url(:default)
- else
%img{src: ContentConfig.url_for(:logo)}
%li.powered-by
%img{src: '/favicon.ico'}
%span
= t 'powered_by'
%a{href: '/'}
= t 'title'
- unless @hide_ofn_navigation
%ul.nav-main-menu
- [*1..7].each do |menu_number|
- menu_name = "menu_#{menu_number}"
- if ContentConfig[menu_name].present?
%li
%a{href: t("#{menu_name}_url") }
%span.nav-primary
= t "#{menu_name}_title"
= cache_with_locale ContentConfig.cache_key do
%ul.nav-main-menu
- [*1..7].each do |menu_number|
- menu_name = "menu_#{menu_number}"
- if ContentConfig[menu_name].present?
%li
%a{href: t("#{menu_name}_url") }
%span.nav-primary
= t "#{menu_name}_title"
%ul.nav-icons-menu
- if OpenFoodNetwork::I18nConfig.selectable_locales.count > 1
= render 'shared/menu/language_selector'
@@ -31,11 +34,12 @@
- else
= render 'shared/menu/signed_in'
%li.current_hub{"ng-controller" => "CurrentHubCtrl", "ng-show" => "CurrentHub.hub.id", "ng-cloak" => true}
%a{href: main_app.shop_path}
%span{ class: "top-bar--current-hub-prefix" }
= t 'label_shopping'
= '@'
%span{ class: "top-bar--current-hub-name" } {{ CurrentHub.hub.name | truncate:25 }}
%li.cart{"ng-cloak" => true}
= render partial: "shared/menu/cart"
= cache_with_locale "cart" do
%li.current_hub{"ng-controller" => "CurrentHubCtrl", "ng-show" => "CurrentHub.hub.id", "ng-cloak" => true}
%a{href: main_app.shop_path}
%span{ class: "top-bar--current-hub-prefix" }
= t 'label_shopping'
= '@'
%span{ class: "top-bar--current-hub-name" } {{ CurrentHub.hub.name | truncate:25 }}
%li.cart{"ng-cloak" => true}
= render partial: "shared/menu/cart"

View File

@@ -1,25 +1,26 @@
%nav.tab-bar.show-for-medium-down
%section.left
%a.left-off-canvas-toggle.menu-icon
= image_pack_tag "menu/btn-menu-mobile.png"
= cache_with_locale [@white_label_distributor, ContentConfig.cache_key] do
%nav.tab-bar.show-for-medium-down
%section.left
%a.left-off-canvas-toggle.menu-icon
= image_pack_tag "menu/btn-menu-mobile.png"
%section.left
.ofn-logo
%a{href: main_app.root_path}
- if @white_label_logo&.variable?
= image_tag @white_label_distributor.white_label_logo_url(:mobile)
- else
%img{src: ContentConfig.url_for(:logo_mobile), srcset: ContentConfig.url_for(:logo_mobile_svg), width: "75", height: "26"}
%section.left
.ofn-logo
%a{href: main_app.root_path}
- if @white_label_logo&.variable?
= image_tag @white_label_distributor.white_label_logo_url(:mobile)
- else
%img{src: ContentConfig.url_for(:logo_mobile), srcset: ContentConfig.url_for(:logo_mobile_svg), width: "75", height: "26"}
%section.right{"ng-cloak" => true}
%span.cart-span{"ng-class" => "{ dirty: Cart.dirty || Cart.empty(), 'pure-dirty': Cart.dirty }"}
%a.icon{ng: {click: 'toggleCartSidebar()'}}
%span
= t '.cart'
%span.count
= image_pack_tag "menu/icn-cart.svg"
%span
{{ Cart.total_item_count() }}
%section.right{"ng-cloak" => true}
%span.cart-span{"ng-class" => "{ dirty: Cart.dirty || Cart.empty(), 'pure-dirty': Cart.dirty }"}
%a.icon{ng: {click: 'toggleCartSidebar()'}}
%span
= t '.cart'
%span.count
= image_pack_tag "menu/icn-cart.svg"
%span
{{ Cart.total_item_count() }}
%a{href: main_app.shop_path}
{{ CurrentHub.hub.name }}
%a{href: main_app.shop_path}
{{ CurrentHub.hub.name }}

View File

@@ -1,16 +1,18 @@
%aside.left-off-canvas-menu.show-for-medium-down{ ng: { controller: "OffcanvasCtrl" } }
%ul.off-canvas-list
%li.ofn-logo
%a{href: main_app.root_path}
%img{src: ContentConfig.url_for(:logo_mobile), srcset: ContentConfig.url_for(:logo_mobile_svg), width: "75", height: "26"}
- [*1..7].each do |menu_number|
- menu_name = "menu_#{menu_number}"
- if ContentConfig[menu_name].present?
%li.li-menu
%a{href: t("#{menu_name}_url") }
%span.nav-primary
%i{class: ContentConfig["#{menu_name}_icon_name"]}
= t "#{menu_name}_title"
= cache_with_locale ContentConfig.cache_key do
%li.ofn-logo
%a{href: main_app.root_path}
%img{src: ContentConfig.url_for(:logo_mobile), srcset: ContentConfig.url_for(:logo_mobile_svg), width: "75", height: "26"}
- [*1..7].each do |menu_number|
- menu_name = "menu_#{menu_number}"
- if ContentConfig[menu_name].present?
%li.li-menu
%a{href: t("#{menu_name}_url") }
%span.nav-primary
%i{class: ContentConfig["#{menu_name}_icon_name"]}
= t "#{menu_name}_title"
- if OpenFoodNetwork::I18nConfig.selectable_locales.count > 1
%li.language-switcher.li-menu
%a

View File

@@ -1,5 +1,6 @@
%li#login-link{ "data-controller": "login-modal" }
%a{"auth": "login", "data-action": "click->login-modal#call" }
%img{ src: image_pack_path("menu/icn-login.svg") }
%span
= t 'label_login'
= cache_with_locale do
%li#login-link{ "data-controller": "login-modal" }
%a{"auth": "login", "data-action": "click->login-modal#call" }
%img{ src: image_pack_path("menu/icn-login.svg") }
%span
= t 'label_login'

View File

@@ -1,9 +1,10 @@
%span{ "ng-show" => "query && ( appliedPropertiesList() || appliedTaxonsList() )" }
= t :products_filters_in
= cache_with_locale do
%span{ "ng-show" => "query && ( appliedPropertiesList() || appliedTaxonsList() )" }
= t :products_filters_in
%span.applied-properties{'ng-bind-html' => 'appliedPropertiesList()'}
%span.applied-properties{'ng-bind-html' => 'appliedPropertiesList()'}
%span{ "ng-show" => "appliedPropertiesList() && appliedTaxonsList()" }
= t :products_and
%span{ "ng-show" => "appliedPropertiesList() && appliedTaxonsList()" }
= t :products_and
%span.applied-taxons{'ng-bind-html' => 'appliedTaxonsList()'}
%span.applied-taxons{'ng-bind-html' => 'appliedTaxonsList()'}

View File

@@ -1,5 +1,6 @@
.filter-shopfront.taxon-selectors{ng: {show: 'supplied_taxons != null'}}
%filter-selector{ 'selector-set' => "taxonSelectors", objects: "supplied_taxons", "active-selectors" => "activeTaxons"}
= cache_with_locale do
.filter-shopfront.taxon-selectors{ng: {show: 'supplied_taxons != null'}}
%filter-selector{ 'selector-set' => "taxonSelectors", objects: "supplied_taxons", "active-selectors" => "activeTaxons"}
.filter-shopfront.property-selectors{ng: {show: 'supplied_properties != null'}}
%filter-selector{ 'selector-set' => "propertySelectors", objects: "supplied_properties", "active-selectors" => "activeProperties"}
.filter-shopfront.property-selectors{ng: {show: 'supplied_properties != null'}}
%filter-selector{ 'selector-set' => "propertySelectors", objects: "supplied_properties", "active-selectors" => "activeProperties"}

View File

@@ -1,51 +1,52 @@
%form{action: main_app.cart_path}
%products{"ng-init" => "refreshStaleData()", "ng-show" => "order_cycle.order_cycle_id != null", "ng-cloak" => true }
= cache_with_locale do
%form{action: main_app.cart_path}
%products{"ng-init" => "refreshStaleData()", "ng-show" => "order_cycle.order_cycle_id != null", "ng-cloak" => true }
= render partial: "shop/products/searchbar"
= render partial: "shop/products/searchbar"
.row
.footer-pad.small-12.columns.product-listing
.row.full
.medium-12.large-9.columns.full
= render partial: "shop/products/search_feedback"
.row
.footer-pad.small-12.columns.product-listing
.row.full
.medium-12.large-9.columns.full
= render partial: "shop/products/search_feedback"
%div.pad-top{ "infinite-scroll" => "loadMore()", "infinite-scroll-distance" => "1", "infinite-scroll-disabled" => 'Products.loading', "infinite-scroll-immediate-check": "false" }
%product.animate-repeat{"ng-controller" => "ProductNodeCtrl", "ng-repeat" => "product in Products.products track by product.id", "id" => "product-{{ product.id }}"}
= render "shop/products/summary"
.shop-variants
.variants.row{"ng-controller": "ShopVariantCtrl", variant: 'variant', "ng-repeat" => "variant in product.variants | orderBy: ['name_to_display','unit_value'] track by variant.id", "id" => "variant-{{ variant.id }}", "ng-class" => "{'out-of-stock': !variant.on_demand && variant.on_hand == 0}"}
= render "shop/products/shop_variant"
%product{"ng-show" => "Products.loading"}
.summary
.small-12.columns.text-center
= t :products_loading
.row.full
.small-12.columns.text-center
= render partial: "components/spinner"
%div.pad-top{ "infinite-scroll" => "loadMore()", "infinite-scroll-distance" => "1", "infinite-scroll-disabled" => 'Products.loading', "infinite-scroll-immediate-check": "false" }
%product.animate-repeat{"ng-controller" => "ProductNodeCtrl", "ng-repeat" => "product in Products.products track by product.id", "id" => "product-{{ product.id }}"}
= render "shop/products/summary"
.shop-variants
.variants.row{"ng-controller": "ShopVariantCtrl", variant: 'variant', "ng-repeat" => "variant in product.variants | orderBy: ['name_to_display','unit_value'] track by variant.id", "id" => "variant-{{ variant.id }}", "ng-class" => "{'out-of-stock': !variant.on_demand && variant.on_hand == 0}"}
= render "shop/products/shop_variant"
%product{"ng-show" => "Products.loading"}
.summary
.small-12.columns.text-center
= t :products_loading
.row.full
.small-12.columns.text-center
= render partial: "components/spinner"
.hide-for-medium-down.large-1.columns
-# Space between products and filters
&nbsp;
.hide-for-medium-down.large-1.columns
-# Space between products and filters
&nbsp;
.sticky-shop-filters-container.thin-scroll-bar.hide-for-medium-down.large-2.columns
%h5.filter-header
= t(:products_filter_by)
%span{ng: {show: 'filtersCount()' }}
= "({{ filtersCount() }} #{t(:products_filter_selected)})"
= render partial: "shop/products/filters"
.expanding-sidebar.shop-filters-sidebar.hide-for-large-up{ng: {show: 'showFilterSidebar', class: "{'shown': showFilterSidebar}"}}
.background{ng: {click: 'toggleFilterSidebar()'}}
.sidebar
%h5
.sticky-shop-filters-container.thin-scroll-bar.hide-for-medium-down.large-2.columns
%h5.filter-header
= t(:products_filter_by)
%span{ng: {show: 'filtersCount()' }}
= "({{ filtersCount() }} #{t(:products_filter_selected)})"
= render partial: "shop/products/filters"
.sidebar-footer
%button.large.dark.left{type: 'button', ng: {click: 'clearFilters()'}}
= t(:products_filter_clear)
%button.large.bright.right{type: 'button', ng: {click: 'toggleFilterSidebar()'}}
= t(:products_filter_done)
.expanding-sidebar.shop-filters-sidebar.hide-for-large-up{ng: {show: 'showFilterSidebar', class: "{'shown': showFilterSidebar}"}}
.background{ng: {click: 'toggleFilterSidebar()'}}
.sidebar
%h5
= t(:products_filter_by)
%span{ng: {show: 'filtersCount()' }}
= "({{ filtersCount() }} #{t(:products_filter_selected)})"
= render partial: "shop/products/filters"
.sidebar-footer
%button.large.dark.left{type: 'button', ng: {click: 'clearFilters()'}}
= t(:products_filter_clear)
%button.large.bright.right{type: 'button', ng: {click: 'toggleFilterSidebar()'}}
= t(:products_filter_done)

View File

@@ -1,24 +1,25 @@
.row.animate-slide{ "ng-show" => "query || appliedPropertiesList() || appliedTaxonsList()" }
.small-12.columns
.alert-box.search-alert.ng-scope
%div{"ng-show" => "Products.products.length > 0"}
= cache_with_locale do
.row.animate-slide{ "ng-show" => "query || appliedPropertiesList() || appliedTaxonsList()" }
.small-12.columns
.alert-box.search-alert.ng-scope
%div{"ng-show" => "Products.products.length > 0"}
%a.clear-all.right{"ng-click" => "clearAll()"}
= t :products_clear
%i.ofn-i_009-close
%a.clear-all.right{"ng-click" => "clearAll()"}
= t :products_clear
%i.ofn-i_009-close
%span.filter-label
= t :products_results_for
%span{ ng: { hide: "!query"} }
%span.applied-search
{{ query }}
= render partial: 'shop/products/applied_filters_feedback'
%span.filter-label
= t :products_results_for
%span{ ng: { hide: "!query"} }
%span.applied-search
{{ query }}
= render partial: 'shop/products/applied_filters_feedback'
%div.no-results-bar{"ng-show" => "Products.products.length == 0 && !Products.loading"}
.row.summary
.small-12.columns
%p.no-results
= t :products_no_results_html, query: "<span class='applied-search'>{{query}}</span>".html_safe
= render partial: 'shop/products/applied_filters_feedback'
%button.clear-search{type: 'button', ng: {click: 'clearAll()'}}
= t :products_clear_search
%div.no-results-bar{"ng-show" => "Products.products.length == 0 && !Products.loading"}
.row.summary
.small-12.columns
%p.no-results
= t :products_no_results_html, query: "<span class='applied-search'>{{query}}</span>".html_safe
= render partial: 'shop/products/applied_filters_feedback'
%button.clear-search{type: 'button', ng: {click: 'clearAll()'}}
= t :products_clear_search

View File

@@ -1,17 +1,18 @@
.shop-searchbar
.row
.small-12.large-5.columns.flex
%div.search-wrap
%input#search.text{"ng-model" => "query",
type: 'search',
placeholder: t(:products_search),
"ng-debounce" => "200",
"disable-enter-with-blur" => true}
%a.clear{type: 'button', ng: {show: 'query', click: 'clearQuery()'}, 'focus-search' => true}
= image_pack_tag "icn-close.png"
= cache_with_locale do
.shop-searchbar
.row
.small-12.large-5.columns.flex
%div.search-wrap
%input#search.text{"ng-model" => "query",
type: 'search',
placeholder: t(:products_search),
"ng-debounce" => "200",
"disable-enter-with-blur" => true}
%a.clear{type: 'button', ng: {show: 'query', click: 'clearQuery()'}, 'focus-search' => true}
= image_pack_tag "icn-close.png"
.hide-for-large-up
%button{type: 'button', ng: {click: 'toggleFilterSidebar()'}}
= t(:products_filter_heading)
%span{ng: {show: 'filtersCount()' }}
({{ filtersCount() }})
.hide-for-large-up
%button{type: 'button', ng: {click: 'toggleFilterSidebar()'}}
= t(:products_filter_heading)
%span{ng: {show: 'filtersCount()' }}
({{ filtersCount() }})

View File

@@ -1,22 +1,23 @@
.small-4.medium-4.large-5.columns.variant-name
.inline{"ng-if" => "::variant.display_name"} {{ ::variant.display_name }}
.variant-unit {{ ::variant.unit_to_display }}
.small-3.medium-3.large-2.columns.variant-price
%price-breakdown{"price-breakdown" => "_", variant: "variant",
"price-breakdown-append-to-body" => "true",
"price-breakdown-placement" => "bottom",
"price-breakdown-animation" => true}
{{ variant.price_with_fees | localizeCurrency }}
.unit-price.variant-unit-price
%question-mark-with-tooltip{"question-mark-with-tooltip" => "_",
"question-mark-with-tooltip-append-to-body" => "true",
"question-mark-with-tooltip-placement" => "top",
"question-mark-with-tooltip-animation" => true,
key: "'js.shopfront.unit_price_tooltip'"}
{{ variant.unit_price_price | localizeCurrency }}&nbsp;/&nbsp;{{ variant.unit_price_unit }}
= cache_with_locale do
.small-4.medium-4.large-5.columns.variant-name
.inline{"ng-if" => "::variant.display_name"} {{ ::variant.display_name }}
.variant-unit {{ ::variant.unit_to_display }}
.small-3.medium-3.large-2.columns.variant-price
%price-breakdown{"price-breakdown" => "_", variant: "variant",
"price-breakdown-append-to-body" => "true",
"price-breakdown-placement" => "bottom",
"price-breakdown-animation" => true}
{{ variant.price_with_fees | localizeCurrency }}
.unit-price.variant-unit-price
%question-mark-with-tooltip{"question-mark-with-tooltip" => "_",
"question-mark-with-tooltip-append-to-body" => "true",
"question-mark-with-tooltip-placement" => "top",
"question-mark-with-tooltip-animation" => true,
key: "'js.shopfront.unit_price_tooltip'"}
{{ variant.unit_price_price | localizeCurrency }}&nbsp;/&nbsp;{{ variant.unit_price_unit }}
.medium-2.large-2.columns.total-price
%span{"ng-class" => "{filled: variant.line_item.total_price}"}
{{ variant.line_item.total_price | localizeCurrency }}
= render partial: "shop/products/shop_variant_no_group_buy"
= render partial: "shop/products/shop_variant_with_group_buy"
.medium-2.large-2.columns.total-price
%span{"ng-class" => "{filled: variant.line_item.total_price}"}
{{ variant.line_item.total_price | localizeCurrency }}
= render partial: "shop/products/shop_variant_no_group_buy"
= render partial: "shop/products/shop_variant_with_group_buy"

View File

@@ -1,22 +1,23 @@
.small-5.medium-3.large-3.columns.variant-quantity-column.text-right{"ng-if" => "::!variant.product.group_buy"}
= cache_with_locale do
.small-5.medium-3.large-3.columns.variant-quantity-column.text-right{"ng-if" => "::!variant.product.group_buy"}
.variant-quantity-inputs{ng: {if: "variant.line_item.quantity == 0"}}
%button.add-variant{type: "button", ng: {click: "add(1)", disabled: "!canAdd(1)"}}
{{ "js.shopfront.variant.add_to_cart" | t }}
.variant-quantity-inputs{ng: {if: "variant.line_item.quantity == 0"}}
%button.add-variant{type: "button", ng: {click: "add(1)", disabled: "!canAdd(1)"}}
{{ "js.shopfront.variant.add_to_cart" | t }}
.variant-quantity-inputs{ng: {if: "variant.line_item.quantity != 0"}}
%button.variant-quantity{type: "button", ng: {click: "add(-1)", disabled: "!canAdd(-1)"}}>
-# U+FF0D Fullwidth Hyphen-Minus
%input.variant-quantity{ type: "number", min: "0", max: "{{ available() }}",
ng: {model: "variant.line_item.quantity", max: "Infinity"}}>
%button.variant-quantity{type: "button", ng: {click: "add(1)", disabled: "!canAdd(1)"}}
-# U+FF0B Fullwidth Plus Sign
.variant-remaining-stock{ng: {if: "displayRemainingInStock()"}}
{{ "js.shopfront.variant.remaining_in_stock" | t:{quantity: available()} }}
.variant-quantity-display{ng: {class: "{visible: variant.line_item.quantity}"}}
{{ "js.shopfront.variant.quantity_in_cart" | t:{quantity: variant.line_item.quantity || 0} }}
%input{type: :hidden,
name: "variants[{{::variant.id}}]",
ng: {model: "variant.line_item.quantity"}}
.variant-quantity-inputs{ng: {if: "variant.line_item.quantity != 0"}}
%button.variant-quantity{type: "button", ng: {click: "add(-1)", disabled: "!canAdd(-1)"}}>
-# U+FF0D Fullwidth Hyphen-Minus
%input.variant-quantity{ type: "number", min: "0", max: "{{ available() }}",
ng: {model: "variant.line_item.quantity", max: "Infinity"}}>
%button.variant-quantity{type: "button", ng: {click: "add(1)", disabled: "!canAdd(1)"}}
-# U+FF0B Fullwidth Plus Sign
.variant-remaining-stock{ng: {if: "displayRemainingInStock()"}}
{{ "js.shopfront.variant.remaining_in_stock" | t:{quantity: available()} }}
.variant-quantity-display{ng: {class: "{visible: variant.line_item.quantity}"}}
{{ "js.shopfront.variant.quantity_in_cart" | t:{quantity: variant.line_item.quantity || 0} }}
%input{type: :hidden,
name: "variants[{{::variant.id}}]",
ng: {model: "variant.line_item.quantity"}}

View File

@@ -1,17 +1,18 @@
.small-5.medium-3.large-3.columns.variant-quantity-column.text-right{"ng-if" => "::variant.product.group_buy"}
= cache_with_locale do
.small-5.medium-3.large-3.columns.variant-quantity-column.text-right{"ng-if" => "::variant.product.group_buy"}
%button.add-variant{type: "button", ng: {if: "!variant.line_item.quantity", click: "addBulk(1)", disabled: "!canAdd(1)"}}
{{ "js.shopfront.variant.add_to_cart" | t }}
%button.bulk-buy.variant-quantity{type: "button", ng: {if: "variant.line_item.quantity", click: "addBulk(0)"}}>
{{ variant.line_item.quantity }}
%button.bulk-buy.variant-quantity{type: "button", ng: {if: "variant.line_item.quantity", click: "addBulk(0)"}}
{{ variant.line_item.max_quantity || "-" }}
%br
.variant-quantity-display{ng: {class: "{visible: variant.line_item.quantity}"}}
{{ "js.shopfront.variant.in_cart" | t }}
%input{type: :hidden,
name: "variants[{{::variant.id}}]",
ng: {model: "variant.line_item.quantity"}}
%input{type: :hidden,
name: "variants[{{::variant.id}}]",
ng: {model: "variant.line_item.max_quantity"}}
%button.add-variant{type: "button", ng: {if: "!variant.line_item.quantity", click: "addBulk(1)", disabled: "!canAdd(1)"}}
{{ "js.shopfront.variant.add_to_cart" | t }}
%button.bulk-buy.variant-quantity{type: "button", ng: {if: "variant.line_item.quantity", click: "addBulk(0)"}}>
{{ variant.line_item.quantity }}
%button.bulk-buy.variant-quantity{type: "button", ng: {if: "variant.line_item.quantity", click: "addBulk(0)"}}
{{ variant.line_item.max_quantity || "-" }}
%br
.variant-quantity-display{ng: {class: "{visible: variant.line_item.quantity}"}}
{{ "js.shopfront.variant.in_cart" | t }}
%input{type: :hidden,
name: "variants[{{::variant.id}}]",
ng: {model: "variant.line_item.quantity"}}
%input{type: :hidden,
name: "variants[{{::variant.id}}]",
ng: {model: "variant.line_item.max_quantity"}}

View File

@@ -1,21 +1,22 @@
.product-thumb
%a{"ng-click" => "triggerProductModal()"}
%span.product-thumb__bulk-label{"ng-if" => "::product.group_buy"}
= t(".bulk")
%img{"ng-src" => "{{::product.primaryImageOrMissing}}"}
= cache_with_locale do
.product-thumb
%a{"ng-click" => "triggerProductModal()"}
%span.product-thumb__bulk-label{"ng-if" => "::product.group_buy"}
= t(".bulk")
%img{"ng-src" => "{{::product.primaryImageOrMissing}}"}
.summary
.summary-header
%h3
%a{"ng-click" => "triggerProductModal()", href: 'javascript:void(0)'}
%span{"ng-bind" => "::product.name"}
.product-description{ng: {"bind-html": "::product.description_html", click: "triggerProductModal()", show: "product.description_html.length"}}
%div{ "ng-switch" => "enterprise.visible" }
.product-producer
= t :products_from
%span{ "ng-switch-when": "hidden", "ng-bind" => "::enterprise.name"}
%span{ "ng-switch-default": true }
%enterprise-modal{"ng-bind" => "::enterprise.name"}
.summary
.summary-header
%h3
%a{"ng-click" => "triggerProductModal()", href: 'javascript:void(0)'}
%span{"ng-bind" => "::product.name"}
.product-description{ng: {"bind-html": "::product.description_html", click: "triggerProductModal()", show: "product.description_html.length"}}
%div{ "ng-switch" => "enterprise.visible" }
.product-producer
= t :products_from
%span{ "ng-switch-when": "hidden", "ng-bind" => "::enterprise.name"}
%span{ "ng-switch-default": true }
%enterprise-modal{"ng-bind" => "::enterprise.name"}
.product-properties.filter-shopfront.property-selectors
%filter-selector{ 'selector-set' => "productPropertySelectors", objects: "[product] | propertiesWithValuesOf" }
.product-properties.filter-shopfront.property-selectors
%filter-selector{ 'selector-set' => "productPropertySelectors", objects: "[product] | propertiesWithValuesOf" }

View File

@@ -1,53 +1,54 @@
.row.active_table_row{"ng-show" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : open()}"}
.columns.small-12.fat.text-center{"ng-show" => "open() && shopfront_loading"}
%p.fullwidth
= render partial: "components/spinner"
= cache_with_locale do
.row.active_table_row{"ng-show" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : open()}"}
.columns.small-12.fat.text-center{"ng-show" => "open() && shopfront_loading"}
%p.fullwidth
= render partial: "components/spinner"
.columns.small-12.medium-6.large-5.fat{"ng-show" => "open() && !shopfront_loading"}
%div{"ng-if" => "::hub.taxons"}
%label
= t :hubs_buy
.trans-sentence
%div
%span.fat-taxons{"ng-repeat" => "taxon in hub.taxons"}
%span{"ng-bind" => "::taxon.name"}
%div
%span.fat-properties{"ng-repeat" => "property in hub.distributed_properties"}
%span{"ng-bind" => "property.presentation"}
%div.show-for-medium-up{"ng-if" => "::hub.taxons.length==0"}
&nbsp;
.columns.small-12.medium-3.large-2.fat{"ng-show" => "open() && !shopfront_loading"}
%div{"ng-if" => "::(hub.pickup || hub.delivery)"}
%label
= t :hubs_delivery_options
%ul.small-block-grid-2.medium-block-grid-1.large-block-grid-1
%li.pickup{"ng-if" => "::hub.pickup"}
%i.ofn-i_038-takeaway
= t :hubs_pickup
%li.delivery{"ng-if" => "::hub.delivery"}
%i.ofn-i_039-delivery
= t :hubs_delivery
.columns.small-12.medium-3.large-5.fat{"ng-show" => "open() && !shopfront_loading"}
%div{"ng-if" => "::hub.producers"}
%label
= t :hubs_producers
%ul.small-block-grid-2.medium-block-grid-1.large-block-grid-2{"ng-class" => "{'show-more-producers' : toggleMoreProducers}", "class" => "producers-list"}
%li{"ng-repeat" => "enterprise in hub.producers | limitTo:7"}
%enterprise-modal
%i.ofn-i_036-producers
%span{"ng-bind" => "::enterprise.name"}
%li{"ng-repeat" => "enterprise in hub.producers.slice(7,hub.producers.length)", "class" => "additional-producer"}
%enterprise-modal
%i.ofn-i_036-producers
%span{"ng-bind" => "::enterprise.name"}
%li{"data-is-link" => "true", "class" => "more-producers-link", "ng-show" => "::hub.producers.length>7"}
%a{"ng-click" => "toggleMoreProducers=!toggleMoreProducers; $event.stopPropagation()"}
.more
+
%span{"ng-bind" => "::hub.producers.length-7"}
= t :label_more
.less
= t :label_less
.columns.small-12.medium-6.large-5.fat{"ng-show" => "open() && !shopfront_loading"}
%div{"ng-if" => "::hub.taxons"}
%label
= t :hubs_buy
.trans-sentence
%div
%span.fat-taxons{"ng-repeat" => "taxon in hub.taxons"}
%span{"ng-bind" => "::taxon.name"}
%div
%span.fat-properties{"ng-repeat" => "property in hub.distributed_properties"}
%span{"ng-bind" => "property.presentation"}
%div.show-for-medium-up{"ng-if" => "::hub.taxons.length==0"}
&nbsp;
.columns.small-12.medium-3.large-2.fat{"ng-show" => "open() && !shopfront_loading"}
%div{"ng-if" => "::(hub.pickup || hub.delivery)"}
%label
= t :hubs_delivery_options
%ul.small-block-grid-2.medium-block-grid-1.large-block-grid-1
%li.pickup{"ng-if" => "::hub.pickup"}
%i.ofn-i_038-takeaway
= t :hubs_pickup
%li.delivery{"ng-if" => "::hub.delivery"}
%i.ofn-i_039-delivery
= t :hubs_delivery
.columns.small-12.medium-3.large-5.fat{"ng-show" => "open() && !shopfront_loading"}
%div{"ng-if" => "::hub.producers"}
%label
= t :hubs_producers
%ul.small-block-grid-2.medium-block-grid-1.large-block-grid-2{"ng-class" => "{'show-more-producers' : toggleMoreProducers}", "class" => "producers-list"}
%li{"ng-repeat" => "enterprise in hub.producers | limitTo:7"}
%enterprise-modal
%i.ofn-i_036-producers
%span{"ng-bind" => "::enterprise.name"}
%li{"ng-repeat" => "enterprise in hub.producers.slice(7,hub.producers.length)", "class" => "additional-producer"}
%enterprise-modal
%i.ofn-i_036-producers
%span{"ng-bind" => "::enterprise.name"}
%li{"data-is-link" => "true", "class" => "more-producers-link", "ng-show" => "::hub.producers.length>7"}
%a{"ng-click" => "toggleMoreProducers=!toggleMoreProducers; $event.stopPropagation()"}
.more
+
%span{"ng-bind" => "::hub.producers.length-7"}
= t :label_more
.less
= t :label_less
%div.show-for-medium-up{"ng-if" => "::hub.producers.length==0"}
&nbsp;
%div.show-for-medium-up{"ng-if" => "::hub.producers.length==0"}
&nbsp;

View File

@@ -7,31 +7,32 @@
= render "shared/components/enterprise_search"
= render "filters"
.row
.small-12.columns
.name-matches{"ng-show" => "nameMatchesFiltered.length > 0"}
%h2
= t :hubs_matches
= render "hubs_table", enterprises: "nameMatches"
= cache_with_locale do
.row
.small-12.columns
.name-matches{"ng-show" => "nameMatchesFiltered.length > 0"}
%h2
= t :hubs_matches
= render "hubs_table", enterprises: "nameMatches"
.distance-matches{"ng-if" => "nameMatchesFiltered.length == 0 || distanceMatchesShown"}
%h2{"ng-show" => "nameMatchesFiltered.length > 0 || query.length > 0"}
= t :hubs_matches
%span{"ng-show" => "nameMatchesFiltered.length > 0"} {{ nameMatchesFiltered[0].name }}...
%span{"ng-hide" => "nameMatchesFiltered.length > 0"} {{ query }}...
.distance-matches{"ng-if" => "nameMatchesFiltered.length == 0 || distanceMatchesShown"}
%h2{"ng-show" => "nameMatchesFiltered.length > 0 || query.length > 0"}
= t :hubs_matches
%span{"ng-show" => "nameMatchesFiltered.length > 0"} {{ nameMatchesFiltered[0].name }}...
%span{"ng-hide" => "nameMatchesFiltered.length > 0"} {{ query }}...
= render "hubs_table", enterprises: "distanceMatches"
= render "hubs_table", enterprises: "distanceMatches"
.show-distance-matches{"ng-show" => "nameMatchesFiltered.length > 0 && !distanceMatchesShown"}
%a{href: "", "ng-click" => "showDistanceMatches()"}
= t :hubs_distance_filter, location: "{{ nameMatchesFiltered[0].name }}"
.more-controls
%span{ng: {show: "closed_shops_loading", cloak: true}}
= render partial: "components/spinner"
%span{ng: {if: "!show_closed", cloak: true}}
%a.button{href: "", ng: {click: "showClosedShops()"}}
= t '.show_closed_shops'
%span{ng: {if: "show_closed", cloak: true}}
%a.button{href: "", ng: {click: "hideClosedShops()"}}
= t '.hide_closed_shops'
%a.button{href: main_app.map_path}= t '.show_on_map'
.show-distance-matches{"ng-show" => "nameMatchesFiltered.length > 0 && !distanceMatchesShown"}
%a{href: "", "ng-click" => "showDistanceMatches()"}
= t :hubs_distance_filter, location: "{{ nameMatchesFiltered[0].name }}"
.more-controls
%span{ng: {show: "closed_shops_loading", cloak: true}}
= render partial: "components/spinner"
%span{ng: {if: "!show_closed", cloak: true}}
%a.button{href: "", ng: {click: "showClosedShops()"}}
= t '.show_closed_shops'
%span{ng: {if: "show_closed", cloak: true}}
%a.button{href: "", ng: {click: "hideClosedShops()"}}
= t '.hide_closed_shops'
%a.button{href: main_app.map_path}= t '.show_on_map'

View File

@@ -1,10 +1,11 @@
.active_table
%hub.active_table_node.row{"ng-repeat" => "hub in #{enterprises}Filtered = (#{enterprises} | closedShops:show_closed | taxons:activeTaxons | properties:activeProperties:'distributed_properties' | shipping:shippingTypes | orderBy:['-active', '+distance', '+orders_close_at'])",
"ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}",
"ng-controller" => "HubNodeCtrl",
id: "{{hub.hash}}"}
.small-12.columns
= render 'skinny'
= render 'fat'
= cache_with_locale enterprises do
.active_table
%hub.active_table_node.row{"ng-repeat" => "hub in #{enterprises}Filtered = (#{enterprises} | closedShops:show_closed | taxons:activeTaxons | properties:activeProperties:'distributed_properties' | shipping:shippingTypes | orderBy:['-active', '+distance', '+orders_close_at'])",
"ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}",
"ng-controller" => "HubNodeCtrl",
id: "{{hub.hash}}"}
.small-12.columns
= render 'skinny'
= render 'fat'
= render 'shared/components/enterprise_no_results', enterprises: "#{enterprises}Filtered"
= render 'shared/components/enterprise_no_results', enterprises: "#{enterprises}Filtered"

View File

@@ -1,46 +1,47 @@
.row.active_table_row{"ng-if" => "hub.is_distributor", "ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}"}
.columns.small-12.medium-5.large-5.skinny-head
%a.hub{"ng-href" => "{{::hub.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub", "data-is-link" => "true"}
%i{ng: {class: "::hub.icon_font"}}
%span.margin-top.hub-name-listing{"ng-bind" => "::hub.name | truncate:40"}
= cache_with_locale do
.row.active_table_row{"ng-if" => "hub.is_distributor", "ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}"}
.columns.small-12.medium-5.large-5.skinny-head
%a.hub{"ng-href" => "{{::hub.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub", "data-is-link" => "true"}
%i{ng: {class: "::hub.icon_font"}}
%span.margin-top.hub-name-listing{"ng-bind" => "::hub.name | truncate:40"}
.columns.small-4.medium-2.large-2
%span.margin-top.ellipsed{"ng-bind" => "::hub.address.city"}
.columns.small-3.medium-2.large-2
%span.margin-top.ellipsed{"ng-bind" => "::hub.address.state_name"}
%span.margin-top{"ng-if" => "hub.distance != null && hub.distance > 0"} ({{ hub.distance / 1000 | number:0 }} km)
.columns.small-4.medium-2.large-2
%span.margin-top.ellipsed{"ng-bind" => "::hub.address.city"}
.columns.small-3.medium-2.large-2
%span.margin-top.ellipsed{"ng-bind" => "::hub.address.state_name"}
%span.margin-top{"ng-if" => "hub.distance != null && hub.distance > 0"} ({{ hub.distance / 1000 | number:0 }} km)
.columns.small-5.medium-3.large-3.text-right.no-wrap.flex.flex-align-center.flex-justify-end{"ng-if" => "::hub.active"}
%a.hub.open_closed.flex.flex-align-center{"ng-href" => "{{::hub.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"}
%span{ ng: { if: "::current()" } }
%em= t :hubs_shopping_here
.columns.small-5.medium-3.large-3.text-right.no-wrap.flex.flex-align-center.flex-justify-end{"ng-if" => "::hub.active"}
%a.hub.open_closed.flex.flex-align-center{"ng-href" => "{{::hub.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"}
%span{ ng: { if: "::current()" } }
%em= t :hubs_shopping_here
%span{ ng: { if: "::!current()" } }
%span{"ng-bind" => "::hub.orders_close_at | sensible_timeframe"}
%i.ofn-i_068-shop-reversed.show-for-medium-up
%span{style: "margin-left: 0.5rem;"}
%i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"}
.columns.small-5.medium-3.large-3.text-right.no-wrap.flex.flex-align-center.flex-justify-end{"ng-if" => "::!hub.active"}
%a.hub.open_closed.flex{"ng-href" => "{{::hub.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"}
%span{ ng: { if: "::current()" } }
%em= t :hubs_shopping_here
%span{ ng: { if: "::!current()" } }
= t :hubs_orders_closed
%i.ofn-i_068-shop-reversed.show-for-medium-up
%span{style: "margin-left: 0.5rem;"}
%i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"}
.row.active_table_row{"ng-if" => "!hub.is_distributor", "ng-class" => "closed"}
.columns.small-12.medium-6.large-5.skinny-head
%a.hub{"ng-click" => "openModal(hub)", "ng-class" => "{primary: hub.active, secondary: !hub.active}"}
%i{ng: {class: "hub.icon_font"}}
%span.hub-name-listing{"ng-bind" => "::hub.name | truncate:40"}
.columns.small-4.medium-2.large-2
%span.ellipsed{"ng-bind" => "::hub.address.city"}
.columns.small-2.medium-1.large-1
%span.ellipsed{"ng-bind" => "::hub.address.state_name"}
.columns.small-6.medium-3.large-4.text-right.no-wrap.flex.flex-align-center.flex-justify-end
%span{ ng: { if: "::!current()" } }
%span{"ng-bind" => "::hub.orders_close_at | sensible_timeframe"}
%i.ofn-i_068-shop-reversed.show-for-medium-up
%span{style: "margin-left: 0.5rem;"}
%i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"}
.columns.small-5.medium-3.large-3.text-right.no-wrap.flex.flex-align-center.flex-justify-end{"ng-if" => "::!hub.active"}
%a.hub.open_closed.flex{"ng-href" => "{{::hub.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"}
%span{ ng: { if: "::current()" } }
%em= t :hubs_shopping_here
%span{ ng: { if: "::!current()" } }
= t :hubs_orders_closed
%i.ofn-i_068-shop-reversed.show-for-medium-up
%span{style: "margin-left: 0.5rem;"}
%i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"}
.row.active_table_row{"ng-if" => "!hub.is_distributor", "ng-class" => "closed"}
.columns.small-12.medium-6.large-5.skinny-head
%a.hub{"ng-click" => "openModal(hub)", "ng-class" => "{primary: hub.active, secondary: !hub.active}"}
%i{ng: {class: "hub.icon_font"}}
%span.hub-name-listing{"ng-bind" => "::hub.name | truncate:40"}
.columns.small-4.medium-2.large-2
%span.ellipsed{"ng-bind" => "::hub.address.city"}
.columns.small-2.medium-1.large-1
%span.ellipsed{"ng-bind" => "::hub.address.state_name"}
.columns.small-6.medium-3.large-4.text-right.no-wrap.flex.flex-align-center.flex-justify-end
%span{ ng: { if: "::!current()" } }
%em= t :hubs_profile_only
%em= t :hubs_profile_only

View File

@@ -23,6 +23,7 @@ end
require_relative "../lib/open_food_network/i18n_config"
require_relative '../lib/spree/core/environment'
require_relative '../lib/spree/core/mail_interceptor'
require_relative "../lib/i18n_digests"
if defined?(Bundler)
# If you precompile assets before deploying to production, use this line
@@ -184,6 +185,9 @@ module Openfoodnetwork
config.i18n.available_locales = OpenFoodNetwork::I18nConfig.available_locales
I18n.locale = config.i18n.locale = config.i18n.default_locale
# Calculate digests for locale files so we can know when they change
I18nDigests.build_digests config.i18n.available_locales
# Setting this to true causes a performance regression in Rails 3.2.17
# When we're on a version with the fix below, we can set it to true
# https://github.com/svenfuchs/i18n/issues/230

25
lib/i18n_digests.rb Normal file
View File

@@ -0,0 +1,25 @@
# frozen_string_literal: true
class I18nDigests
class << self
def build_digests(available_locales)
available_locales.each do |locale|
i18n_digests[locale.to_sym] = locale_file_digest(locale)
end
end
def for_locale(locale)
i18n_digests[locale.to_sym]
end
private
def i18n_digests
Rails.application.config.x.i18n_digests
end
def locale_file_digest(locale)
Digest::MD5.hexdigest(Rails.root.join("config/locales/#{locale}.yml").read)
end
end
end

View File

@@ -42,4 +42,51 @@ describe ApplicationHelper, type: :helper do
end
end
end
describe "#cache_with_locale" do
let(:available_locales) { ["en", "es"] }
let(:current_locale) { "es" }
let(:locale_digest) { "8a7s5dfy28u0as9du" }
let(:options) { { expires_in: 10.seconds } }
before do
allow(I18n).to receive(:available_locales) { available_locales }
allow(I18n).to receive(:locale) { current_locale }
allow(I18nDigests).to receive(:for_locale) { locale_digest }
end
it "passes key, options, and block to #cache method with locale and locale digest appended" do
expect(helper).to receive(:cache_key_with_locale).
with("test-key", current_locale).and_return(["test-key", current_locale, locale_digest])
expect(helper).to receive(:cache).
with(["test-key", current_locale, locale_digest], options) do |&block|
expect(block.call).to eq("cached content")
end
helper.cache_with_locale "test-key", options do
"cached content"
end
end
end
describe "#cache_key_with_locale" do
let(:en_digest) { "asd689asy0239" }
let(:es_digest) { "9d8tu23oirhad" }
before { allow(I18nDigests).to receive(:for_locale).with("en") { en_digest } }
before { allow(I18nDigests).to receive(:for_locale).with("es") { es_digest } }
it "appends locale and digest to a single key" do
expect(
helper.cache_key_with_locale("single-key", "en")
).to eq(["single-key", "en", en_digest])
end
it "appends locale and digest to multiple keys" do
expect(
helper.cache_key_with_locale(["array", "of", "keys"], "es")
).to eq(["array", "of", "keys", "es", es_digest])
end
end
end

View File

@@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'spec_helper'
describe I18nDigests do
describe "#build_digests" do
let(:available_locales) { ["en", "es"] }
let(:md5_hex_regex) { /([a-f0-9]){10}/ }
around do |example|
original = Rails.application.config.x.i18n_digests
example.run
Rails.application.config.x.i18n_digests = original
end
it "computes and stores digests for each locale file" do
Rails.application.config.x.i18n_digests = {}
I18nDigests.build_digests(available_locales)
expect(Rails.application.config.x.i18n_digests.keys).to eq [:en, :es]
expect(Rails.application.config.x.i18n_digests.values).to all match(md5_hex_regex)
expect(
Rails.application.config.x.i18n_digests[:en]
).to eq(Digest::MD5.hexdigest(Rails.root.join("config/locales/en.yml").read))
expect(
Rails.application.config.x.i18n_digests[:es]
).to eq(Digest::MD5.hexdigest(Rails.root.join("config/locales/es.yml").read))
end
end
describe "#for_locale" do
let(:digests) { { en: "as8d7a9sdh", es: "iausyd9asdh" } }
before do
allow(Rails).to receive_message_chain(:application, :config, :x, :i18n_digests) { digests }
end
it "returns the digest for a given locale" do
expect(I18nDigests.for_locale("en")).to eq "as8d7a9sdh"
end
end
end