from __future__ import unicode_literals
from future.builtins import str
from future.utils import with_metaclass
from json import loads
try:
from urllib.request import urlopen
from urllib.parse import urlencode
except ImportError:
from urllib import urlopen, urlencode
from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey
from django.db import models
from django.db.models.base import ModelBase
from django.template.defaultfilters import truncatewords_html
from django.utils.encoding import python_2_unicode_compatible
from django.utils.html import strip_tags
from django.utils.timesince import timesince
from django.utils.timezone import now
from django.utils.translation import ugettext, ugettext_lazy as _
from mezzanine.conf import settings
from mezzanine.core.fields import RichTextField, OrderField
from mezzanine.core.managers import DisplayableManager, CurrentSiteManager
from mezzanine.generic.fields import KeywordsField
from mezzanine.utils.html import TagCloser
from mezzanine.utils.models import base_concrete_model, get_user_model_name
from mezzanine.utils.sites import current_site_id, current_request
from mezzanine.utils.urls import admin_url, slugify, unique_slug
user_model_name = get_user_model_name()
def wrapped_manager(klass):
if settings.USE_MODELTRANSLATION:
from modeltranslation.manager import MultilingualManager
class Mgr(MultilingualManager, klass):
pass
return Mgr()
else:
return klass()
[docs]@python_2_unicode_compatible
class Slugged(SiteRelated):
"""
Abstract model that handles auto-generating slugs. Each slugged
object is also affiliated with a specific site object.
"""
title = models.CharField(_("Title"), max_length=500)
slug = models.CharField(_("URL"), max_length=2000, blank=True,
help_text=_("Leave blank to have the URL auto-generated from "
"the title."))
class Meta:
abstract = True
def __str__(self):
return self.title
[docs] def save(self, *args, **kwargs):
"""
If no slug is provided, generates one before saving.
"""
if not self.slug:
self.slug = self.generate_unique_slug()
super(Slugged, self).save(*args, **kwargs)
[docs] def generate_unique_slug(self):
"""
Create a unique slug by passing the result of get_slug() to
utils.urls.unique_slug, which appends an index if necessary.
"""
# For custom content types, use the ``Page`` instance for
# slug lookup.
concrete_model = base_concrete_model(Slugged, self)
slug_qs = concrete_model.objects.exclude(id=self.id)
return unique_slug(slug_qs, "slug", self.get_slug())
[docs] def get_slug(self):
"""
Allows subclasses to implement their own slug creation logic.
"""
attr = "title"
if settings.USE_MODELTRANSLATION:
from modeltranslation.utils import build_localized_fieldname
attr = build_localized_fieldname(attr, settings.LANGUAGE_CODE)
# Get self.title_xx where xx is the default language, if any.
# Get self.title otherwise.
return slugify(getattr(self, attr, None) or self.title)
def admin_link(self):
return "<a href='%s'>%s</a>" % (self.get_absolute_url(),
ugettext("View on site"))
admin_link.allow_tags = True
admin_link.short_description = ""
[docs]class TimeStamped(models.Model):
"""
Provides created and updated timestamps on models.
"""
class Meta:
abstract = True
created = models.DateTimeField(null=True, editable=False)
updated = models.DateTimeField(null=True, editable=False)
def save(self, *args, **kwargs):
_now = now()
self.updated = _now
if not self.id:
self.created = _now
super(TimeStamped, self).save(*args, **kwargs)
CONTENT_STATUS_DRAFT = 1
CONTENT_STATUS_PUBLISHED = 2
CONTENT_STATUS_CHOICES = (
(CONTENT_STATUS_DRAFT, _("Draft")),
(CONTENT_STATUS_PUBLISHED, _("Published")),
)
SHORT_URL_UNSET = "unset"
[docs]class Displayable(Slugged, MetaData, TimeStamped):
"""
Abstract model that provides features of a visible page on the
website such as publishing fields. Basis of Mezzanine pages,
blog posts, and Cartridge products.
"""
status = models.IntegerField(_("Status"),
choices=CONTENT_STATUS_CHOICES, default=CONTENT_STATUS_PUBLISHED,
help_text=_("With Draft chosen, will only be shown for admin users "
"on the site."))
publish_date = models.DateTimeField(_("Published from"),
help_text=_("With Published chosen, won't be shown until this time"),
blank=True, null=True, db_index=True)
expiry_date = models.DateTimeField(_("Expires on"),
help_text=_("With Published chosen, won't be shown after this time"),
blank=True, null=True)
short_url = models.URLField(blank=True, null=True)
in_sitemap = models.BooleanField(_("Show in sitemap"), default=True)
objects = wrapped_manager(DisplayableManager)
search_fields = {"keywords": 10, "title": 5}
class Meta:
abstract = True
[docs] def save(self, *args, **kwargs):
"""
Set default for ``publish_date``. We can't use ``auto_now_add`` on
the field as it will be blank when a blog post is created from
the quick blog form in the admin dashboard.
"""
if self.publish_date is None:
self.publish_date = now()
super(Displayable, self).save(*args, **kwargs)
def get_admin_url(self):
return admin_url(self, "change", self.id)
[docs] def publish_date_since(self):
"""
Returns the time since ``publish_date``.
"""
return timesince(self.publish_date)
publish_date_since.short_description = _("Published from")
[docs] def published(self):
"""
For non-staff users, return True when status is published and
the publish and expiry dates fall before and after the
current date when specified.
"""
return (self.status == CONTENT_STATUS_PUBLISHED and
(self.publish_date is None or self.publish_date <= now()) and
(self.expiry_date is None or self.expiry_date >= now()))
[docs] def get_absolute_url(self):
"""
Raise an error if called on a subclass without
``get_absolute_url`` defined, to ensure all search results
contains a URL.
"""
name = self.__class__.__name__
raise NotImplementedError("The model %s does not have "
"get_absolute_url defined" % name)
[docs] def get_absolute_url_with_host(self):
"""
Returns host + ``get_absolute_url`` - used by the various
``short_url`` mechanics below.
Technically we should use ``self.site.domain``, here, however
if we were to invoke the ``short_url`` mechanics on a list of
data (eg blog post list view), we'd trigger a db query per
item. Using ``current_request`` should provide the same
result, since site related data should only be loaded based
on the current host anyway.
"""
return current_request().build_absolute_uri(self.get_absolute_url())
[docs] def set_short_url(self):
"""
Generates the ``short_url`` attribute if the model does not
already have one. Used by the ``set_short_url_for`` template
tag and ``TweetableAdmin``.
If no sharing service is defined (bitly is the one implemented,
but others could be by overriding ``generate_short_url``), the
``SHORT_URL_UNSET`` marker gets stored in the DB. In this case,
``short_url`` is temporarily (eg not persisted) set to
host + ``get_absolute_url`` - this is so that we don't
permanently store ``get_absolute_url``, since it may change
over time.
"""
if not self.short_url or self.short_url == SHORT_URL_UNSET:
self.short_url = self.generate_short_url()
self.save()
if self.short_url == SHORT_URL_UNSET:
self.short_url = self.get_absolute_url_with_host()
[docs] def generate_short_url(self):
"""
Returns a new short URL generated using bit.ly if credentials for the
service have been specified.
"""
from mezzanine.conf import settings
if settings.BITLY_ACCESS_TOKEN:
url = "https://api-ssl.bit.ly/v3/shorten?%s" % urlencode({
"access_token": settings.BITLY_ACCESS_TOKEN,
"uri": self.get_absolute_url_with_host(),
})
response = loads(urlopen(url).read().decode("utf-8"))
if response["status_code"] == 200:
return response["data"]["url"]
return SHORT_URL_UNSET
def _get_next_or_previous_by_publish_date(self, is_next, **kwargs):
"""
Retrieves next or previous object by publish date. We implement
our own version instead of Django's so we can hook into the
published manager and concrete subclasses.
"""
arg = "publish_date__gt" if is_next else "publish_date__lt"
order = "publish_date" if is_next else "-publish_date"
lookup = {arg: self.publish_date}
concrete_model = base_concrete_model(Displayable, self)
try:
queryset = concrete_model.objects.published
except AttributeError:
queryset = concrete_model.objects.all
try:
return queryset(**kwargs).filter(**lookup).order_by(order)[0]
except IndexError:
pass
[docs] def get_next_by_publish_date(self, **kwargs):
"""
Retrieves next object by publish date.
"""
return self._get_next_or_previous_by_publish_date(True, **kwargs)
[docs] def get_previous_by_publish_date(self, **kwargs):
"""
Retrieves previous object by publish date.
"""
return self._get_next_or_previous_by_publish_date(False, **kwargs)
[docs]class RichText(models.Model):
"""
Provides a Rich Text field for managing general content and making
it searchable.
"""
content = RichTextField(_("Content"))
search_fields = ("content",)
class Meta:
abstract = True
[docs]class OrderableBase(ModelBase):
"""
Checks for ``order_with_respect_to`` on the model's inner ``Meta``
class and if found, copies it to a custom attribute and deletes it
since it will cause errors when used with ``ForeignKey("self")``.
Also creates the ``ordering`` attribute on the ``Meta`` class if
not yet provided.
"""
def __new__(cls, name, bases, attrs):
if "Meta" not in attrs:
class Meta:
pass
attrs["Meta"] = Meta
if hasattr(attrs["Meta"], "order_with_respect_to"):
order_field = attrs["Meta"].order_with_respect_to
attrs["order_with_respect_to"] = order_field
del attrs["Meta"].order_with_respect_to
if not hasattr(attrs["Meta"], "ordering"):
setattr(attrs["Meta"], "ordering", ("_order",))
return super(OrderableBase, cls).__new__(cls, name, bases, attrs)
[docs]class Orderable(with_metaclass(OrderableBase, models.Model)):
"""
Abstract model that provides a custom ordering integer field
similar to using Meta's ``order_with_respect_to``, since to
date (Django 1.2) this doesn't work with ``ForeignKey("self")``,
or with Generic Relations. We may also want this feature for
models that aren't ordered with respect to a particular field.
"""
_order = OrderField(_("Order"), null=True)
class Meta:
abstract = True
[docs] def with_respect_to(self):
"""
Returns a dict to use as a filter for ordering operations
containing the original ``Meta.order_with_respect_to`` value
if provided. If the field is a Generic Relation, the dict
returned contains names and values for looking up the
relation's ``ct_field`` and ``fk_field`` attributes.
"""
try:
name = self.order_with_respect_to
value = getattr(self, name)
except AttributeError:
# No ``order_with_respect_to`` specified on the model.
return {}
# Support for generic relations.
field = getattr(self.__class__, name)
if isinstance(field, GenericForeignKey):
names = (field.ct_field, field.fk_field)
return dict([(n, getattr(self, n)) for n in names])
return {name: value}
[docs] def save(self, *args, **kwargs):
"""
Set the initial ordering value.
"""
if self._order is None:
lookup = self.with_respect_to()
lookup["_order__isnull"] = False
concrete_model = base_concrete_model(Orderable, self)
self._order = concrete_model.objects.filter(**lookup).count()
super(Orderable, self).save(*args, **kwargs)
[docs] def delete(self, *args, **kwargs):
"""
Update the ordering values for siblings.
"""
lookup = self.with_respect_to()
lookup["_order__gte"] = self._order
concrete_model = base_concrete_model(Orderable, self)
after = concrete_model.objects.filter(**lookup)
after.update(_order=models.F("_order") - 1)
super(Orderable, self).delete(*args, **kwargs)
def _get_next_or_previous_by_order(self, is_next, **kwargs):
"""
Retrieves next or previous object by order. We implement our
own version instead of Django's so we can hook into the
published manager, concrete subclasses and our custom
``with_respect_to`` method.
"""
lookup = self.with_respect_to()
lookup["_order"] = self._order + (1 if is_next else -1)
concrete_model = base_concrete_model(Orderable, self)
try:
queryset = concrete_model.objects.published
except AttributeError:
queryset = concrete_model.objects.filter
try:
return queryset(**kwargs).get(**lookup)
except concrete_model.DoesNotExist:
pass
[docs] def get_next_by_order(self, **kwargs):
"""
Retrieves next object by order.
"""
return self._get_next_or_previous_by_order(True, **kwargs)
[docs] def get_previous_by_order(self, **kwargs):
"""
Retrieves previous object by order.
"""
return self._get_next_or_previous_by_order(False, **kwargs)
[docs]class Ownable(models.Model):
"""
Abstract model that provides ownership of an object for a user.
"""
user = models.ForeignKey(user_model_name, on_delete=models.CASCADE,
verbose_name=_("Author"), related_name="%(class)ss")
class Meta:
abstract = True
[docs] def is_editable(self, request):
"""
Restrict in-line editing to the objects's owner and superusers.
"""
return request.user.is_superuser or request.user.id == self.user_id
[docs]class ContentTyped(models.Model):
"""
Mixin for models that can be subclassed to create custom types.
In order to use them:
- Inherit model from ContentTyped.
- Call the set_content_model() method in the model's save() method.
- Inherit that model's ModelAdmin from ContentTypesAdmin.
- Include "admin/includes/content_typed_change_list.html" in the
change_list.html template.
"""
content_model = models.CharField(editable=False, max_length=50, null=True)
class Meta:
abstract = True
[docs] @classmethod
def get_content_model_name(cls):
"""
Return the name of the OneToOneField django automatically creates for
child classes in multi-table inheritance.
"""
return cls._meta.object_name.lower()
[docs] @classmethod
def get_content_models(cls):
""" Return all subclasses of the concrete model. """
concrete_model = base_concrete_model(ContentTyped, cls)
return [m for m in apps.get_models()
if m is not concrete_model and issubclass(m, concrete_model)]
[docs] def set_content_model(self):
"""
Set content_model to the child class's related name, or None if this is
the base class.
"""
if not self.content_model:
is_base_class = (
base_concrete_model(ContentTyped, self) == self.__class__)
self.content_model = (
None if is_base_class else self.get_content_model_name())
[docs] def get_content_model(self):
"""
Return content model, or if this is the base class return it.
"""
return (getattr(self, self.content_model) if self.content_model
else self)
[docs]class SitePermission(models.Model):
"""
Permission relationship between a user and a site that's
used instead of ``User.is_staff``, for admin and inline-editing
access.
"""
user = models.OneToOneField(user_model_name, on_delete=models.CASCADE,
verbose_name=_("Author"), related_name="%(class)ss")
sites = models.ManyToManyField("sites.Site", blank=True,
verbose_name=_("Sites"))
class Meta:
verbose_name = _("Site permission")
verbose_name_plural = _("Site permissions")