# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2017, Shoop Commerce Ltd. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import with_statement
from django.db import models
from django.db.models import Q
from django.db.transaction import atomic
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from enumfields import Enum, EnumIntegerField
from filer.fields.image import FilerImageField
from mptt.managers import TreeManager
from mptt.models import MPTTModel, TreeForeignKey
from mptt.querysets import TreeQuerySet
from parler.managers import TranslatableQuerySet
from parler.models import (
TranslatableManager, TranslatableModel, TranslatedFields
)
from shuup.core.fields import InternalIdentifierField
from shuup.core.signals import category_deleted
from shuup.core.utils.slugs import generate_multilanguage_slugs
from shuup.utils.analog import define_log_model, LogEntryKind
class CategoryStatus(Enum):
INVISIBLE = 0
VISIBLE = 1
DELETED = 2
class Labels:
INVISIBLE = _('invisible')
VISIBLE = _('visible')
DELETED = _('deleted')
class CategoryVisibility(Enum):
VISIBLE_TO_ALL = 1
VISIBLE_TO_LOGGED_IN = 2
VISIBLE_TO_GROUPS = 3
class Labels:
VISIBLE_TO_ALL = _('visible to all')
VISIBLE_TO_LOGGED_IN = _('visible to logged in customers')
VISIBLE_TO_GROUPS = _('visible to certain customer groups')
class CategoryQuerySet(TranslatableQuerySet, TreeQuerySet):
pass
class CategoryManager(TreeManager, TranslatableManager):
queryset_class = CategoryQuerySet
def get_queryset(self):
return self.queryset_class(self.model, using=self._db).order_by(self.tree_id_attr, self.left_attr)
def all_visible(self, customer, shop=None, language=None):
root = (self.language(language) if language else self).all()
if shop:
root = root.filter(shops=shop)
if customer and customer.is_all_seeing:
qs = root.exclude(status=CategoryStatus.DELETED)
else:
qs = root.filter(status=CategoryStatus.VISIBLE)
if customer and not customer.is_anonymous:
qs = qs.filter(
Q(visibility__in=(CategoryVisibility.VISIBLE_TO_ALL, CategoryVisibility.VISIBLE_TO_LOGGED_IN)) |
Q(visibility_groups__in=customer.groups.all())
)
else:
qs = qs.filter(visibility=CategoryVisibility.VISIBLE_TO_ALL)
return qs.distinct()
def all_except_deleted(self, language=None):
return (self.language(language) if language else self).exclude(status=CategoryStatus.DELETED)
@python_2_unicode_compatible
class Category(MPTTModel, TranslatableModel):
parent = TreeForeignKey(
'self', null=True, blank=True, related_name='children',
verbose_name=_('parent category'), on_delete=models.CASCADE, help_text=_(
"If your category is a sub-category of another category, you can link them here."
)
)
shops = models.ManyToManyField(
"Shop", blank=True, related_name="categories", verbose_name=_("shops"), help_text=_(
"You can select which shops the category is visible in."
)
)
identifier = InternalIdentifierField(unique=True)
status = EnumIntegerField(
CategoryStatus, db_index=True, verbose_name=_('status'), default=CategoryStatus.VISIBLE, help_text=_(
"Here you can choose whether or not you want the category to be visible in your store."
)
)
image = FilerImageField(verbose_name=_('image'), blank=True, null=True, on_delete=models.SET_NULL, help_text=_(
"Category image. Will be shown at theme."
))
ordering = models.IntegerField(default=0, verbose_name=_('ordering'), help_text=_(
"You can set the order of categories in your store numerically."
)
)
visibility = EnumIntegerField(
CategoryVisibility, db_index=True, default=CategoryVisibility.VISIBLE_TO_ALL,
verbose_name=_('visibility limitations'), help_text=_(
"You can choose to limit who sees your category based on whether they are logged in or if they are "
" part of a customer group."
)
)
visible_in_menu = models.BooleanField(verbose_name=_("visible in menu"), default=True, help_text=_(
"Check this if this category should be visible in menu."
))
visibility_groups = models.ManyToManyField(
"ContactGroup", blank=True, verbose_name=_('visible for groups'), related_name=u"visible_categories",
help_text=_(
"Select the customer groups you would like to be able to see the category. "
"These groups are defined in Contacts Settings - Contact Groups."
)
)
translations = TranslatedFields(
name=models.CharField(max_length=128, verbose_name=_('name'), help_text=_(
"Enter a descriptive name for your product category. "
"Products can be found in menus and in search in your store under the category name."
)
),
description=models.TextField(verbose_name=_('description'), blank=True, help_text=_(
"Give your product category a detailed description. "
"This will help shoppers find your products under that category in your store and on the web."
)
),
slug=models.SlugField(blank=True, null=True, verbose_name=_('slug'), help_text=_(
"Enter a URL slug for your category. "
"This is what your product category page URL will be. "
"A default will be created using the category name."
))
)
objects = CategoryManager()
class Meta:
ordering = ('tree_id', 'lft')
verbose_name = _('category')
verbose_name_plural = _('categories')
class MPTTMeta:
order_insertion_by = ["ordering"]
def __str__(self):
return self.safe_translation_getter("name", any_language=True)
[docs] def is_visible(self, customer):
if customer and customer.is_all_seeing:
return (self.status != CategoryStatus.DELETED)
if self.status != CategoryStatus.VISIBLE:
return False
if not customer or customer.is_anonymous:
if self.visibility != CategoryVisibility.VISIBLE_TO_ALL:
return False
else:
if self.visibility == CategoryVisibility.VISIBLE_TO_GROUPS:
group_ids = customer.groups.all().values_list("id", flat=True)
return self.visibility_groups.filter(id__in=group_ids).exists()
return True
@staticmethod
def _get_slug_name(self, translation):
if self.status == CategoryStatus.DELETED:
return None
return getattr(translation, "name", self.pk)
[docs] def delete(self, using=None):
raise NotImplementedError("Not implemented: Use `soft_delete()` for categories.")
@atomic
[docs] def soft_delete(self, user=None):
if not self.status == CategoryStatus.DELETED:
for shop_product in self.primary_shop_products.all():
shop_product.categories.remove(self)
shop_product.primary_category = None
shop_product.save()
for shop_product in self.shop_products.all():
shop_product.categories.remove(self)
shop_product.primary_category = None
shop_product.save()
for child in self.children.all():
child.parent = None
child.save()
self.status = CategoryStatus.DELETED
self.add_log_entry("Deleted.", kind=LogEntryKind.DELETION, user=user)
self.save()
category_deleted.send(sender=type(self), category=self)
[docs] def save(self, *args, **kwargs):
rv = super(Category, self).save(*args, **kwargs)
generate_multilanguage_slugs(self, self._get_slug_name)
return rv
CategoryLogEntry = define_log_model(Category)