# -*- coding: utf-8 -*- from __future__ import unicode_literals import sys from datetime import datetime from django.core.urlresolvers import reverse from django.db import models from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import python_2_unicode_compatible from django.utils import six from django.utils.functional import lazy from django.utils.safestring import mark_safe mark_safe_lazy = lazy(mark_safe, six.text_type) newsbox_models = [] def newsbox_mcls_processor(mcls, class_name, class_bases, class_attrs, base, newsbox_opts): if 'newsbox_title' not in class_attrs: class_attrs['newsbox_title'] = models.CharField( verbose_name=_('title'), help_text=_('The title should not be too long and should be explicit'), max_length=255) newsbox_opts['trans_fieldnames'].append('newsbox_title') if 'newsbox_slug' not in class_attrs: class_attrs['newsbox_slug'] = models.SlugField( verbose_name=_('slug'), help_text=_('The part of the title that is used in the URL')) newsbox_opts['trans_fieldnames'].append('newsbox_slug') if 'newsbox_summary' not in class_attrs: class_attrs['newsbox_summary'] = models.TextField( verbose_name=_('summary'), help_text=_('This text must be short, it will be displayed in the lists.')) newsbox_opts['trans_fieldnames'].append('newsbox_summary') if 'newsbox_body' not in class_attrs: class_attrs['newsbox_body'] = models.TextField( verbose_name=_('body'), null=False, blank=True) newsbox_opts['trans_fieldnames'].append('newsbox_body') def newsboxseo_mcls_processor(mcls, class_name, class_bases, class_attrs, base, newsbox_opts): class_attrs['newsbox_meta_description'] = models.TextField( verbose_name=_('meta description'), help_text=mark_safe_lazy( _('The meta-description allow to improve your referencing and indicates to ' 'search engines a summary of your page. To draft her better, you can consult ' '%(help_link)sthis help page%(link_end)s.') % { 'help_link': _( # Translators: This is the "help_link" URL for the meta-description help '%(start_tag)s' 'https://support.google.com/webmasters/answer/35624?rd=1&hl=en#1' '%(end_tag)s') % {'start_tag': '<a target="_blank" href="', 'end_tag': '">'}, 'link_end': '</a>'} ), max_length=255, blank=True, null=True) class_attrs['newsbox_meta_keywords'] = models.CharField( verbose_name=_('meta keywords'), help_text=_('List of few keywords (4-5) separated by commas. These words must be relevant ' 'with the contents of your page : they must be present in the contents of the ' 'page where they should be put in \"bold\" to highlight their importance. ' 'Also try to place them in the title of your page. Note that even if Google ' 'does not use this meta anymore, other search engines still use these, like ' 'Yandex, the most used search engine in Russia.'), max_length=255, blank=True, null=True) class_attrs['newsbox_canonical_url'] = models.URLField( verbose_name=_('canonical URL'), help_text=mark_safe_lazy( _('This field allows you to indicate to search engines the "official" URL of a ' 'duplicated contents. If your contents come from another page (of your website ' 'or another one), then you should indicate the URL of the "source" to avoid ' '"duplicated contents" penalization of search engines. You can consult ' '%(help_link_1)sthe moz dossier%(end_link)s on this matter or ' '%(help_link_2)sGoogle help%(end_link)s.') % { 'help_link_1': _( # Translators: This is the "help_link_1" URL for the canonical URL help '%(start_tag)s' 'https://moz.com/blog/canonical-url-tag-the-most-important-advancement-in-seo-practices-since-sitemaps' # noqa '%(end_tag)s') % {'start_tag': '<a target="_blank" href="', 'end_tag': '">'}, 'help_link_2': _( # Translators: This is the "help_link_2" URL for the canonical URL help '%(start_tag)s' 'https://support.google.com/webmasters/answer/139066?hl=en#2' '%(end_tag)s') % {'start_tag': '<a target="_blank" href="', 'end_tag': '">'}, 'end_link': '</a>'} ), null=True, blank=True,) newsbox_opts['trans_fieldnames'] += [ 'newsbox_meta_description', 'newsbox_meta_keywords', 'newsbox_canonical_url'] ################################################################################################### ################################################################################################### class NewsboxManager(models.Manager): """ Filter published news """ def published(self): q = self.model.q_published() return super(NewsboxManager, self).get_queryset().filter(q) ################################################################################################### ################################################################################################### class NewsboxModelBase(models.base.ModelBase): """ Metaclass used to inject django-newsboxfunctionalities into django orm. """ def __new__(mcls, class_name, class_bases, class_attrs): super_new = super(NewsboxModelBase, mcls).__new__ # six.with_metaclass() inserts an extra class called 'NewBase' in the # inheritance tree: Model -> NewBase -> object. But the initialization # should be executed only once for a given model class. # attrs will never be empty for classes declared in the standard way # (ie. with the `class` keyword). This is quite robust. if class_name == 'NewBase' and class_attrs == {}: return super_new(mcls, class_name, class_bases, class_attrs) # Also ensure initialization is only performed for subclasses of Model # (excluding Model class itself). # parents = [b for b in class_bases # if isinstance(b, NewsboxModelBase) and # not (b.__name__ == 'NewBase' and b.__mro__ == (b, object))] # retrieve newsbox options in Meta class and delete these from the # Meta class to avoid django complain about unsupported keys. # We register our meta options in `_newsbox_meta` attr_meta = class_attrs.get('Meta', None) if attr_meta and not getattr(attr_meta, 'ordering', None): class_attrs['Meta'].ordering = ('-newsbox_publication_start_date',) abstract = getattr(attr_meta, 'abstract', False) newsbox_opts = {} newsbox_opts_keys = [ 'detail_url_name', 'list_url_name', 'metaclass_base_processor', 'metaclass_final_processor', 'trans_fieldnames'] for opt in newsbox_opts_keys: newsbox_opt = 'newsbox_' + opt if hasattr(attr_meta, newsbox_opt): newsbox_opts[opt] = getattr(attr_meta, newsbox_opt) delattr(attr_meta, newsbox_opt) if 'trans_fieldnames' not in newsbox_opts: newsbox_opts['trans_fieldnames'] = [] # register newsbox_objects manager class_attrs['newsbox_objects'] = NewsboxManager() # Call specific metaclass processor of all bases if not abstract: for base in class_bases: try: processor = base._newsbox_meta['metaclass_base_processor'] except (AttributeError, KeyError): continue processor = getattr(sys.modules[base.__module__], processor) processor(mcls, class_name, class_bases, class_attrs, base, newsbox_opts) for base in class_bases: try: processor = base._newsbox_meta['metaclass_final_processor'] except (AttributeError, KeyError): continue processor = getattr(sys.modules[base.__module__], processor) processor(mcls, class_name, class_bases, class_attrs, base, newsbox_opts) if 'default_manager' not in class_attrs: class_attrs['default_manager'] = class_attrs['newsbox_objects'] if 'objects' not in class_attrs: class_attrs['objects'] = class_attrs['newsbox_objects'] # contructs class cls = super_new(mcls, class_name, class_bases, class_attrs) # Add the detail_url_name to _newsbox_meta. if not defined, we build the # default one with this format : <app_label>:<model_name>_detail # Names ar lowered. if not abstract: if 'detail_url_name' not in newsbox_opts: newsbox_opts['detail_url_name'] = '%s:%s_detail' % (cls._meta.app_label.lower(), cls._meta.model_name.lower()) if 'list_url_name' not in newsbox_opts: newsbox_opts['list_url_name'] = '%s:%s_list' % (cls._meta.app_label.lower(), cls._meta.model_name.lower()) newsbox_models.append(cls) cls.add_to_class('_newsbox_meta', newsbox_opts) return cls ################################################################################################### ################################################################################################### @python_2_unicode_compatible class NewsboxBase(six.with_metaclass(NewsboxModelBase, models.Model)): """ Abstract class to build your own news """ STATUS_DRAFT = 'draft' STATUS_PUBLISHED = 'published' STATUS_ARCHIVED = 'archived' STATUS_CHOICES = ( (STATUS_DRAFT, _('draft')), (STATUS_PUBLISHED, _('published')), (STATUS_ARCHIVED, _('archived')), ) # Automatic fields newsbox_creation_date = models.DateTimeField( verbose_name=_('creation date'), auto_now_add=True, editable=False) newsbox_changed_date = models.DateTimeField( verbose_name=_('last update date'), auto_now=True, editable=False) # event datetime newsbox_date = models.DateTimeField( verbose_name=_('datetime'), help_text=_('Datetime of the event related to this news'), default=None, db_index=False, null=True, blank=True, ) # Basic Publication Fields newsbox_publication_start_date = models.DateTimeField( verbose_name=_('publication start date'), default=datetime.now, db_index=True, help_text=_( 'When the news should go live. ' 'Status must be "Published" for news to go live.'), null=False, blank=False) newsbox_status = models.CharField( verbose_name=_('status'), max_length=10, choices=STATUS_CHOICES, default=STATUS_DRAFT, help_text=_('Use "Draft" to prepare your content before publishing and "Archived" to ' 'unpublish content without deleting it')) default_manager = NewsboxManager() objects = default_manager class Meta: abstract = True verbose_name = _('news') verbose_name_plural = _('news') ordering = ('-newsbox_publication_start_date', ) newsbox_metaclass_base_processor = 'newsbox_mcls_processor' permissions = ( ('publish', _('Can publish')), ) @classmethod def q_published(cls): # Only published News q = models.Q(newsbox_status=cls.STATUS_PUBLISHED) # exclude news with publication start date in the future q &= models.Q(newsbox_publication_start_date__lte=now()) return q @classmethod def _has_perm(cls, action, user, obj=None): if user.is_superuser: return True return user.has_perm('%s.%s_%s' % (cls._meta.app_label, action, cls._meta.model_name), obj) @classmethod def has_publish_permission(cls, user, obj=None): return cls._has_perm('publish', user, obj) or cls._has_perm('change', user, obj) @classmethod def has_change_permission(cls, user, obj=None): return cls._has_perm('change', user, obj) def __str__(self): return six.text_type(self.newsbox_title) @property def is_published(self): if self.newsbox_status != self.STATUS_PUBLISHED: return False return now() >= self.newsbox_publication_start_date def publish(self, commit=True): updated = False if self.newsbox_status != self.STATUS_PUBLISHED: self.newsbox_status = self.STATUS_PUBLISHED updated = True if self.newsbox_publication_start_date: now_datetime = now() if now_datetime < self.newsbox_publication_start_date: self.newsbox_publication_start_date = now_datetime updated = True if updated and commit: self.save() return updated def unpublish(self, commit=True): updated = False if self.newsbox_status == self.STATUS_PUBLISHED: self.newsbox_status = self.STATUS_ARCHIVED updated = True if updated and commit: self.save() return updated def get_slug(self, *args, **kwargs): return self.newsbox_slug def get_absolute_url(self, *args, **kwargs): return reverse(self._newsbox_meta['detail_url_name'], args=( self.newsbox_publication_start_date.strftime("%Y"), self.newsbox_publication_start_date.strftime("%m"), self.newsbox_publication_start_date.strftime("%d"), self.get_slug(),)) def save(self, *args, **kwargs): # be sure to have a publication start date if not self.newsbox_publication_start_date: self.newsbox_publication_start_date = self.newsbox_creation_date super(NewsboxBase, self).save(*args, **kwargs) ################################################################################################### ################################################################################################### class NewsboxExpiredBase(six.with_metaclass(NewsboxModelBase, models.Model)): """ Define News which expires in time and will not be displayed in front """ newsbox_publication_end_date = models.DateTimeField( verbose_name=_('publication end date'), null=True, blank=True, db_index=True, help_text=_('When the news will expire. Leave empty to never expire.')) class Meta: abstract = True @classmethod def q_published(cls): q = super(NewsboxExpiredBase, cls).q_published() # exclude news which are obsolete q &= (models.Q(newsbox_publication_end_date__gt=now()) | models.Q(newsbox_publication_end_date__isnull=True)) return q @property def is_published(self): if not super(NewsboxExpiredBase, self).is_published: return False return not self.newsbox_publication_end_date or now() <= self.newsbox_publication_end_date def publish(self, commit=True): updated = super(NewsboxExpiredBase, self).publish(commit=False) if self.newsbox_publication_end_date: if now() > self.newsbox_publication_end_date: self.newsbox_publication_end_date = None updated = True if updated and commit: self.save() return updated ################################################################################################### ################################################################################################### class NewsboxSEOBase(six.with_metaclass(NewsboxModelBase, models.Model)): """ Define News which have SEO fields """ # SEO Fields newsbox_indexed = models.PositiveSmallIntegerField( verbose_name=_('indexed'), default=1, choices=((0, _('Not indexed on search engines'), ), (1, _('Indexed on search engines'), )), help_text=_('An unindexed news will not be indexed by search engines.')) class Meta: abstract = True newsbox_metaclass_base_processor = 'newsboxseo_mcls_processor' def get_meta_informations(self): if not hasattr(self, '_meta_informations'): self._meta_informations = { 'description': self.newsbox_meta_description, 'keywords': self.newsbox_meta_keywords, 'robots': [], 'title': '%s' % self, } if not self.newsbox_indexed: self._meta_informations['robots'].append('noindex') if self.newsbox_meta_description: self._meta_informations['robots'].append('noodp') return self._meta_informations