class Product(TaxableItem, AttributableMixin, TranslatableModel): COMMON_SELECT_RELATED = ("type", "primary_image", "tax_class") # Metadata created_on = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_('created on')) modified_on = models.DateTimeField(auto_now=True, editable=False, verbose_name=_('modified on')) deleted = models.BooleanField(default=False, editable=False, db_index=True, verbose_name=_('deleted')) # Behavior mode = EnumIntegerField(ProductMode, default=ProductMode.NORMAL, verbose_name=_('mode')) variation_parent = models.ForeignKey( "self", null=True, blank=True, related_name='variation_children', on_delete=models.PROTECT, verbose_name=_('variation parent')) stock_behavior = EnumIntegerField(StockBehavior, default=StockBehavior.UNSTOCKED, verbose_name=_('stock')) shipping_mode = EnumIntegerField(ShippingMode, default=ShippingMode.NOT_SHIPPED, verbose_name=_('shipping mode')) sales_unit = models.ForeignKey("SalesUnit", verbose_name=_('unit'), blank=True, null=True, on_delete=models.PROTECT) tax_class = models.ForeignKey("TaxClass", verbose_name=_('tax class'), on_delete=models.PROTECT) # Identification type = models.ForeignKey( "ProductType", related_name='products', on_delete=models.PROTECT, db_index=True, verbose_name=_('product type')) sku = models.CharField(db_index=True, max_length=128, verbose_name=_('SKU'), unique=True) gtin = models.CharField(blank=True, max_length=40, verbose_name=_('GTIN'), help_text=_('Global Trade Item Number')) barcode = models.CharField(blank=True, max_length=40, verbose_name=_('barcode')) accounting_identifier = models.CharField(max_length=32, blank=True, verbose_name=_('bookkeeping account')) profit_center = models.CharField(max_length=32, verbose_name=_('profit center'), blank=True) cost_center = models.CharField(max_length=32, verbose_name=_('cost center'), blank=True) # Category is duplicated here because not all products necessarily belong in Shops (i.e. have # ShopProduct instances), but they should nevertheless be searchable by category in other # places, such as administration UIs. category = models.ForeignKey( "Category", related_name='primary_products', blank=True, null=True, verbose_name=_('primary category'), help_text=_("only used for administration and reporting"), on_delete=models.PROTECT) # Physical dimensions width = MeasurementField(unit="mm", verbose_name=_('width (mm)')) height = MeasurementField(unit="mm", verbose_name=_('height (mm)')) depth = MeasurementField(unit="mm", verbose_name=_('depth (mm)')) net_weight = MeasurementField(unit="g", verbose_name=_('net weight (g)')) gross_weight = MeasurementField(unit="g", verbose_name=_('gross weight (g)')) # Misc. manufacturer = models.ForeignKey( "Manufacturer", blank=True, null=True, verbose_name=_('manufacturer'), on_delete=models.PROTECT) primary_image = models.ForeignKey( "ProductMedia", null=True, blank=True, related_name="primary_image_for_products", on_delete=models.SET_NULL, verbose_name=_("primary image")) translations = TranslatedFields( name=models.CharField(max_length=256, verbose_name=_('name')), description=models.TextField(blank=True, verbose_name=_('description')), slug=models.SlugField(verbose_name=_('slug'), max_length=255, null=True), keywords=models.TextField(blank=True, verbose_name=_('keywords')), status_text=models.CharField( max_length=128, blank=True, verbose_name=_('status text'), help_text=_( 'This text will be shown alongside the product in the shop.' ' (Ex.: "Available in a month")')), variation_name=models.CharField( max_length=128, blank=True, verbose_name=_('variation name')) ) objects = ProductQuerySet.as_manager() class Meta: ordering = ('-id',) verbose_name = _('product') verbose_name_plural = _('products') def __str__(self): try: return u"%s" % self.name except ObjectDoesNotExist: return self.sku def get_shop_instance(self, shop): """ :type shop: shuup.core.models.Shop :rtype: shuup.core.models.ShopProduct """ shop_inst_cache = self.__dict__.setdefault("_shop_inst_cache", {}) cached = shop_inst_cache.get(shop) if cached: return cached shop_inst = self.shop_products.get(shop=shop) shop_inst._product_cache = self shop_inst._shop_cache = shop shop_inst_cache[shop] = shop_inst return shop_inst def get_priced_children(self, context, quantity=1): """ Get child products with price infos sorted by price. :rtype: list[(Product,PriceInfo)] :return: List of products and their price infos sorted from cheapest to most expensive. """ priced_children = ( (child, child.get_price_info(context, quantity=quantity)) for child in self.variation_children.all()) return sorted(priced_children, key=(lambda x: x[1].price)) def get_cheapest_child_price(self, context, quantity=1): price_info = self.get_cheapest_child_price_info(context, quantity) if price_info: return price_info.price def get_child_price_range(self, context, quantity=1): """ Get the prices for cheapest and the most expensive child The attribute used for sorting is `PriceInfo.price`. Return (`None`, `None`) if `self.variation_children` do not exist. This is because we cannot return anything sensible. :type context: shuup.core.pricing.PricingContextable :type quantity: int :return: a tuple of prices :rtype: (shuup.core.pricing.Price, shuup.core.pricing.Price) """ items = [c.get_price_info(context, quantity=quantity) for c in self.variation_children.all()] if not items: return (None, None) infos = sorted(items, key=lambda x: x.price) return (infos[0].price, infos[-1].price) def get_cheapest_child_price_info(self, context, quantity=1): """ Get the `PriceInfo` of the cheapest variation child The attribute used for sorting is `PriceInfo.price`. Return `None` if `self.variation_children` do not exist. This is because we cannot return anything sensible. :type context: shuup.core.pricing.PricingContextable :rtype: shuup.core.pricing.PriceInfo """ items = [c.get_price_info(context, quantity=quantity) for c in self.variation_children.all()] if not items: return None return sorted(items, key=lambda x: x.price)[0] def get_price_info(self, context, quantity=1): """ Get `PriceInfo` object for the product in given context. Returned `PriceInfo` object contains calculated `price` and `base_price`. The calculation of prices is handled in the current pricing module. :type context: shuup.core.pricing.PricingContextable :rtype: shuup.core.pricing.PriceInfo """ from shuup.core.pricing import get_price_info return get_price_info(product=self, context=context, quantity=quantity) def get_price(self, context, quantity=1): """ Get price of the product within given context. .. note:: When the current pricing module implements pricing steps, it is possible that ``p.get_price(ctx) * 123`` is not equal to ``p.get_price(ctx, quantity=123)``, since there could be quantity discounts in effect, but usually they are equal. :type context: shuup.core.pricing.PricingContextable :rtype: shuup.core.pricing.Price """ return self.get_price_info(context, quantity).price def get_base_price(self, context, quantity=1): """ Get base price of the product within given context. Base price differs from the (effective) price when there are discounts in effect. :type context: shuup.core.pricing.PricingContextable :rtype: shuup.core.pricing.Price """ return self.get_price_info(context, quantity=quantity).base_price def get_available_attribute_queryset(self): if self.type_id: return self.type.attributes.visible() else: return Attribute.objects.none() def get_available_variation_results(self): """ Get a dict of `combination_hash` to product ID of variable variation results. :return: Mapping of combination hashes to product IDs :rtype: dict[str, int] """ return dict( ProductVariationResult.objects.filter(product=self).filter(status=1) .values_list("combination_hash", "result_id") ) def get_all_available_combinations(self): """ Generate all available combinations of variation variables. If the product is not a variable variation parent, the iterator is empty. Because of possible combinatorial explosion this is a generator function. (For example 6 variables with 5 options each explodes to 15,625 combinations.) :return: Iterable of combination information dicts. :rtype: Iterable[dict] """ return get_all_available_combinations(self) def clear_variation(self): """ Fully remove variation information. Make this product a non-variation parent. """ self.simplify_variation() for child in self.variation_children.all(): if child.variation_parent_id == self.pk: child.unlink_from_parent() self.verify_mode() self.save() def simplify_variation(self): """ Remove variation variables from the given variation parent, turning it into a simple variation (or a normal product, if it has no children). :param product: Variation parent to not be variable any longer. :type product: shuup.core.models.Product """ ProductVariationVariable.objects.filter(product=self).delete() ProductVariationResult.objects.filter(product=self).delete() self.verify_mode() self.save() @staticmethod def _get_slug_name(self): if self.deleted: return None return (self.safe_translation_getter("name") or self.sku) def save(self, *args, **kwargs): if self.net_weight and self.net_weight > 0: self.gross_weight = max(self.net_weight, self.gross_weight) rv = super(Product, self).save(*args, **kwargs) generate_multilanguage_slugs(self, self._get_slug_name) return rv def delete(self, using=None): raise NotImplementedError("Not implemented: Use `soft_delete()` for products.") def soft_delete(self, user=None): if not self.deleted: self.deleted = True self.add_log_entry("Deleted.", kind=LogEntryKind.DELETION, user=user) # Bypassing local `save()` on purpose. super(Product, self).save(update_fields=("deleted",)) def verify_mode(self): if ProductPackageLink.objects.filter(parent=self).exists(): self.mode = ProductMode.PACKAGE_PARENT self.external_url = None self.variation_children.clear() elif ProductVariationVariable.objects.filter(product=self).exists(): self.mode = ProductMode.VARIABLE_VARIATION_PARENT elif self.variation_children.exists(): if ProductVariationResult.objects.filter(product=self).exists(): self.mode = ProductMode.VARIABLE_VARIATION_PARENT else: self.mode = ProductMode.SIMPLE_VARIATION_PARENT self.external_url = None ProductPackageLink.objects.filter(parent=self).delete() elif self.variation_parent: self.mode = ProductMode.VARIATION_CHILD ProductPackageLink.objects.filter(parent=self).delete() self.variation_children.clear() self.external_url = None else: self.mode = ProductMode.NORMAL def unlink_from_parent(self): if self.variation_parent: parent = self.variation_parent self.variation_parent = None self.save() parent.verify_mode() self.verify_mode() self.save() ProductVariationResult.objects.filter(result=self).delete() return True def link_to_parent(self, parent, variables=None, combination_hash=None): """ :param parent: The parent to link to. :type parent: Product :param variables: Optional dict of {variable identifier: value identifier} for complex variable linkage :type variables: dict|None :param combination_hash: Optional combination hash (for variable variations), if precomputed. Mutually exclusive with `variables` :type combination_hash: str|None """ if combination_hash: if variables: raise ValueError("`combination_hash` and `variables` are mutually exclusive") variables = True # Simplifies the below invariant checks self._raise_if_cant_link_to_parent(parent, variables) self.unlink_from_parent() self.variation_parent = parent self.verify_mode() self.save() if not parent.is_variation_parent(): parent.verify_mode() parent.save() if variables: if not combination_hash: # No precalculated hash, need to figure that out combination_hash = get_combination_hash_from_variable_mapping(parent, variables=variables) pvr = ProductVariationResult.objects.create( product=parent, combination_hash=combination_hash, result=self ) if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT: parent.verify_mode() parent.save() return pvr else: return True def _raise_if_cant_link_to_parent(self, parent, variables): """ Validates relation possibility for `self.link_to_parent()` :param parent: parent product of self :type parent: Product :param variables: :type variables: dict|None """ if parent.is_variation_child(): raise ImpossibleProductModeException( _("Multilevel parentage hierarchies aren't supported (parent is a child already)"), code="multilevel" ) if parent.mode == ProductMode.VARIABLE_VARIATION_PARENT and not variables: raise ImpossibleProductModeException( _("Parent is a variable variation parent, yet variables were not passed"), code="no_variables" ) if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT and variables: raise ImpossibleProductModeException( "Parent is a simple variation parent, yet variables were passed", code="extra_variables" ) if self.mode == ProductMode.SIMPLE_VARIATION_PARENT: raise ImpossibleProductModeException( _("Multilevel parentage hierarchies aren't supported (this product is a simple variation parent)"), code="multilevel" ) if self.mode == ProductMode.VARIABLE_VARIATION_PARENT: raise ImpossibleProductModeException( _("Multilevel parentage hierarchies aren't supported (this product is a variable variation parent)"), code="multilevel" ) def make_package(self, package_def): if self.mode != ProductMode.NORMAL: raise ImpossibleProductModeException( _("Product is currently not a normal product, can't turn into package"), code="abnormal" ) for child_product, quantity in six.iteritems(package_def): # :type child_product: Product if child_product.is_variation_parent(): raise ImpossibleProductModeException( _("Variation parents can not belong into a package"), code="abnormal" ) if child_product.is_package_parent(): raise ImpossibleProductModeException(_("Packages can't be nested"), code="multilevel") if quantity <= 0: raise ImpossibleProductModeException(_("Quantity %s is invalid") % quantity, code="quantity") ProductPackageLink.objects.create(parent=self, child=child_product, quantity=quantity) self.verify_mode() def get_package_child_to_quantity_map(self): if self.is_package_parent(): product_id_to_quantity = dict( ProductPackageLink.objects.filter(parent=self).values_list("child_id", "quantity") ) products = dict((p.pk, p) for p in Product.objects.filter(pk__in=product_id_to_quantity.keys())) return {products[product_id]: quantity for (product_id, quantity) in six.iteritems(product_id_to_quantity)} return {} def is_variation_parent(self): return self.mode in (ProductMode.SIMPLE_VARIATION_PARENT, ProductMode.VARIABLE_VARIATION_PARENT) def is_variation_child(self): return (self.mode == ProductMode.VARIATION_CHILD) def get_variation_siblings(self): return Product.objects.filter(variation_parent=self.variation_parent).exclude(pk=self.pk) def is_package_parent(self): return (self.mode == ProductMode.PACKAGE_PARENT) def is_package_child(self): return ProductPackageLink.objects.filter(child=self).exists() def get_all_package_parents(self): return Product.objects.filter(pk__in=( ProductPackageLink.objects.filter(child=self).values_list("parent", flat=True) )) def get_all_package_children(self): return Product.objects.filter(pk__in=( ProductPackageLink.objects.filter(parent=self).values_list("child", flat=True) )) def get_public_media(self): return self.media.filter(enabled=True, public=True).exclude(kind=ProductMediaKind.IMAGE) def is_stocked(self): return (self.stock_behavior == StockBehavior.STOCKED)
class GDPRSettings(TranslatableModel): shop = models.OneToOneField("shuup.Shop", related_name="gdpr_settings", on_delete=models.CASCADE) enabled = models.BooleanField(default=False, verbose_name=_('enabled'), help_text=_("Define if the GDPR is active.")) skip_consent_on_auth = models.BooleanField( default=False, verbose_name=_("skip consent on login"), help_text=_("Do not require consent on login when GDPR is activated.")) privacy_policy_page = models.ForeignKey( on_delete=models.CASCADE, to="shuup_simple_cms.Page", null=True, verbose_name=_("privacy policy page"), help_text=_( "Choose your privacy policy page here. If this page changes, customers will be " "prompted for new consent.")) consent_pages = models.ManyToManyField( "shuup_simple_cms.Page", verbose_name=_("consent pages"), related_name="consent_settings", help_text=_( "Choose pages here which are being monitored for customer consent. If any of these pages change, " "the customer is being prompted for a new consent.")) translations = TranslatedFields( cookie_banner_content=models.TextField( blank=True, verbose_name=_("cookie banner content"), help_text=_( "The text to be presented to users in a pop-up warning.")), cookie_privacy_excerpt=models.TextField( blank=True, verbose_name=_("cookie privacy excerpt"), help_text=_( "The summary text to be presented about cookie privacy.")), auth_consent_text=models.TextField( blank=True, verbose_name=_("login consent text"), help_text=_( "Shown in login page between the form and the button. " "Optional, but should be considered when the consent on login is disabled." ))) class Meta: verbose_name = _('GDPR settings') verbose_name_plural = _('GDPR settings') def __str__(self): return _("GDPR for {}").format(self.shop) def set_default_content(self): language = get_language() for code, name in settings.LANGUAGES: activate(code) self.set_current_language(code) self.cookie_banner_content = settings.SHUUP_GDPR_DEFAULT_BANNER_STRING self.cookie_privacy_excerpt = settings.SHUUP_GDPR_DEFAULT_EXCERPT_STRING self.save() self.set_current_language(language) activate(language) @classmethod def get_for_shop(cls, shop): instance, created = cls.objects.get_or_create(shop=shop) if created or not instance.safe_translation_getter( "cookie_banner_content"): instance.set_default_content() return instance
class DeliveryBoard(TranslatableModel): translations = TranslatedFields(delivery_comment=models.CharField( _("Comment"), max_length=50, blank=True, default=EMPTY_STRING)) delivery_point = models.ForeignKey( "LUT_DeliveryPoint", verbose_name=_("Delivery point"), db_index=True, on_delete=models.PROTECT, ) permanence = models.ForeignKey( "Permanence", verbose_name=REPANIER_SETTINGS_PERMANENCE_NAME, on_delete=models.CASCADE, ) status = models.CharField( max_length=3, choices=LUT_PERMANENCE_STATUS, default=PERMANENCE_PLANNED, verbose_name=_("Status"), ) is_updated_on = models.DateTimeField(_("Updated on"), auto_now=True) highest_status = models.CharField( max_length=3, choices=LUT_PERMANENCE_STATUS, default=PERMANENCE_PLANNED, verbose_name=_("Highest status"), ) def set_status(self, new_status): from repanier.models.invoice import CustomerInvoice from repanier.models.purchase import PurchaseWoReceiver now = timezone.now() self.is_updated_on = now self.status = new_status if self.highest_status < new_status: self.highest_status = new_status self.save(update_fields=["status", "is_updated_on", "highest_status"]) CustomerInvoice.objects.filter( delivery_id=self.id).order_by("?").update(status=new_status) PurchaseWoReceiver.objects.filter( customer_invoice__delivery_id=self.id).order_by("?").update( status=new_status) def get_delivery_display(self, br=False, color=False): short_name = "{}".format( self.delivery_point.safe_translation_getter("short_name", any_language=True, default=EMPTY_STRING)) comment = self.safe_translation_getter("delivery_comment", any_language=True, default=EMPTY_STRING) if color: label = mark_safe('<font color="green">{} {}</font>'.format( comment, short_name)) elif br: label = mark_safe("{}<br>{}".format(comment, short_name)) else: label = "{} {}".format(comment, short_name) return label def get_delivery_status_display(self): return "{} - {}".format(self, self.get_status_display()) def get_delivery_customer_display(self): if self.status != PERMANENCE_SEND: return "{} - {}".format(self, self.get_status_display()) else: return "{} - {}".format(self, _("Orders closed")) def __str__(self): return self.get_delivery_display() class Meta: verbose_name = _("Delivery board") verbose_name_plural = _("Deliveries board") ordering = ("id", )
class Article(TranslatedAutoSlugifyMixin, TranslationHelperMixin, TranslatableModel): # TranslatedAutoSlugifyMixin options slug_source_field_name = 'title' slug_default = _('untitled-article') # when True, updates the article's search_data field # whenever the article is saved or a plugin is saved # on the article's content placeholder. update_search_on_save = getattr( settings, 'ALDRYN_NEWSBLOG_UPDATE_SEARCH_DATA_ON_SAVE', False ) translations = TranslatedFields( title=models.CharField(_('title'), max_length=234), slug=models.SlugField( verbose_name=_('slug'), max_length=255, db_index=True, blank=True, help_text=_( 'Used in the URL. If changed, the URL will change. ' 'Clear it to have it re-created automatically.'), ), lead_in=HTMLField( verbose_name=_('lead'), default='', help_text=_( 'The lead gives the reader the main idea of the story, this ' 'is useful in overviews, lists or as an introduction to your ' 'article.' ), blank=True, ), meta_title=models.CharField( max_length=255, verbose_name=_('meta title'), blank=True, default=''), meta_description=models.TextField( verbose_name=_('meta description'), blank=True, default=''), meta_keywords=models.TextField( verbose_name=_('meta keywords'), blank=True, default=''), meta={'unique_together': (('language_code', 'slug', ), )}, search_data=models.TextField(blank=True, editable=False) ) content = PlaceholderField('newsblog_article_content', related_name='newsblog_article_content') author = models.ForeignKey( Person, null=True, blank=True, verbose_name=_('author'), on_delete=models.CASCADE, ) owner = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('owner'), on_delete=models.CASCADE, ) app_config = AppHookConfigField( NewsBlogConfig, verbose_name=_('Section'), help_text='', ) categories = CategoryManyToManyField('aldryn_categories.Category', verbose_name=_('categories'), blank=True) publishing_date = models.DateTimeField(_('publishing date'), default=now) is_published = models.BooleanField(_('is published'), default=False, db_index=True) is_featured = models.BooleanField(_('is featured'), default=False, db_index=True) featured_image = FilerImageField( verbose_name=_('featured image'), null=True, blank=True, on_delete=models.SET_NULL, ) tags = TaggableManager(blank=True) # Setting "symmetrical" to False since it's a bit unexpected that if you # set "B relates to A" you immediately have also "A relates to B". It have # to be forced to False because by default it's True if rel.to is "self": # # https://github.com/django/django/blob/1.8.4/django/db/models/fields/related.py#L2144 # # which in the end causes to add reversed releted-to entry as well: # # https://github.com/django/django/blob/1.8.4/django/db/models/fields/related.py#L977 related = SortedManyToManyField('self', verbose_name=_('related articles'), blank=True, symmetrical=False) objects = RelatedManager() class Meta: ordering = ['-publishing_date'] @property def published(self): """ Returns True only if the article (is_published == True) AND has a published_date that has passed. """ return self.is_published and self.publishing_date <= now() @property def future(self): """ Returns True if the article is published but is scheduled for a future date/time. """ return self.is_published and self.publishing_date > now() def get_absolute_url(self, language=None): """Returns the url for this Article in the selected permalink format.""" if not language: language = get_current_language() kwargs = {} permalink_type = self.app_config.permalink_type if 'y' in permalink_type: kwargs.update(year=self.publishing_date.year) if 'm' in permalink_type: kwargs.update(month="%02d" % self.publishing_date.month) if 'd' in permalink_type: kwargs.update(day="%02d" % self.publishing_date.day) if 'i' in permalink_type: kwargs.update(pk=self.pk) if 's' in permalink_type: slug, lang = self.known_translation_getter( 'slug', default=None, language_code=language) if slug and lang: site_id = getattr(settings, 'SITE_ID', None) if get_redirect_on_fallback(language, site_id): language = lang kwargs.update(slug=slug) if self.app_config and self.app_config.namespace: namespace = '{0}:'.format(self.app_config.namespace) else: namespace = '' with override(language): return reverse('{0}article-detail'.format(namespace), kwargs=kwargs) def get_search_data(self, language=None, request=None): """ Provides an index for use with Haystack, or, for populating Article.translations.search_data. """ if not self.pk: return '' if language is None: language = get_current_language() if request is None: request = get_request(language=language) description = self.safe_translation_getter('lead_in', '') text_bits = [strip_tags(description)] for category in self.categories.all(): text_bits.append( force_text(category.safe_translation_getter('name'))) for tag in self.tags.all(): text_bits.append(force_text(tag.name)) if self.content: plugins = self.content.cmsplugin_set.filter(language=language) for base_plugin in plugins: plugin_text_content = ' '.join( get_plugin_index_data(base_plugin, request)) text_bits.append(plugin_text_content) return ' '.join(text_bits) def save(self, *args, **kwargs): # Update the search index if self.update_search_on_save: self.search_data = self.get_search_data() # Ensure there is an owner. if self.app_config.create_authors and self.author is None: self.author = Person.objects.get_or_create( user=self.owner, defaults={ 'name': ' '.join(( self.owner.first_name, self.owner.last_name, )), })[0] # slug would be generated by TranslatedAutoSlugifyMixin super(Article, self).save(*args, **kwargs) def __str__(self): return self.safe_translation_getter('title', any_language=True)
class JobOpening(TranslatedAutoSlugifyMixin, TranslationHelperMixin, TranslatableModel): slug_source_field_name = 'title' translations = TranslatedFields( title=models.CharField(_('title'), max_length=255), slug=models.SlugField( _('slug'), max_length=255, blank=True, unique=False, db_index=False, help_text=_('Auto-generated. Used in the URL. If changed, the URL ' 'will change. Clear it to have the slug re-created.')), lead_in=HTMLField( _('short description'), blank=True, help_text=_('This text will be displayed in lists.'))) content = PlaceholderField('Job Opening Content') category = models.ForeignKey(JobCategory, verbose_name=_('category'), related_name='jobs') created = models.DateTimeField(auto_now_add=True) is_active = models.BooleanField(_('active?'), default=True) publication_start = models.DateTimeField(_('published since'), null=True, blank=True) publication_end = models.DateTimeField(_('published until'), null=True, blank=True) can_apply = models.BooleanField(_('viewer can apply for the job?'), default=True) ordering = models.IntegerField(_('ordering'), default=0) objects = JobOpeningsManager() class Meta: verbose_name = _('job opening') verbose_name_plural = _('job openings') # DO NOT attempt to add 'translated__title' here. ordering = [ 'ordering', ] def __str__(self): return self.safe_translation_getter('title', str(self.pk)) def _slug_exists(self, *args, **kwargs): """Provide additional filtering for slug generation""" qs = kwargs.get('qs', None) if qs is None: qs = self._get_slug_queryset() # limit qs to current app_config only kwargs['qs'] = qs.filter(category__app_config=self.category.app_config) return super(JobOpening, self)._slug_exists(*args, **kwargs) def get_absolute_url(self, language=None): language = language or self.get_current_language() slug = self.safe_translation_getter('slug', language_code=language) category_slug = self.category.safe_translation_getter( 'slug', language_code=language) namespace = getattr(self.category.app_config, "namespace", "aldryn_jobs") with force_language(language): try: # FIXME: does not looks correct return category url here if not slug: return self.category.get_absolute_url(language=language) kwargs = { 'category_slug': category_slug, 'job_opening_slug': slug, } return reverse('{0}:job-opening-detail'.format(namespace), kwargs=kwargs, current_app=self.category.app_config.namespace) except NoReverseMatch: # FIXME: this is wrong, if have some problem in reverse # we should know return "/%s/" % language def get_active(self): return all([ self.is_active, self.publication_start is None or self.publication_start <= now(), self.publication_end is None or self.publication_end > now() ]) def get_notification_emails(self): return self.category.get_notification_emails()
class Subnavigator(TranslatableModel): STATUS_CHOICES = ( ('odinary', 'Odinary'), ('form', 'Form'), ('call', 'Call'), ('service', 'Service'), ('example', 'Example'), ('login', 'Login'), ) translations = TranslatedFields( name=models.CharField(max_length=200, verbose_name='Название раздела', help_text='название основного раздела'), slug=models.SlugField(max_length=200, null=True, unique=True, blank=True, verbose_name='URL адрес', help_text='название url адреса'), hreflogo=models.CharField(max_length=100, blank=True, verbose_name='URL картинка', help_text='URL картинка'), alt=models.CharField(max_length=150, blank=True, verbose_name='Alt картинка', help_text='описание картинки'), title=models.CharField( max_length=300, blank=True, verbose_name='Заголовок', help_text='описание заголовка в строке браузера'), descrtionmeta=models.TextField( max_length=300, blank=True, verbose_name='Описание страницы', help_text='описание страницы в поискаовика'), keywordsmeta=models.TextField( max_length=300, blank=True, verbose_name='Ключевые слова', help_text='ключевые слова для поисковика'), ) subname = models.ForeignKey('Navconstruct', on_delete=models.CASCADE, related_name="sub", null=True, blank=True, verbose_name='Основное меню', help_text='привязка к основному меню') template_name = models.ForeignKey('Templates', on_delete=models.CASCADE, related_name="subtemp", null=True, blank=True, verbose_name='Templates', help_text='привязка к Templates') pictures = models.ForeignKey( Pictures, on_delete=models.CASCADE, null=True, blank=True, related_name="subpict", ) status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='odinary') class Meta: verbose_name = "Подраздел" verbose_name_plural = "Подменю" def get_absolute_url(self): return reverse('standart:submain', args=[self.subname.slug, self.slug]) def __str__(self): return self.name
class Post(ModelMeta, TranslatableModel): """ Blog post """ author = models.ForeignKey(dj_settings.AUTH_USER_MODEL, verbose_name=_('author'), null=True, blank=True, related_name='djangocms_blog_post_author') date_created = models.DateTimeField(_('created'), auto_now_add=True) date_modified = models.DateTimeField(_('last modified'), auto_now=True) date_published = models.DateTimeField(_('published since'), default=timezone.now) date_published_end = models.DateTimeField(_('published until'), null=True, blank=True) publish = models.BooleanField(_('publish'), default=False) categories = models.ManyToManyField('djangocms_blog.BlogCategory', verbose_name=_('category'), related_name='blog_posts', blank=True) main_image = FilerImageField(verbose_name=_('main image'), blank=True, null=True, on_delete=models.SET_NULL, related_name='djangocms_blog_post_image') main_image_thumbnail = models.ForeignKey( thumbnail_model, verbose_name=_('main image thumbnail'), related_name='djangocms_blog_post_thumbnail', on_delete=models.SET_NULL, blank=True, null=True) main_image_full = models.ForeignKey( thumbnail_model, verbose_name=_('main image full'), related_name='djangocms_blog_post_full', on_delete=models.SET_NULL, blank=True, null=True) enable_comments = models.BooleanField( verbose_name=_('enable comments on post'), default=get_setting('ENABLE_COMMENTS')) sites = models.ManyToManyField( 'sites.Site', verbose_name=_('Site(s)'), blank=True, help_text=_('Select sites in which to show the post. ' 'If none is set it will be ' 'visible in all the configured sites.')) app_config = AppHookConfigField(BlogConfig, null=True, verbose_name=_('app. config')) translations = TranslatedFields( title=models.CharField(_('title'), max_length=255), slug=models.SlugField(_('slug'), blank=True, db_index=True), abstract=HTMLField(_('abstract'), blank=True, default=''), meta_description=models.TextField( verbose_name=_('post meta description'), blank=True, default=''), meta_keywords=models.TextField(verbose_name=_('post meta keywords'), blank=True, default=''), meta_title=models.CharField( verbose_name=_('post meta title'), help_text=_('used in title tag and social sharing'), max_length=255, blank=True, default=''), post_text=HTMLField(_('text'), default='', blank=True), meta={'unique_together': (('language_code', 'slug'), )}) content = PlaceholderField('post_content', related_name='post_content') objects = GenericDateTaggedManager() tags = TaggableManager(blank=True, related_name='djangocms_blog_tags') _metadata = { 'title': 'get_title', 'description': 'get_description', 'keywords': 'get_keywords', 'og_description': 'get_description', 'twitter_description': 'get_description', 'gplus_description': 'get_description', 'locale': 'get_locale', 'image': 'get_image_full_url', 'object_type': 'get_meta_attribute', 'og_type': 'get_meta_attribute', 'og_app_id': 'get_meta_attribute', 'og_profile_id': 'get_meta_attribute', 'og_publisher': 'get_meta_attribute', 'og_author_url': 'get_meta_attribute', 'og_author': 'get_meta_attribute', 'twitter_type': 'get_meta_attribute', 'twitter_site': 'get_meta_attribute', 'twitter_author': 'get_meta_attribute', 'gplus_type': 'get_meta_attribute', 'gplus_author': 'get_meta_attribute', 'published_time': 'date_published', 'modified_time': 'date_modified', 'expiration_time': 'date_published_end', 'tag': 'get_tags', 'url': 'get_absolute_url', } class Meta: verbose_name = _('blog article') verbose_name_plural = _('blog articles') ordering = ('-date_published', '-date_created') get_latest_by = 'date_published' def __str__(self): return self.safe_translation_getter('title') def get_absolute_url(self, lang=None): if not lang: lang = get_language() category = self.categories.first() kwargs = {} urlconf = get_setting('PERMALINK_URLS')[self.app_config.url_patterns] if '<year>' in urlconf: kwargs['year'] = self.date_published.year if '<month>' in urlconf: kwargs['month'] = '%02d' % self.date_published.month if '<day>' in urlconf: kwargs['day'] = '%02d' % self.date_published.day if '<slug>' in urlconf: kwargs['slug'] = self.safe_translation_getter( 'slug', language_code=lang, any_language=True) # NOQA if '<category>' in urlconf: kwargs['category'] = category.safe_translation_getter( 'slug', language_code=lang, any_language=True) # NOQA return reverse('%s:post-detail' % self.app_config.namespace, kwargs=kwargs) def get_meta_attribute(self, param): """ Retrieves django-meta attributes from apphook config instance :param param: django-meta attribute passed as key """ attr = None value = getattr(self.app_config, param) if value: attr = getattr(self, value, None) if attr is not None: if callable(attr): try: data = attr(param) except TypeError: data = attr() else: data = attr else: data = value return data def save_translation(self, translation, *args, **kwargs): if not translation.slug and translation.title: translation.slug = slugify(translation.title) super(Post, self).save_translation(translation, *args, **kwargs) def get_title(self): title = self.safe_translation_getter('meta_title', any_language=True) if not title: title = self.safe_translation_getter('title', any_language=True) return title.strip() def get_keywords(self): return self.safe_translation_getter('meta_keywords', default='').strip().split(',') def get_locale(self): return self.get_current_language() def get_description(self): description = self.safe_translation_getter('meta_description', any_language=True) if not description: description = self.safe_translation_getter('abstract', any_language=True) return escape(strip_tags(description)).strip() def get_image_full_url(self): if self.main_image: return self.build_absolute_uri(self.main_image.url) return '' def get_tags(self): taglist = [tag.name for tag in self.tags.all()] return ','.join(taglist) def get_author(self): return self.author def _set_default_author(self, current_user): if not self.author_id and self.app_config.set_author: if get_setting('AUTHOR_DEFAULT') is True: user = current_user else: user = get_user_model().objects.get( username=get_setting('AUTHOR_DEFAULT')) self.author = user def thumbnail_options(self): if self.main_image_thumbnail_id: return self.main_image_thumbnail.as_dict else: return get_setting('IMAGE_THUMBNAIL_SIZE') def full_image_options(self): if self.main_image_full_id: return self.main_image_full.as_dict else: return get_setting('IMAGE_FULL_SIZE') def get_full_url(self): return self.build_absolute_uri(self.get_absolute_url())
class ProxyModel(ProxyBase): proxy_translations = TranslatedFields(proxy_title=models.CharField( max_length=200)) class Meta: proxy = True
class ForeignKeyTranslationModel(TranslatableModel): translations = TranslatedFields(translated_foreign=models.ForeignKey( "RegularModel", on_delete=models.CASCADE), ) shared = models.CharField(max_length=200)
class Level2(Level1): l2_translations = TranslatedFields(l2_title=models.CharField( max_length=200))
class ProxyBase(TranslatableModel): base_translations = TranslatedFields(base_title=models.CharField( max_length=200))
class Level1(TranslatableModel): l1_translations = TranslatedFields(l1_title=models.CharField( max_length=200))
class ConcreteModel(AbstractModel): translations = TranslatedFields( tr_title=models.CharField("Translated Title", max_length=200))
class Page(MPTTModel, TranslatableModel): shop = models.ForeignKey("shuup.Shop", verbose_name=_('shop')) supplier = models.ForeignKey("shuup.Supplier", null=True, blank=True, verbose_name=_('supplier')) available_from = models.DateTimeField( default=now, null=True, blank=True, db_index=True, verbose_name=_('available from'), help_text=_( "Set an available from date to restrict the page to be available only after a certain date and time. " "This is useful for pages describing sales campaigns or other time-sensitive pages." ) ) available_to = models.DateTimeField( null=True, blank=True, db_index=True, verbose_name=_('available to'), help_text=_( "Set an available to date to restrict the page to be available only after a certain date and time. " "This is useful for pages describing sales campaigns or other time-sensitive pages." ) ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+", on_delete=models.SET_NULL, verbose_name=_('created by') ) modified_by = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+", on_delete=models.SET_NULL, verbose_name=_('modified by') ) created_on = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_('created on')) modified_on = models.DateTimeField(auto_now=True, editable=False, verbose_name=_('modified on')) identifier = InternalIdentifierField( unique=False, help_text=_('This identifier can be used in templates to create URLs'), editable=True ) visible_in_menu = models.BooleanField(verbose_name=_("visible in menu"), default=False, help_text=_( "Check this if this page should have a link in the top menu of the store front." )) parent = TreeForeignKey( "self", blank=True, null=True, related_name="children", verbose_name=_("parent"), help_text=_( "Set this to a parent page if this page should be subcategorized under another page." )) list_children_on_page = models.BooleanField(verbose_name=_("list children on page"), default=False, help_text=_( "Check this if this page should list its children pages." )) show_child_timestamps = models.BooleanField(verbose_name=_("show child page timestamps"), default=True, help_text=_( "Check this if you want to show timestamps on the child pages. Please note, that this " "requires the children to be listed on the page as well." )) deleted = models.BooleanField(default=False, verbose_name=_("deleted")) translations = TranslatedFields( title=models.CharField(max_length=256, verbose_name=_('title'), help_text=_( "The page title. This is shown anywhere links to your page are shown." )), url=models.CharField( max_length=100, verbose_name=_('URL'), default=None, blank=True, null=True, help_text=_( "The page url. Choose a descriptive url so that search engines can rank your page higher. " "Often the best url is simply the page title with spaces replaced with dashes." ) ), content=models.TextField(verbose_name=_('content'), help_text=_( "The page content. This is the text that is displayed when customers click on your page link." "You can leave this empty and add all page content through placeholder editor in shop front." "To edit the style of the page you can use the Snippet plugin which is in shop front editor." )) ) template_name = models.TextField( max_length=500, verbose_name=_("Template path"), default=settings.SHUUP_SIMPLE_CMS_DEFAULT_TEMPLATE ) render_title = models.BooleanField(verbose_name=_("render title"), default=True, help_text=_( "Check this if this page should have a visible title" )) objects = TreeManager.from_queryset(PageQuerySet)() class Meta: ordering = ('-id',) verbose_name = _('page') verbose_name_plural = _('pages') unique_together = ("shop", "identifier") def delete(self, using=None): raise NotImplementedError("Not implemented: Use `soft_delete()`") def soft_delete(self, user=None): if not self.deleted: self.deleted = True self.add_log_entry("Deleted.", kind=LogEntryKind.DELETION, user=user) # Bypassing local `save()` on purpose. super(Page, self).save(update_fields=("deleted",)) def clean(self): url = getattr(self, "url", None) if url: page_translation = self._meta.model._parler_meta.root_model shop_pages = Page.objects.for_shop(self.shop).values_list("id", flat=True) url_checker = page_translation.objects.filter(url=url, master_id__in=shop_pages) if self.pk: url_checker = url_checker.exclude(master_id=self.pk) if url_checker.exists(): raise ValidationError(_("URL already exists."), code="invalid_url") def is_visible(self, dt=None): if not dt: dt = now() return ( (self.available_from and self.available_from <= dt) and (self.available_to is None or self.available_to >= dt) ) def save(self, *args, **kwargs): with reversion.create_revision(): super(Page, self).save(*args, **kwargs) def get_html(self): return self.content @classmethod def create_initial_revision(cls, page): from reversion.models import Version if not Version.objects.get_for_object(page).exists(): with reversion.create_revision(): page.save() def __str__(self): return force_text(self.safe_translation_getter("title", any_language=True, default=_("Untitled")))
class Staff(MPTTModel, TranslatableModel): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) customer_responsible = models.ForeignKey( "Customer", verbose_name=_("Customer responsible"), on_delete=models.PROTECT, null=True, default=None, blank=False, ) login_attempt_counter = models.DecimalField( _("Login attempt counter"), default=DECIMAL_ZERO, max_digits=2, decimal_places=0 ) translations = TranslatedFields( long_name=models.CharField( _("Long name"), max_length=100, db_index=True, blank=True, default=EMPTY_STRING, ), function_description=HTMLField( _("Function description"), configuration="CKEDITOR_SETTINGS_MODEL2", blank=True, default=EMPTY_STRING, ), ) is_repanier_admin = models.BooleanField(_("Repanier administrator"), default=False) is_order_manager = models.BooleanField( _("Offers in preparation manager"), default=False ) is_invoice_manager = models.BooleanField(_("Billing offers manager"), default=False) is_webmaster = models.BooleanField(_("Webmaster"), default=False) is_other_manager = models.BooleanField(_("Other responsibility"), default=False) can_be_contacted = models.BooleanField(_("Can be contacted"), default=True) password_reset_on = models.DateTimeField( _("Password reset on"), null=True, blank=True, default=None ) is_active = models.BooleanField(_("Active"), default=True) @classmethod def get_or_create_any_coordinator(cls): coordinator = ( ( cls.objects.filter( is_active=True, is_repanier_admin=True, can_be_contacted=True ) .order_by("id") .first() ) or ( cls.objects.filter(is_active=True, is_repanier_admin=True) .order_by("id") .first() ) or (cls.objects.filter(is_active=True).order_by("id").first()) or (cls.objects.order_by("id").first()) ) if coordinator is None: # Create the very first staff member from repanier.models.customer import Customer very_first_customer = Customer.get_or_create_the_very_first_customer() coordinator = Staff.objects.create( is_active=True, is_repanier_admin=True, is_webmaster=True, customer_responsible=very_first_customer, can_be_contacted=True, ) cur_language = translation.get_language() for language in settings.PARLER_LANGUAGES[settings.SITE_ID]: language_code = language["code"] translation.activate(language_code) coordinator.set_current_language(language_code) coordinator.long_name = _("Coordinator") coordinator.save() translation.activate(cur_language) return coordinator @classmethod def get_or_create_order_responsible(cls): signature = [] html_signature = [] to_email = [] order_responsible_qs = cls.objects.filter( is_active=True, is_order_manager=True ).order_by("?") if not order_responsible_qs.exists(): order_responsible = Staff.get_or_create_any_coordinator() order_responsible.is_active = True order_responsible.is_order_manager = True order_responsible.save(update_fields=["is_active", "is_order_manager"]) for order_responsible in order_responsible_qs: customer_responsible = order_responsible.customer_responsible can_be_contacted = order_responsible.can_be_contacted if customer_responsible is not None: if can_be_contacted: signature.append(order_responsible.get_str_member) html_signature.append(order_responsible.get_html_signature) to_email.extend(order_responsible.get_to_email) separator = chr(10) + " " return { "signature": separator.join(signature), "html_signature": mark_safe("<br>".join(html_signature)), "to_email": to_email, } @classmethod def get_or_create_invoice_responsible(cls): signature = [] html_signature = [] to_email = [] invoice_responsible_qs = cls.objects.filter( is_active=True, is_invoice_manager=True ).order_by("?") if not invoice_responsible_qs.exists(): invoice_responsible = Staff.get_or_create_any_coordinator() invoice_responsible.is_active = True invoice_responsible.is_order_manager = True invoice_responsible.save(update_fields=["is_active", "is_order_manager"]) for invoice_responsible in invoice_responsible_qs: customer_responsible = invoice_responsible.customer_responsible can_be_contacted = invoice_responsible.can_be_contacted if customer_responsible is not None: if can_be_contacted: signature.append(invoice_responsible.get_str_member) html_signature.append(invoice_responsible.get_html_signature) to_email.extend(invoice_responsible.get_to_email) separator = chr(10) + " " return { "signature": separator.join(signature), "html_signature": mark_safe("<br>".join(html_signature)), "to_email": to_email, } @cached_property def get_html_signature(self): function_name = self.safe_translation_getter( "long_name", any_language=True, default=EMPTY_STRING ) if self.customer_responsible is not None: customer = self.customer_responsible customer_name = customer.long_basket_name or customer.short_basket_name customer_contact_info = "{}{}".format( customer_name, customer.get_phone1(prefix=" - ") ) html_signature = mark_safe( "{}<br>{}<br>{}".format( customer_contact_info, function_name, settings.REPANIER_SETTINGS_GROUP_NAME, ) ) else: html_signature = mark_safe( "{}<br>{}".format(function_name, settings.REPANIER_SETTINGS_GROUP_NAME) ) return html_signature @cached_property def get_to_email(self): if self.customer_responsible is not None: to_email = [self.customer_responsible.user.email] else: to_email = [settings.DEFAULT_FROM_EMAIL] return to_email @cached_property def get_str_member(self): if self.customer_responsible is not None: return "{} : {}{}".format( self, self.customer_responsible.long_basket_name or self.customer_responsible, self.customer_responsible.get_phone1(prefix=" (", postfix=")"), ) else: return "{}".format(self) objects = StaffManager() def __str__(self): return self.safe_translation_getter( "long_name", any_language=True, default=EMPTY_STRING ) class Meta: verbose_name = _("Staff member") verbose_name_plural = _("Staff members")
class ManyToManyOnlyFieldsTranslationModel(TranslatableModel): translations = TranslatedFields( translated_many_to_many=models.ManyToManyField("RegularModel"), ) shared = models.CharField(max_length=200)
class Navconstruct(TranslatableModel): MENU_CHOICES = ( ('first', 'First'), ('second', 'Second'), ) STATUS_CHOICES = ( ('odinary', 'Odinary'), ('form', 'Form'), ('call', 'Call'), ('service', 'Service'), ('example', 'Example'), ('login', 'Login'), ) translations = TranslatedFields( name=models.CharField(max_length=200, verbose_name='Название раздела', help_text='название основного раздела'), slug=models.SlugField(max_length=200, null=True, unique=True, blank=True, verbose_name='URL адрес', help_text='название url адреса'), hreflogo=models.TextField(max_length=100, blank=True, verbose_name='Текст блока', help_text='текст блока'), alt=models.TextField(max_length=300, blank=True, verbose_name='Текст блока', help_text='текст блока'), title=models.CharField( max_length=300, blank=True, verbose_name='Заголовок', help_text='описание заголовка в строке браузера'), descrtionmeta=models.TextField( max_length=300, blank=True, verbose_name='Описание страницы', help_text='описание страницы в поискаовика'), keywordsmeta=models.TextField( max_length=300, blank=True, verbose_name='Ключевые слова', help_text='ключевые слова для поисковика'), ) order = models.IntegerField( null=True, blank=True, verbose_name='Порядок в меню', help_text='последователность отображения в меню') template_name = models.ForeignKey('Templates', on_delete=models.CASCADE, related_name="navtemp", null=True, blank=True, verbose_name='Templates', help_text='привязка к Templates') example = models.BooleanField(verbose_name='Наличие примера', help_text='наличие примеров в разделе', default=False) pictures = models.ForeignKey( Pictures, on_delete=models.CASCADE, null=True, blank=True, related_name="navpict", ) status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='odinary') bar = models.CharField(max_length=10, choices=MENU_CHOICES, default='first') newslug = models.ForeignKey( Templatecategory, on_delete=models.CASCADE, null=True, blank=True, related_name="newslug", verbose_name='Первое меню', ) newsslug = models.ForeignKey( Templatecategory, on_delete=models.CASCADE, null=True, blank=True, related_name="newsslug", verbose_name='Второе меню', ) site = models.ManyToManyField( Site, blank=True, related_name="site", ) class Meta: verbose_name = "Раздел" verbose_name_plural = "Основное меню" ordering = [ "order", ] def get_absolute_url(self): if self.bar == 'first': return reverse('standart:navigator', args=[self.newslug]) elif self.bar == 'second': return reverse('standart:submain', args=[self.newslug, self.newsslug]) def __str__(self): return self.name
class ManyToManyAndOtherFieldsTranslationModel(TranslatableModel): translations = TranslatedFields( tr_title=models.CharField("Translated Title", max_length=200), translated_many_to_many=models.ManyToManyField("RegularModel"), ) shared = models.CharField(max_length=200)
class TeamMember(TranslatableModel): ''' Model for members of the TEDxNTUA organizing team. The `team` attribute is represented as a CharField with limited possible values. The definition follows the official documentation example: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices ''' EXPERIENCE = 'experience' IT = 'it' FUNDRAISING = 'fundraising' GRAPHICS = 'graphics' MEDIA = 'media' SPEAKERS = 'speakers' VENUE_PRODUCTION = 'venue-production' PHOTOGRAPHY = 'photography' TEAM_CHOICES = ( (EXPERIENCE, 'Experience'), (IT, 'IT'), (FUNDRAISING, 'Fundraising'), (GRAPHICS, 'Graphics'), (MEDIA, 'Media'), (SPEAKERS, 'Speakers'), (VENUE_PRODUCTION, 'Venue & Production'), (PHOTOGRAPHY, 'Photography'), ) translations = TranslatedFields( name=models.CharField(max_length=255, default='')) email = models.EmailField(null=True, blank=True) link = models.CharField(max_length=255, default='', null=True, blank=True) team = models.CharField(max_length=16, choices=TEAM_CHOICES) image = VersatileImageField( 'Image 1', upload_to='team/', width_field='image_width', height_field='image_height', null=True, blank=True, ) image_height = models.PositiveIntegerField(editable=False, null=True) image_width = models.PositiveIntegerField(editable=False, null=True) image_alt = VersatileImageField( 'Image 2', upload_to='team/', width_field='image_alt_width', height_field='image_alt_height', null=True, blank=True, ) image_alt_height = models.PositiveIntegerField(editable=False, null=True) image_alt_width = models.PositiveIntegerField(editable=False, null=True) is_published = models.BooleanField(_('Published'), default=True) objects = TeamMemberManager() def __str__(self): ''' Objects of the TeamMember class are represented as strings by their fullname property ''' return self.name
class IntegerPrimaryKeyModel(TranslatableModel): translations = TranslatedFields( tr_title=models.CharField("Translated Title", max_length=200))
class Attribute(TranslatableModel): identifier = InternalIdentifierField(unique=True, blank=False, null=False, editable=True) searchable = models.BooleanField(default=True, verbose_name=_("searchable")) type = EnumIntegerField(AttributeType, default=AttributeType.TRANSLATED_STRING, verbose_name=_("type")) visibility_mode = EnumIntegerField( AttributeVisibility, default=AttributeVisibility.SHOW_ON_PRODUCT_PAGE, verbose_name=_("visibility mode")) translations = TranslatedFields( name=models.CharField(max_length=64, verbose_name=_("name")), ) objects = AttributeQuerySet.as_manager() class Meta: verbose_name = _('attribute') verbose_name_plural = _('attributes') def __str__(self): return u'%s' % self.name def save(self, *args, **kwargs): if not self.identifier: raise ValueError(u"Attribute with null identifier not allowed") self.identifier = flatten(("%s" % self.identifier).lower()) return super(Attribute, self).save(*args, **kwargs) def formfield(self, **kwargs): """ Get a form field for this attribute. :param kwargs: Kwargs to pass for the form field class. :return: Form field. :rtype: forms.Field """ kwargs.setdefault("required", False) kwargs.setdefault("label", self.safe_translation_getter("name", self.identifier)) if self.type == AttributeType.INTEGER: return forms.IntegerField(**kwargs) elif self.type == AttributeType.DECIMAL: return forms.DecimalField(**kwargs) elif self.type == AttributeType.BOOLEAN: return forms.NullBooleanField(**kwargs) elif self.type == AttributeType.TIMEDELTA: kwargs.setdefault("help_text", "(as seconds)") # TODO: This should be more user friendly return forms.DecimalField(**kwargs) elif self.type == AttributeType.DATETIME: return forms.DateTimeField(**kwargs) elif self.type == AttributeType.DATE: return forms.DateField(**kwargs) elif self.type == AttributeType.UNTRANSLATED_STRING: return forms.CharField(**kwargs) elif self.type == AttributeType.TRANSLATED_STRING: # Note: this isn't enough for actually saving multi-language entries; # the caller will have to deal with calling this function several # times for that. return forms.CharField(**kwargs) else: raise ValueError("`formfield` can't deal with fields of type %r" % self.type) @property def is_translated(self): return (self.type == AttributeType.TRANSLATED_STRING) @property def is_stringy(self): # Pun intended. return (self.type in ATTRIBUTE_STRING_TYPES) @property def is_numeric(self): return (self.type in ATTRIBUTE_NUMERIC_TYPES) @property def is_temporal(self): return (self.type in ATTRIBUTE_DATETIME_TYPES) def is_null_value(self, value): """ Find out whether the given value is null from this attribute's point of view. :param value: A value :type value: object :return: Nulliness boolean :rtype: bool """ if self.type == AttributeType.BOOLEAN: return (value is None) return (not value)
class UUIDPrimaryKeyModel(TranslatableModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) translations = TranslatedFields( tr_title=models.CharField("Translated Title", max_length=200))
class JobCategory(TranslatedAutoSlugifyMixin, TranslationHelperMixin, TranslatableModel): slug_source_field_name = 'name' translations = TranslatedFields( name=models.CharField(_('name'), max_length=255), slug=models.SlugField( _('slug'), max_length=255, blank=True, help_text=_('Auto-generated. Used in the URL. If changed, the URL ' 'will change. Clear it to have the slug re-created.'))) supervisors = models.ManyToManyField( get_user_model_for_fields(), verbose_name=_('supervisors'), # FIXME: This is mis-named should be "job_categories"? related_name='job_opening_categories', help_text=_('Supervisors will be notified via email when a new ' 'job application arrives.'), blank=True) app_config = models.ForeignKey(JobsConfig, null=True, verbose_name=_('app configuration'), related_name='categories') ordering = models.IntegerField(_('ordering'), default=0) objects = AppHookConfigTranslatableManager() class Meta: verbose_name = _('job category') verbose_name_plural = _('job categories') ordering = ['ordering'] def __str__(self): return self.safe_translation_getter('name', str(self.pk)) def _slug_exists(self, *args, **kwargs): """Provide additional filtering for slug generation""" qs = kwargs.get('qs', None) if qs is None: qs = self._get_slug_queryset() # limit qs to current app_config only kwargs['qs'] = qs.filter(app_config=self.app_config) return super(JobCategory, self)._slug_exists(*args, **kwargs) def get_absolute_url(self, language=None): language = language or self.get_current_language() slug = self.safe_translation_getter('slug', language_code=language) if self.app_config_id: namespace = self.app_config.namespace else: namespace = 'aldryn_jobs' with force_language(language): try: if not slug: return reverse('{0}:job-opening-list'.format(namespace)) kwargs = {'category_slug': slug} return reverse( '{0}:category-job-opening-list'.format(namespace), kwargs=kwargs, current_app=self.app_config.namespace) except NoReverseMatch: return "/%s/" % language def get_notification_emails(self): return self.supervisors.values_list('email', flat=True) # We keep this 'count' name for compatibility in templates: # there used to be annotate() call with the same property name. def count(self): return self.jobs.active().count()
class NewsBlogConfig(TranslatableModel, AppHookConfig): """Adds some translatable, per-app-instance fields.""" translations = TranslatedFields(app_title=models.CharField( _('application title'), max_length=234), ) permalink_type = models.CharField( _('permalink type'), max_length=8, blank=False, default='slug', choices=PERMALINK_CHOICES, help_text=_('Choose the style of urls to use from the examples. ' '(Note, all types are relative to apphook)')) non_permalink_handling = models.SmallIntegerField( _('non-permalink handling'), blank=False, default=302, choices=NON_PERMALINK_HANDLING, help_text=_('How to handle non-permalink urls?')) paginate_by = models.PositiveIntegerField( _('Paginate size'), blank=False, default=5, help_text=_('When paginating list views, how many articles per page?'), ) pagination_pages_start = models.PositiveIntegerField( _('Pagination pages start'), blank=False, default=10, help_text=_('When paginating list views, after how many pages ' 'should we start grouping the page numbers.'), ) pagination_pages_visible = models.PositiveIntegerField( _('Pagination pages visible'), blank=False, default=4, help_text=_('When grouping page numbers, this determines how many ' 'pages are visible on each side of the active page.'), ) template_prefix = models.CharField( max_length=20, null=True, blank=True, choices=TEMPLATE_PREFIX_CHOICES, verbose_name=_("Prefix for template dirs")) # ALDRYN_NEWSBLOG_CREATE_AUTHOR create_authors = models.BooleanField( _('Auto-create authors?'), default=True, help_text=_('Automatically create authors from logged-in user?'), ) # ALDRYN_NEWSBLOG_SEARCH search_indexed = models.BooleanField( _('Include in search index?'), default=True, help_text=_('Include articles in search indexes?'), ) placeholder_base_top = PlaceholderField( 'newsblog_base_top', related_name='aldryn_newsblog_base_top', ) placeholder_base_sidebar = PlaceholderField( 'newsblog_base_sidebar', related_name='aldryn_newsblog_base_sidebar', ) placeholder_list_top = PlaceholderField( 'newsblog_list_top', related_name='aldryn_newsblog_list_top', ) placeholder_list_footer = PlaceholderField( 'newsblog_list_footer', related_name='aldryn_newsblog_list_footer', ) placeholder_detail_top = PlaceholderField( 'newsblog_detail_top', related_name='aldryn_newsblog_detail_top', ) placeholder_detail_bottom = PlaceholderField( 'newsblog_detail_bottom', related_name='aldryn_newsblog_detail_bottom', ) placeholder_detail_footer = PlaceholderField( 'newsblog_detail_footer', related_name='aldryn_newsblog_detail_footer', ) def get_app_title(self): return getattr(self, 'app_title', _('untitled')) class Meta: verbose_name = 'config' verbose_name_plural = 'configs'
class Slide(TranslatableWshopModel): carousel = models.ForeignKey(Carousel, related_name="slides", on_delete=models.CASCADE) name = models.CharField( max_length=50, blank=True, null=True, verbose_name=_("name"), help_text=_("Name is only used to configure slides.") ) product_link = models.ForeignKey( Product, related_name="+", blank=True, null=True, verbose_name=_("product link"), help_text=_( "Set the product detail page that should be shown when this slide is clicked, if any." )) category_link = models.ForeignKey( Category, related_name="+", blank=True, null=True, verbose_name=_("category link"), help_text=_( "Set the product category page that should be shown when this slide is clicked, if any." )) cms_page_link = models.ForeignKey( Page, related_name="+", verbose_name=_("cms page link"), blank=True, null=True, help_text=_( "Set the web page that should be shown when the slide is clicked, if any." )) ordering = models.IntegerField(default=0, blank=True, null=True, verbose_name=_("ordering"), help_text=_( "Set the numeric order in which this slide should appear relative to other slides in this carousel." )) target = EnumIntegerField( LinkTargetType, default=LinkTargetType.CURRENT, verbose_name=_("link target"), help_text=_( "Set this to current if clicking on this slide should open a new browser tab." ) ) available_from = models.DateTimeField(null=True, blank=True, verbose_name=_('available from'), help_text=_( "Set the date and time from which this slide should be visible in the carousel. " "This is useful to advertise sales campaigns or other time-sensitive marketing." )) available_to = models.DateTimeField(null=True, blank=True, verbose_name=_('available to'), help_text=_( "Set the date and time from which this slide should be visible in the carousel. " "This is useful to advertise sales campaigns or other time-sensitive marketing." )) translations = TranslatedFields( caption=models.CharField( max_length=80, blank=True, null=True, verbose_name=_("caption"), help_text=_( "Text that describes the image. Used for search engine purposes." ) ), caption_text=models.TextField( blank=True, null=True, verbose_name=_("caption text"), help_text=_("When displayed in banner box mode, caption text is shown as a tooltip"), ), external_link=models.CharField( max_length=160, blank=True, null=True, verbose_name=_("external link"), help_text=_( "Set the external site that should be shown when this slide is clicked, if any." )), image=FilerImageField( blank=True, null=True, related_name="+", verbose_name=_("image"), on_delete=models.PROTECT, help_text=_( "The slide image to show." )) ) def __str__(self): return "%s %s" % (_("Slide"), self.pk) class Meta: verbose_name = _("Slide") verbose_name_plural = _("Slides") ordering = ("ordering", "id") def get_translated_field(self, attr): if not self.safe_translation_getter(attr): return self.safe_translation_getter(attr, language_code=settings.PARLER_DEFAULT_LANGUAGE_CODE) return getattr(self, attr) def get_link_url(self): """ Get right link url for this slide. Initially external link is used. If not set link will fallback to product_link, external_link or cms_page_link in this order. :return: return correct link url for slide if set :rtype: str|None """ external_link = self.get_translated_field("external_link") if external_link: return external_link elif self.product_link: return reverse("wshop:product", kwargs=dict(pk=self.product_link.pk, slug=self.product_link.slug)) elif self.category_link: return reverse("wshop:category", kwargs=dict(pk=self.category_link.pk, slug=self.category_link.slug)) elif self.cms_page_link: return reverse("wshop:cms_page", kwargs=dict(url=self.cms_page_link.url)) def is_visible(self, dt=None): """ Get slides that should be publicly visible. This does not do permission checking. :param dt: Datetime for visibility check :type dt: datetime.datetime :return: Public visibility status :rtype: bool """ if not dt: dt = now() return ( (self.available_from and self.available_from <= dt) and (self.available_to is None or self.available_to >= dt) ) def get_link_target(self): """ Return link target type string based on selection :return: Target type string :rtype: str """ if self.target == LinkTargetType.NEW: return "_blank" else: return "_self" @property def easy_thumbnails_thumbnailer(self): """ Get Thumbnailer instance for the translated image. Will return None if file cannot be thumbnailed. :rtype:easy_thumbnails.files.Thumbnailer|None """ image = self.get_translated_field("image") if not image: return try: return get_thumbnailer(image) except ValueError: return get_thumbnailer(image.filer_image_file) except: return None def get_thumbnail(self, **kwargs): """ Get thumbnail for the translated image This will return None if there is no file :rtype: easy_thumbnails.files.ThumbnailFile|None """ kwargs.setdefault("size", (self.carousel.image_width, self.carousel.image_height)) kwargs.setdefault("crop", True) # sane defaults kwargs.setdefault("upscale", True) # sane defaults if kwargs["size"] is (0, 0): return None thumbnailer = self.easy_thumbnails_thumbnailer if not thumbnailer: return None return thumbnailer.get_thumbnail(thumbnail_options=kwargs) objects = SlideQuerySet.as_manager()
class OrderStatus(TranslatableModel): identifier = InternalIdentifierField( db_index=True, blank=False, editable=True, unique=True, help_text= _("Internal identifier for status. This is used to identify the statuses in Shuup." )) ordering = models.IntegerField( db_index=True, default=0, verbose_name=_('ordering'), help_text= _("The processing order of statuses. Default is always processed first." )) role = EnumIntegerField( OrderStatusRole, db_index=True, default=OrderStatusRole.NONE, verbose_name=_('role'), help_text=_( "Role of status. One role can have multiple order statuses.")) default = models.BooleanField( default=False, db_index=True, verbose_name=_('default'), help_text=_("Defines if the status should be considered as default.")) is_active = models.BooleanField( default=True, db_index=True, verbose_name=_('is active'), help_text=_("Define if the status is usable.")) objects = OrderStatusQuerySet.as_manager() translations = TranslatedFields( name=models.CharField(verbose_name=_("name"), max_length=64, help_text=_("Name of the order status")), public_name=models.CharField( verbose_name=_('public name'), max_length=64, help_text=_("The name shown for customer in shop front."))) class Meta: unique_together = ("identifier", "role") verbose_name = _('order status') verbose_name_plural = _('order statuses') def __str__(self): return force_text( self.safe_translation_getter("name", default=self.identifier)) def save(self, *args, **kwargs): super(OrderStatus, self).save(*args, **kwargs) if self.default and self.role != OrderStatusRole.NONE: # If this status is the default, make the others for this role non-default. OrderStatus.objects.filter(role=self.role).exclude( pk=self.pk).update(default=False)
class Product(TaxableItem, AttributableMixin, TranslatableModel): COMMON_SELECT_RELATED = ("type", "primary_image", "tax_class") # Metadata created_on = models.DateTimeField(auto_now_add=True, editable=False, db_index=True, verbose_name=_('created on')) modified_on = models.DateTimeField(auto_now=True, editable=False, verbose_name=_('modified on')) deleted = models.BooleanField(default=False, editable=False, db_index=True, verbose_name=_('deleted')) # Behavior mode = EnumIntegerField(ProductMode, default=ProductMode.NORMAL, verbose_name=_('mode')) variation_parent = models.ForeignKey( "self", null=True, blank=True, related_name='variation_children', on_delete=models.PROTECT, verbose_name=_('variation parent')) stock_behavior = EnumIntegerField( StockBehavior, default=StockBehavior.UNSTOCKED, verbose_name=_('stock'), help_text=_("Set to stocked if inventory should be managed within Shuup.") ) shipping_mode = EnumIntegerField( ShippingMode, default=ShippingMode.SHIPPED, verbose_name=_('shipping mode'), help_text=_("Set to shipped if the product requires shipment.") ) sales_unit = models.ForeignKey( "SalesUnit", verbose_name=_('sales unit'), blank=True, null=True, on_delete=models.PROTECT, help_text=_( "Select a sales unit for your product. " "This is shown in your store front and is used to determine whether the product can be purchased using " "fractional amounts. Sales units are defined in Products - Sales Units." ) ) tax_class = models.ForeignKey("TaxClass", verbose_name=_('tax class'), on_delete=models.PROTECT, help_text=_( "Select a tax class for your product. " "The tax class is used to determine which taxes to apply to your product. " "Tax classes are defined in Settings - Tax Classes. " "The rules by which taxes are applied are defined in Settings - Tax Rules." ) ) # Identification type = models.ForeignKey( "ProductType", related_name='products', on_delete=models.PROTECT, db_index=True, verbose_name=_('product type'), help_text=_( "Select a product type for your product. " "These allow you to configure custom attributes to help with classification and analysis." ) ) sku = models.CharField( db_index=True, max_length=128, verbose_name=_('SKU'), unique=True, help_text=_( "Enter a SKU (Stock Keeping Unit) number for your product. " "This is a product identification code that helps you track it through your inventory. " "People often use the number by the barcode on the product, " "but you can set up any numerical system you want to keep track of products." ) ) gtin = models.CharField(blank=True, max_length=40, verbose_name=_('GTIN'), help_text=_( "You can enter a Global Trade Item Number. " "This is typically a 14 digit identification number for all of your trade items. " "It can often be found by the barcode." )) barcode = models.CharField(blank=True, max_length=40, verbose_name=_('barcode'), help_text=_( "You can enter the barcode number for your product. This is useful for inventory/stock tracking and analysis." )) accounting_identifier = models.CharField(max_length=32, blank=True, verbose_name=_('bookkeeping account')) profit_center = models.CharField(max_length=32, verbose_name=_('profit center'), blank=True) cost_center = models.CharField(max_length=32, verbose_name=_('cost center'), blank=True) # Physical dimensions width = MeasurementField( unit="mm", verbose_name=_('width (mm)'), help_text=_( "Set the measured width of your product or product packaging. " "This will provide customers with your product size and help with calculating shipping costs." ) ) height = MeasurementField( unit="mm", verbose_name=_('height (mm)'), help_text=_( "Set the measured height of your product or product packaging. " "This will provide customers with your product size and help with calculating shipping costs." ) ) depth = MeasurementField( unit="mm", verbose_name=_('depth (mm)'), help_text=_( "Set the measured depth or length of your product or product packaging. " "This will provide customers with your product size and help with calculating shipping costs." ) ) net_weight = MeasurementField( unit="g", verbose_name=_('net weight (g)'), help_text=_( "Set the measured weight of your product WITHOUT its packaging. " "This will provide customers with your product weight." ) ) gross_weight = MeasurementField( unit="g", verbose_name=_('gross weight (g)'), help_text=_( "Set the measured gross Weight of your product WITH its packaging. " "This will help with calculating shipping costs." ) ) # Misc. manufacturer = models.ForeignKey( "Manufacturer", blank=True, null=True, verbose_name=_('manufacturer'), on_delete=models.PROTECT, help_text=_( "Select a manufacturer for your product. These are defined in Products Settings - Manufacturers" ) ) primary_image = models.ForeignKey( "ProductMedia", null=True, blank=True, related_name="primary_image_for_products", on_delete=models.SET_NULL, verbose_name=_("primary image")) translations = TranslatedFields( name=models.CharField( max_length=256, verbose_name=_('name'), help_text=_("Enter a descriptive name for your product. This will be its title in your store.")), description=models.TextField( blank=True, verbose_name=_('description'), help_text=_( "To make your product stand out, give it an awesome description. " "This is what will help your shoppers learn about your products. " "It will also help shoppers find them in the store and on the web." ) ), short_description=models.CharField( max_length=150, blank=True, verbose_name=_('short description'), help_text=_( "Enter a short description for your product. " "The short description will be used to get the attention of your " "customer with a small but precise description of your product." ) ), slug=models.SlugField( verbose_name=_('slug'), max_length=255, blank=True, null=True, help_text=_( "Enter a URL Slug for your product. This is what your product page URL will be. " "A default will be created using the product name." ) ), keywords=models.TextField(blank=True, verbose_name=_('keywords'), help_text=_( "You can enter keywords that describe your product. " "This will help your shoppers learn about your products. " "It will also help shoppers find them in the store and on the web." ) ), status_text=models.CharField( max_length=128, blank=True, verbose_name=_('status text'), help_text=_( 'This text will be shown alongside the product in the shop. ' 'It is useful for informing customers of special stock numbers or preorders. ' '(Ex.: "Available in a month")' ) ), variation_name=models.CharField( max_length=128, blank=True, verbose_name=_('variation name'), help_text=_( "You can enter a name for the variation of your product. " "This could be for example different colors or versions." ) ) ) objects = ProductQuerySet.as_manager() class Meta: ordering = ('-id',) verbose_name = _('product') verbose_name_plural = _('products') def __str__(self): try: return u"%s" % self.name except ObjectDoesNotExist: return self.sku def get_shop_instance(self, shop, allow_cache=False): """ :type shop: shuup.core.models.Shop :rtype: shuup.core.models.ShopProduct """ # FIXME: Temporary removed the cache to prevent parler issues # Uncomment this as soon as https://github.com/shuup/shuup/issues/1323 is fixed # and Django Parler version is bumped with the fix # from shuup.core.utils import context_cache # key, val = context_cache.get_cached_value( # identifier="shop_product", item=self, context={"shop": shop}, allow_cache=allow_cache) # if val is not None: # return val shop_inst = self.shop_products.get(shop_id=shop.id) # context_cache.set_cached_value(key, shop_inst) return shop_inst def get_priced_children(self, context, quantity=1): """ Get child products with price infos sorted by price. :rtype: list[(Product,PriceInfo)] :return: List of products and their price infos sorted from cheapest to most expensive. """ from shuup.core.models import ShopProduct priced_children = [] shop_product_query = Q( shop=context.shop, product_id__in=self.variation_children.all().values_list("id", flat=True) ) for shop_product in ShopProduct.objects.filter(shop_product_query): if shop_product.is_orderable(supplier=None, customer=context.customer, quantity=1): child = shop_product.product priced_children.append((child, child.get_price_info(context, quantity=quantity))) return sorted(priced_children, key=(lambda x: x[1].price)) def get_cheapest_child_price(self, context, quantity=1): price_info = self.get_cheapest_child_price_info(context, quantity) if price_info: return price_info.price def get_child_price_range(self, context, quantity=1): """ Get the prices for cheapest and the most expensive child The attribute used for sorting is `PriceInfo.price`. Return (`None`, `None`) if `self.variation_children` do not exist. This is because we cannot return anything sensible. :type context: shuup.core.pricing.PricingContextable :type quantity: int :return: a tuple of prices :rtype: (shuup.core.pricing.Price, shuup.core.pricing.Price) """ items = [c.get_price_info(context, quantity=quantity) for c in self.variation_children.all()] if not items: return (None, None) infos = sorted(items, key=lambda x: x.price) return (infos[0].price, infos[-1].price) def get_cheapest_child_price_info(self, context, quantity=1): """ Get the `PriceInfo` of the cheapest variation child The attribute used for sorting is `PriceInfo.price`. Return `None` if `self.variation_children` do not exist. This is because we cannot return anything sensible. :type context: shuup.core.pricing.PricingContextable :rtype: shuup.core.pricing.PriceInfo """ items = [c.get_price_info(context, quantity=quantity) for c in self.variation_children.all()] if not items: return None return sorted(items, key=lambda x: x.price)[0] def get_price_info(self, context, quantity=1): """ Get `PriceInfo` object for the product in given context. Returned `PriceInfo` object contains calculated `price` and `base_price`. The calculation of prices is handled in the current pricing module. :type context: shuup.core.pricing.PricingContextable :rtype: shuup.core.pricing.PriceInfo """ from shuup.core.pricing import get_price_info return get_price_info(product=self, context=context, quantity=quantity) def get_price(self, context, quantity=1): """ Get price of the product within given context. .. note:: When the current pricing module implements pricing steps, it is possible that ``p.get_price(ctx) * 123`` is not equal to ``p.get_price(ctx, quantity=123)``, since there could be quantity discounts in effect, but usually they are equal. :type context: shuup.core.pricing.PricingContextable :rtype: shuup.core.pricing.Price """ return self.get_price_info(context, quantity).price def get_base_price(self, context, quantity=1): """ Get base price of the product within given context. Base price differs from the (effective) price when there are discounts in effect. :type context: shuup.core.pricing.PricingContextable :rtype: shuup.core.pricing.Price """ return self.get_price_info(context, quantity=quantity).base_price def get_available_attribute_queryset(self): if self.type_id: return self.type.attributes.visible() else: return Attribute.objects.none() def get_available_variation_results(self): """ Get a dict of `combination_hash` to product ID of variable variation results. :return: Mapping of combination hashes to product IDs :rtype: dict[str, int] """ return dict( ProductVariationResult.objects.filter(product=self).filter(status=1) .values_list("combination_hash", "result_id") ) def get_all_available_combinations(self): """ Generate all available combinations of variation variables. If the product is not a variable variation parent, the iterator is empty. Because of possible combinatorial explosion this is a generator function. (For example 6 variables with 5 options each explodes to 15,625 combinations.) :return: Iterable of combination information dicts. :rtype: Iterable[dict] """ return get_all_available_combinations(self) def clear_variation(self): """ Fully remove variation information. Make this product a non-variation parent. """ self.simplify_variation() for child in self.variation_children.all(): if child.variation_parent_id == self.pk: child.unlink_from_parent() self.verify_mode() self.save() def simplify_variation(self): """ Remove variation variables from the given variation parent, turning it into a simple variation (or a normal product, if it has no children). :param product: Variation parent to not be variable any longer. :type product: shuup.core.models.Product """ ProductVariationVariable.objects.filter(product=self).delete() ProductVariationResult.objects.filter(product=self).delete() self.verify_mode() self.save() @staticmethod def _get_slug_name(self, translation=None): if self.deleted: return None return getattr(translation, "name", self.sku) def save(self, *args, **kwargs): self.clean() if self.net_weight and self.net_weight > 0: self.gross_weight = max(self.net_weight, self.gross_weight) rv = super(Product, self).save(*args, **kwargs) generate_multilanguage_slugs(self, self._get_slug_name) return rv def clean(self): pre_clean.send(type(self), instance=self) super(Product, self).clean() post_clean.send(type(self), instance=self) def delete(self, using=None): raise NotImplementedError("Not implemented: Use `soft_delete()` for products.") def soft_delete(self, user=None): if not self.deleted: self.deleted = True self.add_log_entry("Deleted.", kind=LogEntryKind.DELETION, user=user) # Bypassing local `save()` on purpose. super(Product, self).save(update_fields=("deleted",)) def verify_mode(self): if ProductPackageLink.objects.filter(parent=self).exists(): self.mode = ProductMode.PACKAGE_PARENT self.external_url = None self.variation_children.clear() elif ProductVariationVariable.objects.filter(product=self).exists(): self.mode = ProductMode.VARIABLE_VARIATION_PARENT elif self.variation_children.exists(): if ProductVariationResult.objects.filter(product=self).exists(): self.mode = ProductMode.VARIABLE_VARIATION_PARENT else: self.mode = ProductMode.SIMPLE_VARIATION_PARENT self.external_url = None ProductPackageLink.objects.filter(parent=self).delete() elif self.variation_parent: self.mode = ProductMode.VARIATION_CHILD ProductPackageLink.objects.filter(parent=self).delete() self.variation_children.clear() self.external_url = None else: self.mode = ProductMode.NORMAL def unlink_from_parent(self): if self.variation_parent: parent = self.variation_parent self.variation_parent = None self.save() parent.verify_mode() self.verify_mode() self.save() ProductVariationResult.objects.filter(result=self).delete() return True def link_to_parent(self, parent, variables=None, combination_hash=None): """ :param parent: The parent to link to. :type parent: Product :param variables: Optional dict of {variable identifier: value identifier} for complex variable linkage :type variables: dict|None :param combination_hash: Optional combination hash (for variable variations), if precomputed. Mutually exclusive with `variables` :type combination_hash: str|None """ if combination_hash: if variables: raise ValueError("`combination_hash` and `variables` are mutually exclusive") variables = True # Simplifies the below invariant checks self._raise_if_cant_link_to_parent(parent, variables) self.unlink_from_parent() self.variation_parent = parent self.verify_mode() self.save() if not parent.is_variation_parent(): parent.verify_mode() parent.save() if variables: if not combination_hash: # No precalculated hash, need to figure that out combination_hash = get_combination_hash_from_variable_mapping(parent, variables=variables) pvr = ProductVariationResult.objects.create( product=parent, combination_hash=combination_hash, result=self ) if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT: parent.verify_mode() parent.save() return pvr else: return True def _raise_if_cant_link_to_parent(self, parent, variables): """ Validates relation possibility for `self.link_to_parent()` :param parent: parent product of self :type parent: Product :param variables: :type variables: dict|None """ if parent.is_variation_child(): raise ImpossibleProductModeException( _("Multilevel parentage hierarchies aren't supported (parent is a child already)"), code="multilevel" ) if parent.mode == ProductMode.VARIABLE_VARIATION_PARENT and not variables: raise ImpossibleProductModeException( _("Parent is a variable variation parent, yet variables were not passed"), code="no_variables" ) if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT and variables: raise ImpossibleProductModeException( "Parent is a simple variation parent, yet variables were passed", code="extra_variables" ) if self.mode == ProductMode.SIMPLE_VARIATION_PARENT: raise ImpossibleProductModeException( _("Multilevel parentage hierarchies aren't supported (this product is a simple variation parent)"), code="multilevel" ) if self.mode == ProductMode.VARIABLE_VARIATION_PARENT: raise ImpossibleProductModeException( _("Multilevel parentage hierarchies aren't supported (this product is a variable variation parent)"), code="multilevel" ) def make_package(self, package_def): if self.mode != ProductMode.NORMAL: raise ImpossibleProductModeException( _("Product is currently not a normal product, can't turn into package"), code="abnormal" ) for child_product, quantity in six.iteritems(package_def): if child_product.pk == self.pk: raise ImpossibleProductModeException(_("Package can't contain itself"), code="content") # :type child_product: Product if child_product.is_variation_parent(): raise ImpossibleProductModeException( _("Variation parents can not belong into a package"), code="abnormal" ) if child_product.is_container(): raise ImpossibleProductModeException(_("Packages can't be nested"), code="multilevel") if quantity <= 0: raise ImpossibleProductModeException(_("Quantity %s is invalid") % quantity, code="quantity") ProductPackageLink.objects.create(parent=self, child=child_product, quantity=quantity) self.verify_mode() def get_package_child_to_quantity_map(self): if self.is_container(): product_id_to_quantity = dict( ProductPackageLink.objects.filter(parent=self).values_list("child_id", "quantity") ) products = dict((p.pk, p) for p in Product.objects.filter(pk__in=product_id_to_quantity.keys())) return {products[product_id]: quantity for (product_id, quantity) in six.iteritems(product_id_to_quantity)} return {} def is_variation_parent(self): return self.mode in (ProductMode.SIMPLE_VARIATION_PARENT, ProductMode.VARIABLE_VARIATION_PARENT) def is_variation_child(self): return (self.mode == ProductMode.VARIATION_CHILD) def get_variation_siblings(self): return Product.objects.filter(variation_parent=self.variation_parent).exclude(pk=self.pk) def is_package_parent(self): return (self.mode == ProductMode.PACKAGE_PARENT) def is_subscription_parent(self): return (self.mode == ProductMode.SUBSCRIPTION) def is_package_child(self): return ProductPackageLink.objects.filter(child=self).exists() def get_all_package_parents(self): return Product.objects.filter(pk__in=( ProductPackageLink.objects.filter(child=self).values_list("parent", flat=True) )) def get_all_package_children(self): return Product.objects.filter(pk__in=( ProductPackageLink.objects.filter(parent=self).values_list("child", flat=True) )) def get_public_media(self): return self.media.filter(enabled=True, public=True).exclude(kind=ProductMediaKind.IMAGE) def is_stocked(self): return (self.stock_behavior == StockBehavior.STOCKED) def is_container(self): return (self.is_package_parent() or self.is_subscription_parent())
class SuomiFiAccessLevel(TranslatableModel): translations = TranslatedFields(name=models.CharField(max_length=100), description=models.TextField(blank=True)) shorthand = models.CharField(max_length=100, unique=True) attributes = models.ManyToManyField(SuomiFiUserAttribute)
class ShopProduct(MoneyPropped, TranslatableModel): shop = models.ForeignKey("Shop", related_name="shop_products", on_delete=models.CASCADE, verbose_name=_("shop")) product = UnsavedForeignKey("Product", related_name="shop_products", on_delete=models.CASCADE, verbose_name=_("product")) suppliers = models.ManyToManyField( "Supplier", related_name="shop_products", blank=True, verbose_name=_("suppliers"), help_text= _("List your suppliers here. Suppliers can be found in Product Settings - Suppliers." )) visibility = EnumIntegerField( ShopProductVisibility, default=ShopProductVisibility.ALWAYS_VISIBLE, db_index=True, verbose_name=_("visibility"), help_text=mark_safe_lazy( _("Select if you want your product to be seen and found by customers. " "<p>Not visible: Product will not be shown in your store front or found in search.</p>" "<p>Searchable: Product will be shown in search but not listed on any category page.</p>" "<p>Listed: Product will be shown on category pages but not shown in search results.</p>" "<p>Always Visible: Product will be shown in your store front and found in search.</p>" ))) purchasable = models.BooleanField(default=True, db_index=True, verbose_name=_("purchasable")) visibility_limit = EnumIntegerField( ProductVisibility, db_index=True, default=ProductVisibility.VISIBLE_TO_ALL, verbose_name=_('visibility limitations'), help_text= _("Select whether you want your product to have special limitations on its visibility in your store. " "You can make products visible to all, visible to only logged in users, or visible only to certain " "customer groups.")) visibility_groups = models.ManyToManyField( "ContactGroup", related_name='visible_products', verbose_name=_('visible for groups'), blank=True, help_text= _(u"Select the groups you would like to make your product visible for. " u"These groups are defined in Contacts Settings - Contact Groups.")) backorder_maximum = QuantityField( default=0, blank=True, null=True, verbose_name=_('backorder maximum'), help_text=_( "The number of units that can be purchased after the product is out of stock. " "Set to blank for product to be purchasable without limits.")) purchase_multiple = QuantityField( default=0, verbose_name=_('purchase multiple'), help_text=_( "Set this if the product needs to be purchased in multiples. " "For example, if the purchase multiple is set to 2, then customers are required to order the product " "in multiples of 2.")) minimum_purchase_quantity = QuantityField( default=1, verbose_name=_('minimum purchase'), help_text=_( "Set a minimum number of products needed to be ordered for the purchase. " "This is useful for setting bulk orders and B2B purchases.")) limit_shipping_methods = models.BooleanField( default=False, verbose_name=_("limited for shipping methods"), help_text=_( "Check this if you want to limit your product to use only select payment methods. " "You can select the payment method(s) in the field below.")) limit_payment_methods = models.BooleanField( default=False, verbose_name=_("limited for payment methods"), help_text=_( "Check this if you want to limit your product to use only select payment methods. " "You can select the payment method(s) in the field below.")) shipping_methods = models.ManyToManyField( "ShippingMethod", related_name='shipping_products', verbose_name=_('shipping methods'), blank=True, help_text=_( "Select the shipping methods you would like to limit the product to using. " "These are defined in Settings - Shipping Methods.")) payment_methods = models.ManyToManyField( "PaymentMethod", related_name='payment_products', verbose_name=_('payment methods'), blank=True, help_text=_( "Select the payment methods you would like to limit the product to using. " "These are defined in Settings - Payment Methods.")) primary_category = models.ForeignKey( "Category", related_name='primary_shop_products', verbose_name=_('primary category'), blank=True, null=True, on_delete=models.PROTECT, help_text=_( "Choose the primary category for your product. " "This will be the main category for classification in the system. " "Your product can be found under this category in your store. " "Categories are defined in Products Settings - Categories.")) categories = models.ManyToManyField( "Category", related_name='shop_products', verbose_name=_('categories'), blank=True, help_text=_( "Add secondary categories for your product. " "These are other categories that your product fits under and that it can be found by in your store." )) shop_primary_image = models.ForeignKey( "ProductMedia", null=True, blank=True, related_name="primary_image_for_shop_products", on_delete=models.SET_NULL, verbose_name=_("primary image"), help_text= _("Click this to set this image as the primary display image for your product." )) # the default price of this product in the shop default_price = PriceProperty('default_price_value', 'shop.currency', 'shop.prices_include_tax') default_price_value = MoneyValueField( verbose_name=_("default price"), null=True, blank=True, help_text=_( "This is the default individual base unit (or multi-pack) price of the product. " "All discounts or coupons will be based off of this price.")) minimum_price = PriceProperty('minimum_price_value', 'shop.currency', 'shop.prices_include_tax') minimum_price_value = MoneyValueField( verbose_name=_("minimum price"), null=True, blank=True, help_text= _("This is the default price that the product cannot go under in your store, " "despite coupons or discounts being applied. " "This is useful to make sure your product price stays above cost.")) display_unit = models.ForeignKey( DisplayUnit, null=True, blank=True, verbose_name=_("display unit"), help_text=_("Unit for displaying quantities of this product")) translations = TranslatedFields( name=models.CharField( blank=True, null=True, max_length=256, verbose_name=_('name'), help_text= _("Enter a descriptive name for your product. This will be its title in your store." )), description=models.TextField( blank=True, null=True, verbose_name=_( 'description'), help_text= _("To make your product stand out, give it an awesome description. " "This is what will help your shoppers learn about your products. " "It will also help shoppers find them in the store and on the web." )), short_description=models.CharField( blank=True, null=True, max_length=150, verbose_name=_('short description'), help_text= _("Enter a short description for your product. " "The short description will be used to get the attention of your " "customer with a small but precise description of your product." )), status_text=models.CharField( max_length=128, blank=True, verbose_name=_('status text'), help_text=_( 'This text will be shown alongside the product in the shop. ' 'It is useful for informing customers of special stock numbers or preorders. ' '(Ex.: "Available in a month")'))) class Meta: unique_together = (( "shop", "product", ), ) def save(self, *args, **kwargs): self.clean() super(ShopProduct, self).save(*args, **kwargs) for supplier in self.suppliers.enabled(): supplier.module.update_stock(product_id=self.product.id) def clean(self): pre_clean.send(type(self), instance=self) super(ShopProduct, self).clean() if self.display_unit: if self.display_unit.internal_unit != self.product.sales_unit: raise ValidationError({ 'display_unit': _("Invalid display unit: Internal unit of " "the selected display unit does not match " "with the sales unit of the product") }) post_clean.send(type(self), instance=self) def is_list_visible(self): """ Return True if this product should be visible in listings in general, without taking into account any other visibility limitations. :rtype: bool """ if self.product.deleted: return False if not self.listed: return False if self.product.is_variation_child(): return False return True @property def primary_image(self): if self.shop_primary_image_id: return self.shop_primary_image else: return self.product.primary_image @property def searchable(self): return self.visibility in (ShopProductVisibility.SEARCHABLE, ShopProductVisibility.ALWAYS_VISIBLE) @property def listed(self): return self.visibility in (ShopProductVisibility.LISTED, ShopProductVisibility.ALWAYS_VISIBLE) @property def visible(self): return not (self.visibility == ShopProductVisibility.NOT_VISIBLE) @property def public_primary_image(self): primary_image = self.primary_image return primary_image if primary_image and primary_image.public else None def get_visibility_errors(self, customer): if self.product.deleted: yield ValidationError(_('This product has been deleted.'), code="product_deleted") if customer and customer.is_all_seeing: # None of the further conditions matter for omniscient customers. return if not self.visible: yield ValidationError(_('This product is not visible.'), code="product_not_visible") is_logged_in = (bool(customer) and not customer.is_anonymous) if not is_logged_in and self.visibility_limit != ProductVisibility.VISIBLE_TO_ALL: yield ValidationError( _('The Product is invisible to users not logged in.'), code="product_not_visible_to_anonymous") if is_logged_in and self.visibility_limit == ProductVisibility.VISIBLE_TO_GROUPS: # TODO: Optimization user_groups = set(customer.groups.all().values_list("pk", flat=True)) my_groups = set(self.visibility_groups.values_list("pk", flat=True)) if not bool(user_groups & my_groups): yield ValidationError( _('This product is not visible to your group.'), code="product_not_visible_to_group") # TODO: Remove from Shuup 2.0 for receiver, response in get_visibility_errors.send( ShopProduct, shop_product=self, customer=customer): warnings.warn("Visibility errors through signals are deprecated", DeprecationWarning) for error in response: yield error def get_orderability_errors(self, supplier, quantity, customer, ignore_minimum=False): """ Yield ValidationErrors that would cause this product to not be orderable. Shop product to be orderable it needs to be visible visible and purchasable :param supplier: Supplier to order this product from. May be None. :type supplier: shuup.core.models.Supplier :param quantity: Quantity to order. :type quantity: int|Decimal :param customer: Customer contact. :type customer: shuup.core.models.Contact :param ignore_minimum: Ignore any limitations caused by quantity minimums. :type ignore_minimum: bool :return: Iterable[ValidationError] """ for error in self.get_visibility_errors(customer): yield error for error in self.get_purchasability_errors(supplier, customer, quantity, ignore_minimum): yield error def get_purchasability_errors(self, supplier, customer, quantity, ignore_minimum=False): """ Yield ValidationErrors that would cause this product to not be purchasable. Shop product to be purchasable it has to have purchasable attribute set on and pass all quantity and supplier checks. :param supplier: Supplier to order this product from. May be None. :type supplier: shuup.core.models.Supplier :param quantity: Quantity to order. :type quantity: int|Decimal :param customer: Customer contact. :type customer: shuup.core.models.Contact :param ignore_minimum: Ignore any limitations caused by quantity minimums. :type ignore_minimum: bool :return: Iterable[ValidationError] """ if not self.purchasable: yield ValidationError(_('The product is not purchasable'), code="not_purchasable") for error in self.get_quantity_errors(quantity, ignore_minimum): yield error for error in self.get_supplier_errors(supplier, customer, quantity, ignore_minimum): yield error # TODO: Remove from Shuup 2.0 for receiver, response in get_orderability_errors.send( ShopProduct, shop_product=self, customer=customer, supplier=supplier, quantity=quantity): warnings.warn("Orderability errors through signals are deprecated", DeprecationWarning) for error in response: yield error def get_quantity_errors(self, quantity, ignore_minimum): if not ignore_minimum and quantity < self.minimum_purchase_quantity: yield ValidationError(_( 'The purchase quantity needs to be at least %d for this product.' ) % self.minimum_purchase_quantity, code="purchase_quantity_not_met") purchase_multiple = self.purchase_multiple if quantity > 0 and purchase_multiple > 0 and (quantity % purchase_multiple) != 0: p = (quantity // purchase_multiple) smaller_p = max(purchase_multiple, p * purchase_multiple) larger_p = max(purchase_multiple, (p + 1) * purchase_multiple) render_qty = self.unit.render_quantity if larger_p == smaller_p: message = _("The product can only be ordered in multiples of " "{package_size}, for example {amount}").format( package_size=render_qty(purchase_multiple), amount=render_qty(smaller_p)) else: message = _("The product can only be ordered in multiples of " "{package_size}, for example {smaller_amount} or " "{larger_amount}").format( package_size=render_qty(purchase_multiple), smaller_amount=render_qty(smaller_p), larger_amount=render_qty(larger_p)) yield ValidationError(message, code="invalid_purchase_multiple") def get_supplier_errors(self, supplier, customer, quantity, ignore_minimum): enabled_supplier_pks = self.suppliers.enabled().values_list("pk", flat=True) if supplier is None and not enabled_supplier_pks: # `ShopProduct` must have at least one `Supplier`. # If supplier is not given and the `ShopProduct` itself # doesn't have suppliers we cannot sell this product. yield ValidationError(_('The product has no supplier.'), code="no_supplier") if supplier and supplier.pk not in enabled_supplier_pks: yield ValidationError(_('The product is not supplied by %s.') % supplier, code="invalid_supplier") errors = [] if self.product.mode == ProductMode.SIMPLE_VARIATION_PARENT: errors = self.get_orderability_errors_for_simple_variation_parent( supplier, customer) elif self.product.mode == ProductMode.VARIABLE_VARIATION_PARENT: errors = self.get_orderability_errors_for_variable_variation_parent( supplier, customer) elif self.product.is_package_parent(): errors = self.get_orderability_errors_for_package_parent( supplier, customer, quantity, ignore_minimum) elif supplier: # Test supplier orderability only for variation children and normal products errors = supplier.get_orderability_errors(self, quantity, customer=customer) for error in errors: yield error def get_orderability_errors_for_simple_variation_parent( self, supplier, customer): sellable = False for child_product in self.product.variation_children.all(): try: child_shop_product = child_product.get_shop_instance(self.shop) except ShopProduct.DoesNotExist: continue if child_shop_product.is_orderable( supplier=supplier, customer=customer, quantity=child_shop_product.minimum_purchase_quantity, allow_cache=False): sellable = True break if not sellable: yield ValidationError(_("Product has no sellable children"), code="no_sellable_children") def get_orderability_errors_for_variable_variation_parent( self, supplier, customer): from shuup.core.models import ProductVariationResult sellable = False for combo in self.product.get_all_available_combinations(): res = ProductVariationResult.resolve(self.product, combo["variable_to_value"]) if not res: continue try: child_shop_product = res.get_shop_instance(self.shop) except ShopProduct.DoesNotExist: continue if child_shop_product.is_orderable( supplier=supplier, customer=customer, quantity=child_shop_product.minimum_purchase_quantity, allow_cache=False): sellable = True break if not sellable: yield ValidationError(_("Product has no sellable children"), code="no_sellable_children") def get_orderability_errors_for_package_parent(self, supplier, customer, quantity, ignore_minimum): for child_product, child_quantity in six.iteritems( self.product.get_package_child_to_quantity_map()): try: child_shop_product = child_product.get_shop_instance( shop=self.shop, allow_cache=False) except ShopProduct.DoesNotExist: yield ValidationError("%s: Not available in %s" % (child_product, self.shop), code="invalid_shop") else: for error in child_shop_product.get_orderability_errors( supplier=supplier, quantity=(quantity * child_quantity), customer=customer, ignore_minimum=ignore_minimum): message = getattr(error, "message", "") code = getattr(error, "code", None) yield ValidationError("%s: %s" % (child_product, message), code=code) def raise_if_not_orderable(self, supplier, customer, quantity, ignore_minimum=False): for message in self.get_orderability_errors( supplier=supplier, quantity=quantity, customer=customer, ignore_minimum=ignore_minimum): raise ProductNotOrderableProblem(message.args[0]) def raise_if_not_visible(self, customer): for message in self.get_visibility_errors(customer=customer): raise ProductNotVisibleProblem(message.args[0]) def is_orderable(self, supplier, customer, quantity, allow_cache=True): """ Product to be orderable it needs to be visible and purchasable """ key, val = context_cache.get_cached_value( identifier="is_orderable", item=self, context={"customer": customer}, supplier=supplier, stock_managed=bool(supplier and supplier.stock_managed), quantity=quantity, allow_cache=allow_cache) if customer and val is not None: return val if not supplier: supplier = self.get_supplier(customer, quantity) for message in self.get_orderability_errors(supplier=supplier, quantity=quantity, customer=customer): if customer: context_cache.set_cached_value(key, False) return False if customer: context_cache.set_cached_value(key, True) return True def is_visible(self, customer): """ Visible products is shown in store front based on customer or customer group limitations """ for message in self.get_visibility_errors(customer=customer): return False return True def is_purchasable(self, supplier, customer, quantity): """ Whether product can be purchasable """ for message in self.get_purchasability_errors(supplier, customer, quantity): return False return True @property def quantity_step(self): """ Quantity step for purchasing this product. :rtype: decimal.Decimal Example: <input type="number" step="{{ shop_product.quantity_step }}"> """ step = self.purchase_multiple or self._sales_unit.quantity_step return self._sales_unit.round(step) @property def rounded_minimum_purchase_quantity(self): """ The minimum purchase quantity, rounded to the sales unit's precision. :rtype: decimal.Decimal Example: <input type="number" min="{{ shop_product.rounded_minimum_purchase_quantity }}" value="{{ shop_product.rounded_minimum_purchase_quantity }}"> """ return self._sales_unit.round(self.minimum_purchase_quantity) @property def display_quantity_step(self): """ Quantity step of this shop product in the display unit. Note: This can never be smaller than the display precision. """ return max(self.unit.to_display(self.quantity_step), self.unit.display_precision) @property def display_quantity_minimum(self): """ Quantity minimum of this shop product in the display unit. Note: This can never be smaller than the display precision. """ return max(self.unit.to_display(self.minimum_purchase_quantity), self.unit.display_precision) @property def unit(self): """ Unit of this product. :rtype: shuup.core.models.UnitInterface """ return UnitInterface(self._sales_unit, self.display_unit) @property def _sales_unit(self): return self.product.sales_unit or PiecesSalesUnit() @property def images(self): return self.product.media.filter( shops=self.shop, kind=ProductMediaKind.IMAGE).order_by("ordering") @property def public_images(self): return self.images.filter(public=True) def get_supplier(self, customer=None, quantity=None, shipping_address=None): supplier_strategy = cached_load( "SHUUP_SHOP_PRODUCT_SUPPLIERS_STRATEGY") kwargs = { "shop_product": self, "customer": customer, "quantity": quantity, "shipping_address": shipping_address } return supplier_strategy().get_supplier(**kwargs) def __str__(self): return self.get_name() def get_name(self): return self._safe_get_string("name") def get_description(self): return self._safe_get_string("description") def get_short_description(self): return self._safe_get_string("short_description") def _safe_get_string(self, key): return (self.safe_translation_getter(key, any_language=True) or self.product.safe_translation_getter(key, any_language=True))
class ProductMedia(TranslatableModel): identifier = InternalIdentifierField(unique=True) product = models.ForeignKey("Product", related_name="media", on_delete=models.CASCADE, verbose_name=_("product")) shops = models.ManyToManyField( "Shop", related_name="product_media", verbose_name=_("shops"), help_text=_("Select which shops you would like the product media to be visible in."), ) kind = EnumIntegerField( ProductMediaKind, db_index=True, default=ProductMediaKind.GENERIC_FILE, verbose_name=_("kind"), help_text=_( "Select what type the media is. It can either be a normal file, part of the documentation, or a sample." ), ) file = FilerFileField(blank=True, null=True, verbose_name=_("file"), on_delete=models.CASCADE) external_url = models.URLField( blank=True, null=True, verbose_name=_("URL"), help_text=_("Enter URL to external file. If this field is filled, the selected media doesn't apply."), ) ordering = models.IntegerField( default=0, verbose_name=_("ordering"), help_text=_( "You can assign numerical values to images to tell the order in which they " "shall be displayed on the product page." ), ) # Status enabled = models.BooleanField(db_index=True, default=True, verbose_name=_("enabled")) public = models.BooleanField( default=True, blank=True, verbose_name=_("public (shown on product page)"), help_text=_("Enable this if you want this image be shown on the product page. Enabled by default."), ) purchased = models.BooleanField( default=False, blank=True, verbose_name=_("purchased (shown for finished purchases)"), help_text=_("Enable this if you want the product media to be shown for completed purchases."), ) translations = TranslatedFields( title=models.CharField( blank=True, max_length=128, verbose_name=_("title"), help_text=_( "Choose a title for your product media. This will help it be found in your store and on the web." ), ), description=models.TextField( blank=True, verbose_name=_("description"), help_text=_( "Write a description for your product media. This will help it be found in your store and on the web." ), ), ) class Meta: verbose_name = _("product attachment") verbose_name_plural = _("product attachments") ordering = [ "ordering", ] def __str__(self): # pragma: no cover return self.effective_title @property def effective_title(self): title = self.safe_translation_getter("title") if title: return title if self.file_id: return self.file.label if self.external_url: return self.external_url return _("attachment") @property def url(self): if self.external_url: return self.external_url if self.file: return self.file.url return "" @property def easy_thumbnails_thumbnailer(self): """ Get `Thumbnailer` instance. Will return `None` if file cannot be thumbnailed. :rtype:easy_thumbnails.files.Thumbnailer|None """ if not self.file_id: return None if self.kind != ProductMediaKind.IMAGE: return None return get_thumbnailer(self.file) def get_thumbnail(self, **kwargs): """ Get thumbnail for image. This will return `None` if there is no file or kind is not `ProductMediaKind.IMAGE` :rtype: easy_thumbnails.files.ThumbnailFile|None """ kwargs.setdefault("size", (64, 64)) kwargs.setdefault("crop", True) # sane defaults kwargs.setdefault("upscale", True) # sane defaults if kwargs["size"] == (0, 0): return None thumbnailer = self.easy_thumbnails_thumbnailer if not thumbnailer: return None try: return thumbnailer.get_thumbnail(thumbnail_options=kwargs) except InvalidImageFormatError: return None