class CollectionRule(models.Model): """ We include products in collections according to the conditions defined here which means that we need to recalculate the collection:ns everytime a product is updated. We also need to re calculate whenever a rule is updated. """ collection = models.ForeignKey(CustomCollection, verbose_name=_('collection')) column = ChoiceField(_('column'), choices=COLUMN_CHOICES) condition = StringField(_('condition'), blank=False) position = PositionField() relation = ChoiceField(_('relation'), choices=RELATION_CHOICES) class Meta: unique_together = (('collection', 'column', 'condition', 'relation')) ordering = ('position',) verbose_name = _('collection rule') verbose_name_plural = _('collection rules') def __str__(self): return '{} - {} {} {}'.format( self.collection, self.column, self.relation, self.condition )
class Fulfillment(models.Model): created_at = models.DateTimeField(_('created at'), auto_now_add=True) # line items notify_customer = models.BooleanField(_('notify customer'), default=False) order = models.ForeignKey('orders.Order', verbose_name=_('order')) receipt_testcase = models.NullBooleanField(_('reciept testcase'), default=None) reciept_autorization = StringField(_('receipt authorization')) status = ChoiceField(_('status'), choices=STATUS_CHOICES) tracking_company_rel = models.ForeignKey(TrackingCompany, verbose_name=_('tracking company')) updated_at = models.DateTimeField(_('updated at'), auto_now=True) def tracking_company(self): return self.tracking_company_rel.name def tracking_numbers(self): return list(self.trackingnumber_set.values_list('number', flat=True)) def tracking_number(self): return ', '.join(self.tracking_numbers()) def tracking_urls(self): url = self.tracking_company_rel.tracking_url return [url.format(nr) for nr in self.tracking_numbers()] def tracking_url(self): return ', '.join(self.tracking_url()) def variant_inventory_management(self): raise NotImplemented def fulfillment_line_items(self): raise NotImplemented def item_count(self): return len(self.fulfillment_line_items())
class Comment(models.Model): article = models.ForeignKey('blogs.Article') author = StringField(_('author')) body = TextField(_('body'), help_text=_('Markdown of a comment.')) body_html = TextField(_('body html'), editable=False) created_at = models.DateTimeField(_('created at'), auto_now_add=True) email = models.EmailField(_('email'), blank=True, null=True) ip = StringField(_('ip')) published_at = models.DateTimeField(_('published at'), blank=True, null=True) status = ChoiceField(_('status'), choices=STATUS_CHOICES) updated_at = models.DateTimeField(_('updated at'), auto_now=True) user_agent = StringField(_('user agent')) class Meta: ordering = ('-created_at', ) verbose_name = _('comment') verbose_name_plural = _('commments') def __str__(self): return '{} - {} - {}'.format(self.article, self.author, self.created_at.iso_format()) def get_absolute_url(self): return self.url @cached_property def blog(self): return self.article.blog @cached_property def blog_id(self): return self.blog.pk @property def content(self): return self.body_html def save(self, **kwargs): self.body_html = bleach.clean(markdown.markdown(self.body)) super().save(**kwargs) @cached_property def url(self): return '{}#{}'.format(self.blog.article.url, self.id)
class FulfillmentService(models.Model): callback_url = models.URLField(_('callback url'), blank=True, null=True) format = ChoiceField(_('format'), choices=FORMAT_CHOICES) handle = HandleField(_('handle'), populate_from='name') inventory_management = models.BooleanField(_('inventory management'), help_text=_('fulfillment service tracks product inventory')) name = StringField(_('name'), required=True) provider_id = StringField(_('provider id'), help_text=_('A unique identifier for the fulfillment service provider.'), unique=True) requires_shipping_method = models.BooleanField(_('requires shipping method')) tracking_support = models.BooleanField(_('tracking support')) class Meta: ordering = ('name',) verbose_name = _('fulfillment service') verbose_name_plural = _('fulfillment services') def __str__(self): return self.name
class Blog(MetaFieldsMixin, models.Model): commentable = ChoiceField( _('commentable'), choices=COMMENTABLE_CHOICES, help_text=_('Manage how comments are handled on this blog.')) created_at = models.DateTimeField(_('created at'), auto_now_add=True) feedburner_location = models.URLField(_('feedburner url'), blank=True, null=True) handle_t = TransHandleField(_('handle'), populate_from='title_t') tags_t = TransTagField(_('tag')) template_suffix = StringField(_('template suffix')) title_t = TransStringField(_('title'), required=True) class Meta: ordering = ('title_t', ) verbose_name = _('blog') verbose_name_plural = _('blogs') def __str__(self): return self.title @cached_property def all_tags(self): """ Returns all tags of all articles of a blog. This includes tags of articles that are not in the current pagination view. """ raise NotImplemented @cached_property def articles(self): """ Returns an array of all articles in a blog. """ raise NotImplemented return List() @cached_property def articles_count(self): """ Returns the total number of articles in a blog. This total does not include hidden articles. """ return len(self.articles) @cached_property def comments_enabled(self): """ Returns true if comments are enabled, or false if they are disabled. """ return self.commentable != 'no' @cached_property def feedburner(self): if self.feedburner_location: return True def get_absolute_url(self): return self.url @cached_property def moderated(self): """ Returns true if comments are moderated, or false if they are not moderated. """ return self.commentable == 'moderate' @cached_property def adjecant_articles(self): raise NotImplemented @cached_property def next_article(self): """ Returns the URL of the next (older) post. Returns false if there is no next article. """ return self.adjecant_articles['next'] @cached_property def previous_article(self): """ Returns the URL of the previous (newer) post. Returns false if there is no next article. """ return self.adjecant_articles['previous'] @cached_property def tags(self): """ Returns all tags in a blog. Similar to all_tags, but only returns tags of articles that are in the filtered view. """ raise NotImplemented @cached_property def url(self): """ Returns the relative URL of the blog. """ return '/blogs/{}'.format(self.handle)
class Order(models.Model): billing_address = models.ForeignKey('customers.CustomerAddress', related_name='order_billing_address_set') browser_ip = StringField(_('browser ip')) buyer_accepts_marketing = models.BooleanField(_('buyer accepts marketing'), default=False) cancel_reason = ChoiceField(_('cancel reason'), choices=CANCEL_CHOICES) cancelled_at = models.DateTimeField(_('cancelled at'), blank=True, null=True) cart_token = StringField(_('cart token'), required=True) client_details = models.ForeignKey('orders.ClientDetail', verbose_name=_('client details')) closed_at = models.DateTimeField(_('closed at'), blank=True, null=True) created_at = models.DateTimeField(_('created at'), auto_now_add=True) currency = ChoiceField(_('currency'), choices=CURRENCY_CHOICES) customer = models.ForeignKey('customers.Customer', blank=True, null=True, related_name='order_billing_addresss_set') discounts_m2m = models.ManyToManyField('discounts.Discount', blank=True) email = models.EmailField(_('email'), blank=True, null=True) financial_status = ChoiceField(_('financial status'), choices=FINANCIAL_STATUS_CHOICES) fulfillment_status = ChoiceField(_('fulfilment status'), choices=FULFILLMENT_STATUS_CHOICES) tags = ArrayField(StringField(_('tag'), required=True), verbose_name=_('tags'), default=[]) landing_site = models.URLField(_('landing site'), blank=True, null=True) # line items location = models.ForeignKey('locations.Location', blank=True, null=True) order_number = models.PositiveIntegerField(_('order'), editable=False) # its nice to be searchable note = TextField(_('note')) note_attributes = JSONField(_('note attributes')) processed_at = models.DateTimeField(_('processed at'), blank=True) processing_method = ChoiceField(_('processing method'), choices=PROCESSING_CHOICES) referring_site = models.URLField(_('referring site'), blank=True, null=True) # Do we need to lock this value somehow, infact all related models should be 'locked' on finalization shipping_address = models.ForeignKey('customers.CustomerAddress', blank=True, null=True, related_name='order_shipping_address_set') source_name = StringField(_('source name'), default='api') subtotal_price = models.FloatField(_('subtotal price')) taxes_included = models.BooleanField(_('taxes included')) token = models.CharField(_('token'), max_length=32, default=get_token, editable=False) # All of these total fields are probably better of being calculated values, let's keep # normalizing at a minimum since it requires more thinking power total_discounts = models.FloatField(_('total discounts'), default=0) total_line_items_price = models.FloatField(_('total line items price')) total_price = models.FloatField(_('total price')) total_tax = models.FloatField(_('total tax')) total_weight = models.FloatField(_('total weight')) updated_at = models.DateTimeField(_('updated at'), auto_now=True) user = models.ForeignKey('users.User', blank=True, null=True) order_status_url = models.URLField(_('order status url'), blank=True, null=True) @cached_property def cancel_reason_label(self): return self.get_cancel_reason_label() @cached_property def cancelled(self): return self.cancelled_at is not None @cached_property def customer_url(self): """ Returns the URL of the customer's account page. https://help.shopify.com/themes/liquid/objects/order#order-customer_url """ raise NotImplemented @cached_property def discounts(self): return List(self.discounts_m2m.all()) @cached_property def discount_codes(self): codes = [] for d in self.discouns: codes.append({ 'amount': d.amount(), 'code': d.code, 'type': d.discount_type, }) return codes @cached_property def financial_status_label(self): return self.get_financial_status_label() @cached_property def fulfillment_status_label(self): return self.get_fulfillment_status_label() @cached_property def fulfillments(self): return List(self.fulfillment_set.all()) @property def gateway(self): """ Deprecated as of July 14, 2014. This information is instead available on transactions The payment gateway used. """ return '' @cached_property def name(self): """ The customer's order name as represented by a number. """ return '#{}'.format(self.number) @cached_property def number(self): """ Numerical identifier unique to the shop. A number is sequential and starts at 1000. The example shows number 1, it is a little unclear what this is for. """ return self.pk @cached_property def payment_details(self): """ Deprecated. This information is instead available on transactions An object containing information about the payment. """ return { "avs_result_code": None, "credit_card_bin": None, "cvv_result_code": None, "credit_card_number": None, "credit_card_company": None, } @cached_property def payment_gateway_names(self): """ This should fetch information from the related transaction """ raise NotImplemented @cached_property def refunds(self): """ The list of refunds applied to the order. """ #return List(self.refund_set.all()) raise NotImplemented def save(self, **kwargs): self.order_number = self.pk + 999 if not self.processed_at: self.processed_at = datetime.datetime.now() super().save(**kwargs) @cached_property def shipping_lines(self): """ An array of shipping_line objects, each of which details the shipping methods used. """ raise NotImplemented @cached_property def tax_lines(self): """ An array of tax_line objects, each of which details the total taxes applicable to the order. https://help.shopify.com/api/reference/order#tax-lines-property """ raise NotImplemented @cached_property def tax_price(self): return self.total_tax
class Shop(MetaFieldsMixin, models.Model): address1 = StringField(_('address')) address2 = StringField(_("address con't")) city = StringField(_('city')) county_taxes = models.NullBooleanField(_('county taxes')) country_code = CountryField(_('country'), blank=True, null=True) created_at = models.DateTimeField(_('created at'), auto_now_add=True) currency = ChoiceField(_('currency'), choices=CURRENCY_CHOICES) customer_email = models.EmailField(_('customer email'), blank=True, null=True) description = TextField(_('description')) domain = StringField(_('domain')) email = models.EmailField(_('email'), blank=True, null=True) enabled_payment_types = ArrayField(ChoiceField(_('payment type'), choices=PAYMENT_TYPES_CHOICES), verbose_name=_('enabled payment types'), default=[]) force_ssl = models.BooleanField(_('force ssl'), default=False) google_apps_domain = StringField(_('google apps domain')) google_apps_login_enabled = models.NullBooleanField(_('google apps login enabled'), default=None) iana_timezone = TimeZoneField(_('timezone')) latitude = models.FloatField(_('latitude'), blank=True, null=True) longitude = models.FloatField(_('longitude'), blank=True, null=True) money_format = StringField(_('money format')) money_with_currency_format = StringField(_('money with currency format')) myshopify_domain = StringField(_('my shopify domain')) name = StringField(_('name')) navigation = JSONField(_('navigation'), default={}) password_enabled = models.BooleanField(_('password enabled'), default=False) password_message = TextField(_('password message')) phone = StringField(_('phone')) plan_name = ChoiceField(_('plan name'), choices=PLAN_CHOICES) primary_locale = StringField(_('primary locale'), max_length=10) permanent_domain = StringField(_('permanent domain')) province = StringField(_('region')) province_code = StringField(_('region code')) shop_owner = StringField(_('shop owner')) tax_shipping = models.BooleanField(_('tax shipping'), default=True) taxes_included = models.NullBooleanField(_('taxes included'), default=True) live_theme = models.ForeignKey('shops.Theme', verbose_name=_('live theme'), blank=True, null=True, related_name='shop_live_theme') updated_at = models.DateTimeField(_('updated at'), auto_now=True) zip = StringField(_('postal / Zip code')) class Meta: verbose_name = _('shop') verbose_name_plural = _('shops') ordering = ('name',) def __str__(self): return self.name @cached_property def collections_count(self): """ Returns the number of collections in a shop. """ raise NotImplemented @property def country(self): """ Alias to country_name """ return self.country_name @cached_property def country_name(self): """ The shop's normalized country name. """ if self.contry_code: return self.country_code.name @cached_property def country_upper(self): """ Returns the country in the shop's address using uppercase letters. """ return self.country.upper() def get_absolute_url(self): return self.url @cached_property def has_discounts(self): """ Indicates if any active discounts exist for the shop. """ raise NotImplemented @cached_property def has_gift_cards(self): """ Indicates if any active gift cards exist for the shop. """ raise NotImplemented @cached_property def has_storefront(self): """ Indicates whether the shop has web-based storefront or not. """ raise NotImplemented @cached_property def locale(self): """ Returns the locale that the shop is currently displayed in (ex: en, fr, pt-BR). """ return self.primary_locale @cached_property def plan_display_name(self): """ The display name of the Shopify plan the shop is on. """ return self.get_plan_name_display() @cached_property def products_count(self): """ Returns the number of products in a shop. """ raise NotImplemented @cached_property def setup_required(self): """ Indicates whether the shop has any outstanding setup steps or not. """ raise NotImplemented @cached_property def secure_url(self): """ Returns the full URL of a shop prepended by the https protocol. Example: https://johns-apparel.com """ return 'https://{}'.format(self.domain) @cached_property def source(self): """ ? """ return None @cached_property def street(self): """ Returns the combined values of the Address1 and Address2 fields of the address. """ return ' '.join(filter(None, self.address1, self.address2)) @cached_property def summary(self): """ Returns a summary of the shop's address: 150 Elgin Street, Ottawa, Ontario, Canada The summary takes the form street, city, state/province, country. """ return ', '.join([self.street, self.city, self.province, self.country]) @cached_property def timezone(self): """ The name of the timezone the shop is in. """ # do something with self.iana_timezone raise NotImplemented @cached_property def types(self): """ Returns an array of all unique product types in a shop. """ raise NotImplemented @cached_property def url(self): """ Returns the full URL of a shop. Example: http://johns-apparel.com """ return 'http://{}'.format(self.domain) @cached_property def vendors(self): """ Returns an array of all unique vendors in a shop. """ raise NotImplemented
class Discount(models.Model): applies_once = models.BooleanField(_('limit to one usage'), default=False) applies_once_per_customer = models.BooleanField(_('limit 1 per customer'), default=False) applies_to_id = models.PositiveIntegerField(_('applies to id'), blank=True, null=True) applies_to_resource = ChoiceField(_('applies to resource'), choices=RESOURCE_CHOICES) code = StringField(_('discount code'), required=True, unique=True) discount_type = ChoiceField(_('type'), choices=TYPE_CHOICES) ends_at = models.DateField(_('end date'), blank=True, null=True) minimum_order_amount = models.FloatField(_('minimum order amount'), null=True, blank=True) starts_at = models.DateField(_('start date'), blank=True, null=True) status = ChoiceField(_('status'), choices=STATUS_CHIOCES) times_used = models.PositiveIntegerField(_('used'), blank=True, null=True) usage_limit = models.PositiveIntegerField(_('total available'), blank=True, null=True) value = models.FloatField(_('value'), null=True, blank=True) class Meta: ordering = ('-starts_at', 'code') verbose_name = _('discount') verbose_name_plural = _('discounts') def __str__(self): return self.code def amount(self): """ Returns the amount of the discount. Use one of the money filters to return the value in a monetary format. """ raise NotImplemented @cached_property def title(self): """ Alias of the discount code """ return self.code def total_amount(self): """ Returns the total amount of the discount if it has been applied to multiple line items. Use a money filter to return the value in a monetary format. """ raise NotImplemented def savings(self): """ Returns the amount of the discount's savings. The negative opposite of amount. Use one of the money filters to return the value in a monetary format. """ raise NotImplemented def total_savings(self): """ Returns the total amount of the discount's savings if it has been applied to multiple line items. The negative opposite of total_amount. Use a money filter to return the value in a monetary format. """ raise NotImplemented @cached_property def type(self): """ Returns the type of the discount. """ mapping = { 'fixed_amount': 'FixedAmountDiscount', 'percentage': 'PercentageDiscount', 'shipping': 'ShippingDiscount', } return mapping[self.discount_type]
class Product(MetaFieldsMixin, models.Model): body_html_t = TransWysiwygField(_('description')) collections_m2m = models.ManyToManyField('products.CustomCollection', through='products.Collect', blank=True) created_at = models.DateTimeField(_('created at'), auto_now_add=True) handle_t = TransHandleField(_('handle'), populate_from='title_t') # sh*pify probably has a related table implementation, is this good enough? option1_name_t = TransStringField(_('option #1 name')) option2_name_t = TransStringField(_('option #2 name')) option3_name_t = TransStringField(_('option #3 name')) product_type = StringField( _('product type')) # this might need a related table published_at = models.DateTimeField(_('published at'), help_text=_('publish this product on'), blank=True, null=True) published_scope = ChoiceField(_('visability'), choices=PUBLICATION_CHOICES) tags = ArrayField(StringField(_('tag'), required=True), verbose_name=_('tags'), default=[], blank=True) template_suffix = StringField(_('template suffix')) title_t = TransStringField(_('title'), required=True) updated_at = models.DateTimeField(_('updated at'), auto_now=True) vendor = StringField(_('vendor'), blank=True, null=True) objects = ProductManager() class Meta: ordering = ('title_t', ) verbose_name = _('product') verbose_name_plural = _('products') def __str__(self): return self.title @cached_property def availble(self): """ Returns true if a product is available for purchase. Returns falseif all of the products variants' inventory_quantity values are zero or less, and their inventory_policy is not set to "Allow users to purchase this item, even if it is no longer in stock." """ for v in self.variants(): if v.availble(): return True return False @cached_property def compare_at_prices(self): return List([v.compare_at_price for v in self.variants()]) @cached_property def compare_at_price_max(self): """ Returns the highest compare at price. Use one of the money filters to return the value in a monetary format. """ if self.compare_at_prices: return max(self.compare_at_prices) @cached_property def compare_at_price_min(self): """ Returns the lowest compare at price. Use one of the money filters to return the value in a monetary format. """ if self.compare_at_prices: return min(self.compare_at_prices) @cached_property def compare_at_price_varies(self): """ Returns true if the compare_at_price_min is different from the compare_at_price_max. Returns false if they are the same. """ return self.compare_at_price_max != self.compare_at_price_min @cached_property def collections(self): return List(self.collections_m2m.all()) @property def content(self): """ Alias for body_html """ return self.body_html @property def description(self): """ Alias for body_html """ return self.body_html @cached_property def featured_image(self): """ Returns the relative URL of the product's featured image. """ if self.images: return self.image[0].file.url @cached_property def first_variant(self): if self.variants: return self.variants[0] @cached_property def first_available_variant(self): """ Returns the variant object of the first product variant that is available for purchase. In order for a variant to be available, its variant.inventory_quantity must be greater than zero or variant.inventory_policy must be set to continue. A variant with no inventory_policy is considered available. """ for v in self.variants: if v.available: return v def get_absolute_url(self): return '/products/{}'.format(self.handle) @cached_property def images(self): """ Returns an array of the product's images. Use the product_img_url filter to link to the product image on Shopify's Content Delivery Network. """ return List(self.productimage_set.all()) @cached_property def options(self): """ Returns an array of the product's option names. """ names = [self.option1_name, self.option2_name, self.option3_name] return List([n for n in names if n]) @cached_property def options_with_values(self): """ Returns an array of the product's options including their available and currently selected values. """ opts = List() if self.option1_name: values = [v.option1 for v in self.variants if v.option1] opts.append(Option(self.option1_name, values)) if self.option2_name: values = [v.option2 for v in self.variants if v.option2] opts.append(Option(self.option2_name, values)) if self.option3_name: values = [v.option3 for v in self.variants if v.option3] opts.append(Option(self.option3_name, values)) return opts @property def price(self): """ Alias for price_min """ return self.price_min @cached_property def price_max(self): """ Returns the highest price of the product. Use one of the money filters to return the value in a monetary format. """ if self.prices: return max(self.prices) @cached_property def price_min(self): """ Returns the lowest price of the product. Use one of the money filters to return the value in a monetary format. """ if self.prices: return min(self.prices) @cached_property def price_varies(self): """ Returns true if the product's variants have varying prices. Returns false if all of the product's variants have the same price. """ return self.price_max != self.price_min @cached_property def prices(self): return [v.price for v in self.variants()] @cached_property def selected_variant(self): """ Returns the variant object of the currently-selected variant if there is a valid ?variant= parameter in the URL. Returns nil if there is not. """ request = get_request() variant_id = request.GET.get('variant') for v in self.variants: if variant_id == v.pk: return v @cached_property def selected_or_first_available_variant(self): """ Returns the variant object of the currently-selected variant if there is a valid ?variant= query parameter in the URL. If there is no selected variant, the first available variant is returned. In order for a variant to be available, its variant.inventory_quantity must be greater than zero or variant.inventory_policy must be set to continue. A variant with no inventory_management is considered available. """ request = get_request() if 'variant' in request.GET: return self.selected_variant return self.first_available_variant @property def type(self): """ Alias for product_type """ return self.product_type @cached_property def url(self): """ Alias for get_absolute_url """ return self.get_absolute_url() @cached_property def variants(self): """ Returns an array the product's variants. """ return List(self.productvariant_set.all())
class CustomCollection(MetaFieldsMixin, models.Model): body_html_t = TransWysiwygField(_('description')) disjunctive = models.BooleanField(_('products can match any condition'), default=False) handle_t = TransHandleField(_('handle'), populate_from='title_t') image = ImageField(_('image'), upload_to='products') published = models.BooleanField(_('published'), default=True) published_at = models.DateTimeField(_('published at'), help_text=_('publish this collection on'), blank=True, null=True) published_scope = ChoiceField(_('visability'), choices=PUBLICATION_CHOICES) sort_order = ChoiceField(_('sort'), choices=SORT_CHOICES) template_suffix = StringField(_('template suffix')) title_t = TransStringField(_('title'), required=True) updated_at = models.DateTimeField(_('updated at'), auto_now=True) objects = CustomCollectionManager() class Meta: ordering = ('-title_t',) verbose_name = _('collection') verbose_name_plural = _('collections') def __str__(self): return self.title @cached_property def all_tags(self): """ Returns a list of all product tags in a collection. collection.all_tags will return the full list of tags even when the collection view is filtered. collection.all_tags will return at most 1,000 tags. In comparison, collection.tags returns all tags for a collection for the current view. For example, if a collection is filtered by tag, collection.tags returns only the tags that match the current filter. """ tags = set() for p in self.products: tags.union(p.tags) return List(sorted(tags)) @cached_property def all_types(self): """ Returns a list of all product types in a collection. """ return List(sorted(filter(None, set(p.product_type for p in self.products)))) @cached_property def all_products_count(self): """ Returns the number of products in a collection. collection.all_products_count will return the total number of products even when the collection view is filtered. In comparison, collection.products_count returns all tags for a collection for the current view. For example, if a collection is filtered by tag, collection.products_count returns the number of products that match the current filter. """ return len(self.products) @cached_property def all_vendors(self): """ Returns a list of all product vendors in a collection. """ return List(sorted(filter(None, set(p.vendor for p in self.products)))) def current_type(self): """ Returns the product type on a /collections/types?q=TYPE collection page. """ request = get_request() if request.path.endswith('/types'): return request.GET.get('q') def current_vendor(self): """ Returns the product vendor on a /collections/vendors?q=VENDOR collection page. """ request = get_request() if request.path.endswith('/vendors'): return request.GET.get('q') @cached_property def default_sort_by(self): """ Returns the sort order of the collection, which is set on the collection's page in your admin. """ # inconsistant # API: https://help.shopify.com/api/reference/customcollection#sort-order-property # template: https://help.shopify.com/themes/liquid/objects/collection#collection-default_sort_by mapping = { 'alpha-asc': 'title-descending', 'alpha-desc': 'title-asc', 'best-selling': 'best-selling', 'created': 'created-ascending', 'created-desc': 'created-descending', 'manual': 'manual', 'price-asc': 'price-ascending', 'price-desc': 'price-descending', } return mapping[self.sort_order] def get_absolute_url(self): return '/collection/{}'.format(self.handle) @cached_property def description(self): """ Alias to body_html """ return self.body_html @cached_property def adjecant_products(self): request = get_request() # request.path should look something like: /collections/contemporary-cityscapes/products/auckland handle = request.path.split('/')[-1] res = {'next': False, 'previous': False} for j, p in enumerate(self.products): if p.handle == handle: if j + 1 < len(self.products): res['next'] = self.products[j + 1] if j - 1 >= 0: res['previous'] = self.products[j - 1] return res @cached_property def next_product(self): """ Returns the URL of the next product in the collection. Returns nil if there is no next product. You can use collection.next_product and collection.previous_product with the within filter to create "next" and "previous" links in the product template. """ return self.adjecant_products['next'] @cached_property def previous_product(self): """ Returns the URL of the previous product in the collection. Returns nil if there is no previous product. You can use collection.next_product and collection.previous_product with the within filter to create "next" and "previous" links in the product template. """ return self.adjecant_products['previous'] @cached_property def products(self): """ Returns all of the products in a collection. You can show a maximum of 50 products per page. Use the paginate tag to choose how many products are shown per page. """ return List(self.product_set.all()) def products_count(self): """ Returns the number of products in a collection that match the current view. For example, if you are viewing a collection filtered by tag, collection.products_count will return the number of products that match the chosen tag. """ raise NotImplemented def tags(self): """ Returns the tags of products in a collection that match the current view. For example, if you are viewing a collection filtered by tag, collection.tags will return the tags for the products that match the current filter. """ raise NotImplemented @cached_property def url(self): return self.get_absolute_url()
class ProductVariant(MetaFieldsMixin, models.Model): barcode = StringField(_('barcode'), help_text=_('ISBN, UPC, GTIN, etc.')) compare_at_price = models.FloatField(_('compare at price'), blank=True, null=True) created_at = models.DateTimeField(_('created at'), auto_now_add=True) fulfillment_service = models.ForeignKey('fulfillments.FulfillmentService', blank=True, null=True) image = models.ForeignKey('products.ProductImage', verbose_name=_('image'), blank=True, null=True) inventory_management = ChoiceField( _('track inventory'), help_text=_('Sumsum tracks this products inventory'), choices=INVENTORY_MANAGEMENT_CHOICES) inventory_policy = ChoiceField( _('inventory policy'), help_text=_( "Allow customers to purchase this product when it's out of stock"), choices=INVENTORY_POLICY_CHOICES) inventory_quantity = models.IntegerField(_('quantity'), default=0) next_incoming_date = models.DateField(_('next incoming date'), blank=True, null=True) option1_t = TransStringField(_('option #1')) option2_t = TransStringField(_('option #2')) option3_t = TransStringField(_('option #3')) position = PositionField() price = models.FloatField(_('price'), default=0) product = models.ForeignKey('products.Product', verbose_name=_('product')) requires_shipping = models.BooleanField( _('requires shipping'), help_text=_('This product requires shipping'), default=False) sku = StringField(_('sku'), help_text=_('Stock Keeping Unit')) taxable = models.BooleanField(_('taxable'), default=True) updated_at = models.DateTimeField(_('updated at'), auto_now=True) weight_in_unit = models.FloatField(_('weight'), blank=True, null=True) weight_unit = ChoiceField(_('weight unit'), choices=UNIT_CHOICES, default='kg') class Meta: ordering = ('product', ) verbose_name = _('variant') verbose_name_plural = _('variants') def __str__(self): return self.title @cached_property def grams(self): """ The weight of the product variant in grams. """ if not self.weight_in_unit: return None unit_factor = { 'g': 1, 'kg': 1000, 'oz': 28.349523125, 'lb': 453.59237, } return unit_factor[self.weight_unit] * self.weight_in_unit @cached_property def availble(self): """ Returns True if the variant is available for purchase, or False if it not. For a variant to be available, its variant.inventory_quantity must be greater than zero or variant.inventory_policy must be set to continue. A variant with no variant.inventory_management is also considered available. """ if self.inventory_management == 'blank' or self.inventory_policy == 'continue': return True return self.inventory_quantity > 0 def get_absulute_url(self): raise '{}?variant={}'.format(self.product.get_absolute_url(), self.pk) @cached_property def incoming(self): """ Returns true if the variant has incoming inventory. """ return self.next_incoming_date and self.next_incoming_date >= datetime.date.today( ) def selected(self): """ Returns true if the variant is currently selected by the ?variant= URL parameter, or false if it is not. """ request = get_request() return self.pk == request.GET.get('variant') @cached_property def title(self): """ Returns the concatenation of all the variant's option values, joined by / characters. """ return ' / '.join( filter(None, [self.option1, self.option2, self.option3])) @cached_property def url(self): return self.get_absolute_url()
class User(MetaFieldsMixin, AbstractBaseUser, PermissionsMixin): accepts_marketing = models.BooleanField(_('customer accepts marketing'), default=False) bio = WysiwygField(_('biography')) created_at = models.DateTimeField(_('created at'), auto_now_add=True) default_address = models.ForeignKey('customers.CustomerAddress', verbose_name=_('default address'), blank=True, null=True) email = models.EmailField(_('email address'), unique=True) first_name = StringField(_('first name')) homepage = models.URLField(_('homepage'), blank=True, null=True) last_name = StringField(_('last name')) multipass_identifier = StringField(_('multipass identifier')) note = TextField( _('notes'), help_text=_('Enter any extra notes relating to this customer.')) state = ChoiceField( _('state'), max_length=50, choices=STATE_CHOICES, required=False) # maybe we need to sync this the is_active field tags = ArrayField(StringField(_('tag'), required=True), verbose_name=_('tags'), default=[]) tax_exempt = models.BooleanField(_('customer is tax excempt'), default=False) updated_at = models.DateTimeField(_('updated at'), auto_now=True) verified_email = models.BooleanField(_('verified email'), default=False) is_staff = models.BooleanField( _('staff status'), default=False, help_text=_( 'Designates whether the user can log into this admin site.')) is_active = models.BooleanField( _('active'), default=True, help_text= _('Designates whether this user should be treated as active. Unselect this instead of deleting accounts.' )) USERNAME_FIELD = 'email' objects = UserManager() class Meta: ordering = ('first_name', ) verbose_name = _('user') verbose_name_plural = _('users') def get_full_name(self): return ' '.join(filter(None, [self.first_name, self.last_name])) def get_short_name(self): return self.first_name def email_user(self, subject, message, from_email=None, **kwargs): send_mail(subject, message, from_email, [self.email], **kwargs) @cached_property def account_owner(self): """ https://help.shopify.com/themes/liquid/objects/article#article-user-account_owner Returns "true" if the author of the article is the account owner of the shop. Returns "false" if the author is not the account owner. """ raise NotImplemented