コード例 #1
0
class BasePeoplePlugin(CMSPlugin):

    STYLE_CHOICES = [('standard', _('Standard')),
                     ('feature', _('Feature'))] + get_additional_styles()

    style = models.CharField(_('Style'),
                             choices=STYLE_CHOICES,
                             default=STYLE_CHOICES[0][0],
                             max_length=50)

    people = SortedM2MModelField(
        Person,
        blank=True,
        help_text=_('Select and arrange specific people, or, leave blank to '
                    'select all.'))

    groups = SortedM2MModelField(
        Group,
        blank=True,
        help_text=_('Select and arrange specific groups, or, leave blank to '
                    'select specific people.'))

    # Add an app namespace to related_name to avoid field name clashes
    # with any other plugins that have a field with the same name as the
    # lowercase of the class name of this model.
    # https://github.com/divio/django-cms/issues/5030
    if LTE_DJANGO_1_6:
        # related_name='%(app_label)s_%(class)s' does not work on  Django 1.6
        cmsplugin_ptr = models.OneToOneField(
            CMSPlugin,
            related_name='+',
            parent_link=True,
        )
    else:
        cmsplugin_ptr = models.OneToOneField(
            CMSPlugin,
            related_name='%(app_label)s_%(class)s',
            parent_link=True,
        )

    class Meta:
        abstract = True

    def copy_relations(self, oldinstance):
        self.people = oldinstance.people.all()
        self.groups = oldinstance.groups.all()

    def get_selected_groups(self):
        return self.groups.select_related()

    def get_selected_people(self):
        return self.people.select_related('visual')

    def __str__(self):
        return text_type(self.pk)
コード例 #2
0
ファイル: models.py プロジェクト: simonkern/aldryn-people
class BasePeoplePlugin(CMSPlugin):

    STYLE_CHOICES = [
        ('standard', _('Standard')),
        ('feature', _('Feature'))
    ] + get_additional_styles()

    style = models.CharField(
        _('Style'), choices=STYLE_CHOICES,
        default=STYLE_CHOICES[0][0], max_length=50)

    people = SortedM2MModelField(
        Person, blank=True,
        help_text=_('Select and arrange specific people, or, leave blank to '
                    'select all.')
    )

    class Meta:
        abstract = True

    def copy_relations(self, oldinstance):
        self.people = oldinstance.people.all()

    def get_selected_people(self):
        return self.people.select_related('visual')

    def __str__(self):
        return text_type(self.pk)
コード例 #3
0
class CookieConsentPlugin(CMSPlugin):
    groups = SortedM2MModelField(
        CookieGroup,
        blank=True,
        help_text=_(
            'Select and arrange specific cookie groups, or, leave blank to '
            'select all.'))

    def __str__(self):
        return 'Cookie consent plugin'

    def copy_relations(self, oldinstance):
        self.groups = oldinstance.groups.all()
コード例 #4
0
class BaseDoctorsPlugin(CMSPlugin):

    STYLE_CHOICES = [
        ('standard', _('Standard')),
        # ('feature', _('Feature')),
        ('carousel', _('Carousel')),
    ] + get_additional_styles()

    style = models.CharField(_('Style'),
                             choices=STYLE_CHOICES,
                             default=STYLE_CHOICES[0][0],
                             max_length=50)

    doctors = SortedM2MModelField(
        Doctor,
        blank=True,
        help_text=_('Select and arrange specific doctors, or, leave blank to '
                    'select all.'))

    # Add an app namespace to related_name to avoid field name clashes
    # with any other plugins that have a field with the same name as the
    # lowercase of the class name of this model.
    # https://github.com/divio/django-cms/issues/5030
    cmsplugin_ptr = models.OneToOneField(
        CMSPlugin,
        related_name='%(app_label)s_%(class)s',
        parent_link=True,
        on_delete=models.CASCADE,
    )

    class Meta:
        abstract = True

    def copy_relations(self, oldinstance):
        self.doctors.set(oldinstance.doctors.all())

    def get_selected_doctors(self):
        return self.doctors.select_related('visual')

    def __str__(self):
        return text_type(self.pk)
コード例 #5
0
ファイル: models.py プロジェクト: simonkern/aldryn-people
class Person(TranslationHelperMixin, TranslatedAutoSlugifyMixin,
             TranslatableModel):
    slug_source_field_name = 'name'

    translations = TranslatedFields(
        name=models.CharField(_('name'), max_length=255, blank=False,
            default='', help_text=_("Provide this person's name.")),
        slug=models.SlugField(_('unique slug'), max_length=255, blank=True,
            default='',
            help_text=_("Leave blank to auto-generate a unique slug.")),
        function=models.CharField(_('role'),
            max_length=255, blank=True, default=''),
        description=HTMLField(_('description'),
            blank=True, default='')
    )
    phone = models.CharField(
        verbose_name=_('phone'), null=True, blank=True, max_length=100)
    mobile = models.CharField(
        verbose_name=_('mobile'), null=True, blank=True, max_length=100)
    fax = models.CharField(
        verbose_name=_('fax'), null=True, blank=True, max_length=100)
    email = models.EmailField(
        verbose_name=_("email"), blank=True, default='')
    website = models.URLField(
        verbose_name=_('website'), null=True, blank=True)
    groups = SortedM2MModelField(
        'aldryn_people.Group', default=None, blank=True, related_name='people',
        help_text=_('Choose and order the groups for this person, the first '
                    'will be the "primary group".'))
    visual = FilerImageField(
        null=True, blank=True, default=None, on_delete=models.SET_NULL)
    vcard_enabled = models.BooleanField(
        verbose_name=_('enable vCard download'), default=True)
    user = models.OneToOneField(
        getattr(settings, 'AUTH_USER_MODEL', 'auth.User'),
        null=True, blank=True, related_name='persons')

    class Meta:
        verbose_name = _('Person')
        verbose_name_plural = _('People')

    def __str__(self):
        pkstr = str(self.pk)

        if six.PY2:
            pkstr = six.u(pkstr)
        name = self.safe_translation_getter(
            'name',
            default='',
            any_language=True
        ).strip()
        return name if len(name) > 0 else pkstr

    @property
    def primary_group(self):
        """Simply returns the first in `groups`, if any, else None."""
        return self.groups.first()

    @property
    def comment(self):
        return self.safe_translation_getter('description', '')

    def get_absolute_url(self, language=None):
        if not language:
            language = get_current_language()
        slug, language = self.known_translation_getter(
            'slug', None, language_code=language)
        if slug:
            kwargs = {'slug': slug}
        else:
            kwargs = {'pk': self.pk}
        with override(language):
            # do not fail with 500 error so that if detail view can't be
            # resolved we still can use plugins.
            try:
                url = reverse('aldryn_people:person-detail', kwargs=kwargs)
            except NoReverseMatch:
                url = ''
        return url

    def get_vcard_url(self, language=None):
        if not language:
            language = get_current_language()
        slug = self.safe_translation_getter(
            'slug', None, language_code=language, any_language=False)
        if slug:
            kwargs = {'slug': slug}
        else:
            kwargs = {'pk': self.pk}
        with override(language):
            return reverse('aldryn_people:download_vcard', kwargs=kwargs)

    def get_vcard(self, request=None):
        vcard = Vcard()
        function = self.safe_translation_getter('function')

        safe_name = self.safe_translation_getter(
            'name', default="Person: {0}".format(self.pk))
        vcard.add_line('FN', safe_name)
        vcard.add_line('N', [None, safe_name, None, None, None])

        if self.visual:
            ext = self.visual.extension.upper()
            try:
                with open(self.visual.path, 'rb') as f:
                    data = force_text(base64.b64encode(f.read()))
                    vcard.add_line('PHOTO', data, TYPE=ext, ENCODING='b')
            except IOError:
                if request:
                    url = urlparse.urljoin(request.build_absolute_uri(),
                                           self.visual.url),
                    vcard.add_line('PHOTO', url, TYPE=ext)

        if self.email:
            vcard.add_line('EMAIL', self.email)

        if function:
            vcard.add_line('TITLE', self.function)

        if self.phone:
            vcard.add_line('TEL', self.phone, TYPE='WORK')
        if self.mobile:
            vcard.add_line('TEL', self.mobile, TYPE='CELL')

        if self.fax:
            vcard.add_line('TEL', self.fax, TYPE='FAX')
        if self.website:
            vcard.add_line('URL', self.website)

        if self.primary_group:
            group_name = self.primary_group.safe_translation_getter(
                'name', default="Group: {0}".format(self.primary_group.pk))
            if group_name:
                vcard.add_line('ORG', group_name)
            if (self.primary_group.address or self.primary_group.city or
                    self.primary_group.postal_code):
                vcard.add_line('ADR', (
                    None, None,
                    self.primary_group.address,
                    self.primary_group.city,
                    None,
                    self.primary_group.postal_code,
                    None,
                ), TYPE='WORK')

            if self.primary_group.phone:
                vcard.add_line('TEL', self.primary_group.phone, TYPE='WORK')
            if self.primary_group.fax:
                vcard.add_line('TEL', self.primary_group.fax, TYPE='FAX')
            if self.primary_group.website:
                vcard.add_line('URL', self.primary_group.website)

        if six.PY2:
            vcard = unicode(vcard)

        return vcard
コード例 #6
0
ファイル: models.py プロジェクト: luisza/aldryn-people
class Person(TranslationHelperMixin, TranslatedAutoSlugifyMixin,
             TranslatableModel):
    slug_source_field_name = 'name'

    translations = TranslatedFields(
        name=models.CharField(_('name'),
                              max_length=255,
                              blank=False,
                              default='',
                              help_text=_("Provide this person's name.")),
        slug=models.SlugField(
            _('unique slug'),
            max_length=255,
            blank=True,
            default='',
            help_text=_("Leave blank to auto-generate a unique slug.")),
    )

    profile = models.OneToOneField(
        getattr(settings, 'ALDRYN_PEOPLE_PERSON_PROFILE',
                'aldryn_people.personprofile'))

    groups = SortedM2MModelField(
        'aldryn_people.Group',
        default=None,
        blank=True,
        related_name='people',
        help_text=_('Choose and order the groups for this person, the first '
                    'will be the "primary group".'))
    vcard_enabled = models.BooleanField(
        verbose_name=_('enable vCard download'), default=True)
    user = models.OneToOneField(getattr(settings, 'AUTH_USER_MODEL',
                                        'auth.User'),
                                null=True,
                                blank=True,
                                related_name='persons')

    class Meta:
        verbose_name = _('Person')
        verbose_name_plural = _('People')

    def __str__(self):
        pkstr = str(self.pk)

        if six.PY2:
            pkstr = six.u(pkstr)
        name = self.safe_translation_getter('name',
                                            default='',
                                            any_language=True).strip()
        return name if len(name) > 0 else pkstr

    @property
    def primary_group(self):
        """Simply returns the first in `groups`, if any, else None."""
        return self.groups.first()

    @property
    def comment(self):
        return """self.safe_translation_getter('description', '')"""

    def get_absolute_url(self, language=None):
        if not language:
            language = get_current_language()
        slug, language = self.known_translation_getter('slug',
                                                       None,
                                                       language_code=language)
        if slug:
            kwargs = {'slug': slug}
        else:
            kwargs = {'pk': self.pk}
        with override(language):
            # do not fail with 500 error so that if detail view can't be
            # resolved we still can use plugins.
            try:
                url = reverse('aldryn_people:person-detail', kwargs=kwargs)
            except NoReverseMatch:
                url = ''
        return url

    def get_vcard_url(self, language=None):
        if not language:
            language = get_current_language()
        slug = self.safe_translation_getter('slug',
                                            None,
                                            language_code=language,
                                            any_language=False)
        if slug:
            kwargs = {'slug': slug}
        else:
            kwargs = {'pk': self.pk}
        with override(language):
            return reverse('aldryn_people:download_vcard', kwargs=kwargs)

    def get_vcard(self, request=None):
        vcard = Vcard()
        function = self.safe_translation_getter('function')

        safe_name = self.safe_translation_getter('name',
                                                 default="Person: {0}".format(
                                                     self.pk))
        vcard.add_line('FN', safe_name)
        vcard.add_line('N', [None, safe_name, None, None, None])

        if hasattr(self.profile, "visual") and self.profile.visual:
            ext = self.profile.visual.extension.upper()
            try:
                with open(self.visual.path, 'rb') as f:
                    data = force_text(base64.b64encode(f.read()))
                    vcard.add_line('PHOTO', data, TYPE=ext, ENCODING='b')
            except IOError:
                if request:
                    url = urlparse.urljoin(request.build_absolute_uri(),
                                           self.profile.visual.url),
                    vcard.add_line('PHOTO', url, TYPE=ext)

        fields = [(
            "EMAIL",
            "email",
        ), ('TITLE', "function"), ('TEL', "phone", 'WORK'),
                  ('TEL', "mobile", 'CELL'), ('TEL', "fax", 'FAX'),
                  ('URL', "website")]

        for field in fields:
            if hasattr(self.profile, field[1]) and getattr(
                    self.profile, field[1]):
                if len(field) > 2:
                    vcard.add_line(field[0],
                                   getattr(self.profile, field[1]),
                                   TYPE=field[2])
                else:
                    vcard.add_line(field[0], getattr(self.profile, field[1]))

        if self.primary_group:
            group_name = self.primary_group.safe_translation_getter(
                'name', default="Group: {0}".format(self.primary_group.pk))
            if group_name:
                vcard.add_line('ORG', group_name)
            self.primary_group.get_vcard(vcard)
        return str(vcard)
コード例 #7
0
class Person(TranslationHelperMixin, TranslatedAutoSlugifyMixin,
             TranslatableModel):
    slug_source_field_name = 'name'

    translations = TranslatedFields(
        name=models.CharField(_('name'),
                              max_length=255,
                              blank=False,
                              default='',
                              help_text=_("Provide this person's name.")),
        slug=models.SlugField(
            _('unique slug'),
            max_length=255,
            blank=True,
            default='',
            help_text=_("Leave blank to auto-generate a unique slug.")),
        function=models.CharField(_('role'),
                                  max_length=255,
                                  blank=True,
                                  default=''),
        description=HTMLField(_('description'), blank=True, default=''))
    phone = models.CharField(verbose_name=_('phone'),
                             null=True,
                             blank=True,
                             max_length=100)
    mobile = models.CharField(verbose_name=_('mobile'),
                              null=True,
                              blank=True,
                              max_length=100)
    fax = models.CharField(verbose_name=_('fax'),
                           null=True,
                           blank=True,
                           max_length=100)
    email = models.EmailField(verbose_name=_("email"), blank=True, default='')
    website = models.URLField(verbose_name=_('website'), null=True, blank=True)
    groups = SortedM2MModelField(
        'aldryn_people.Group',
        default=None,
        blank=True,
        related_name='people',
        help_text=_('Choose and order the groups for this person, the first '
                    'will be the "primary group".'))
    regional_group = models.ForeignKey(
        'aldryn_people.RegionalGroup',
        default=None,
        blank=True,
        null=True,
        related_name='people',
        help_text=_('Choose the regional groups for this person.'))
    regional_section_number = models.IntegerField(
        verbose_name=_('Regional section number'),
        blank=True,
        default=None,
        null=True)
    visual = FilerImageField(null=True,
                             blank=True,
                             default=None,
                             on_delete=models.SET_NULL)
    vcard_enabled = models.BooleanField(
        verbose_name=_('enable vCard download'), default=True)
    user = models.OneToOneField(getattr(settings, 'AUTH_USER_MODEL',
                                        'auth.User'),
                                null=True,
                                blank=True,
                                related_name='persons')
    sort_order = models.IntegerField(verbose_name=_('sort order'),
                                     blank=True,
                                     default=999999)
    unit_number = models.CharField(max_length=10,
                                   blank=True,
                                   default='',
                                   verbose_name=_('Unit number'))
    street_number = models.CharField(max_length=10,
                                     blank=True,
                                     default='',
                                     verbose_name=_('Street number'))
    street = models.CharField(max_length=20,
                              blank=True,
                              default='',
                              verbose_name=_('Street/Avenue'))
    city = models.CharField(max_length=20,
                            blank=True,
                            default='',
                            verbose_name=_('City'))
    province = models.CharField(max_length=20,
                                blank=True,
                                default='BC',
                                verbose_name=_('Province'))
    postal = models.CharField(
        max_length=7,
        blank=True,
        default='',
        verbose_name=_('Postal Code'),
        validators=[
            RegexValidator(
                regex='^[a-zA-Z ][0-9 ][a-zA-Z ] [0-9 ][a-zA-Z ][0-9 ]$',
                message='Ex: V1V 9Y9',
                code=_('Invalid Postal Code')),
        ])
    country = models.CharField(max_length=20,
                               blank=True,
                               default='Canada',
                               verbose_name='Country')
    latitude = models.FloatField(null=True,
                                 blank=True,
                                 verbose_name='Latitude')
    longitude = models.FloatField(null=True,
                                  blank=True,
                                  verbose_name='Longitude')
    email_confirmed = models.BooleanField(default=False)

    parish_account = models.CharField(max_length=30,
                                      blank=True,
                                      null=True,
                                      default=None,
                                      verbose_name=_('Parish Account'))
    RELATIONSHIP_CHOICES = [('self', _('Self')), ('spouse', _('Spouse')),
                            ('child', _('Child')), ('other', _('Other'))]
    relationship = models.CharField(_('Relationship'),
                                    choices=RELATIONSHIP_CHOICES,
                                    default=RELATIONSHIP_CHOICES[0][0],
                                    max_length=50)

    class Meta:
        verbose_name = _('Person')
        verbose_name_plural = _('People')

    def __str__(self):
        pkstr = str(self.pk)

        if six.PY2:
            pkstr = six.u(pkstr)
        name = self.safe_translation_getter('name',
                                            default='',
                                            any_language=True).strip()
        return name if len(name) > 0 else pkstr

    @property
    def primary_group(self):
        """Simply returns the first in `groups`, if any, else None."""
        return self.groups.first()

    @property
    def comment(self):
        return self.safe_translation_getter('description', '')

    @property
    def address(self):
        address = ''
        if self.unit_number not in ['', None]:
            address += '#%s, ' % self.unit_number
        if self.street_number not in ['', None]:
            address += '%s ' % self.street_number
        if self.street not in ['', None]:
            address += '%s, ' % self.street
        if self.city not in ['', None]:
            address += '%s, ' % self.city
        address += '%s %s, %s' % (self.province, self.postal, self.country)
        return address

    def clean(self):
        geo_location = get_geographic_coordinates(
            self.get_address_for_geo_location())
        self.longitude = geo_location['lng']
        self.latitude = geo_location['lat']
        if self.regional_section_number and self.regional_group:
            if self.regional_section_number > self.regional_group.number_of_sections:
                self.regional_section_number = None
        else:
            self.regional_section_number = None

    def get_address_for_geo_location(self):
        address = ''
        if self.street_number not in ['', None]:
            address += '%s ' % self.street_number
        else:
            return None
        if self.street not in ['', None]:
            address += '%s, ' % self.street
        else:
            return None
        if self.city not in ['', None]:
            address += '%s, ' % self.city
        else:
            return None
        address += '%s %s, %s' % (self.province, self.postal, self.country)
        return address

    def get_absolute_url(self, language=None):
        if not language:
            language = get_current_language()
        slug, language = self.known_translation_getter('slug',
                                                       None,
                                                       language_code=language)
        if slug:
            kwargs = {'slug': slug}
        else:
            kwargs = {'pk': self.pk}
        with override(language):
            # do not fail with 500 error so that if detail view can't be
            # resolved we still can use plugins.
            try:
                url = reverse('aldryn_people:person-detail', kwargs=kwargs)
            except NoReverseMatch:
                url = ''
        return url

    def get_vcard_url(self, language=None):
        if not language:
            language = get_current_language()
        slug = self.safe_translation_getter('slug',
                                            None,
                                            language_code=language,
                                            any_language=False)
        if slug:
            kwargs = {'slug': slug}
        else:
            kwargs = {'pk': self.pk}
        with override(language):
            return reverse('aldryn_people:download_vcard', kwargs=kwargs)

    def get_vcard(self, request=None):
        vcard = Vcard()
        person_translation = self.translations.model.objects.get(
            master_id=self.id, language_code='en')
        function = person_translation.function

        safe_name = person_translation.name
        vcard.add_line('FN', safe_name)
        vcard.add_line('N', [None, safe_name, None, None, None])

        if self.visual:
            ext = self.visual.extension.upper()
            try:
                with open(self.visual.path, 'rb') as f:
                    data = force_text(base64.b64encode(f.read()))
                    vcard.add_line('PHOTO', data, TYPE=ext, ENCODING='b')
            except IOError:
                if request:
                    url = urlparse.urljoin(request.build_absolute_uri(),
                                           self.visual.url),
                    vcard.add_line('PHOTO', url, TYPE=ext)

        if self.email:
            vcard.add_line('EMAIL', self.email)

        if function:
            vcard.add_line('TITLE', function)

        if self.phone:
            vcard.add_line('TEL', self.phone, TYPE='WORK')
        if self.mobile:
            vcard.add_line('TEL', self.mobile, TYPE='CELL')

        if self.fax:
            vcard.add_line('TEL', self.fax, TYPE='FAX')
        if self.website:
            vcard.add_line('URL', self.website)

        # if self.primary_group:
        #     group_name = self.primary_group.safe_translation_getter(
        #         'name', default="Group: {0}".format(self.primary_group.pk))
        #     if group_name:
        #         vcard.add_line('ORG', group_name)
        #     if (self.primary_group.address or self.primary_group.city or
        #             self.primary_group.postal_code):
        #         vcard.add_line('ADR', (
        #             None, None,
        #             self.primary_group.address,
        #             self.primary_group.city,
        #             None,
        #             self.primary_group.postal_code,
        #             None,
        #         ), TYPE='WORK')
        #
        #     if self.primary_group.phone:
        #         vcard.add_line('TEL', self.primary_group.phone, TYPE='WORK')
        #     if self.primary_group.fax:
        #         vcard.add_line('TEL', self.primary_group.fax, TYPE='FAX')
        #     if self.primary_group.website:
        #         vcard.add_line('URL', self.primary_group.website)

        return str(vcard)
コード例 #8
0
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')
    # original author field
    #author = models.ForeignKey(Person, null=True, blank=True,
    #                           verbose_name=_('author'))

    # Sorted Many to Many field.
    author = SortedM2MModelField(
        'aldryn_people.Person',
        default=None,
        blank=True,
        related_name='author',
        help_text=_('Choose and order the authors for this article'))

    owner = models.ForeignKey(settings.AUTH_USER_MODEL,
                              verbose_name=_('owner'))
    app_config = AppHookConfigField(NewsBlogConfig,
                                    verbose_name=_('Apphook configuration'))
    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(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_unicode(category.safe_translation_getter('name')))
        for tag in self.tags.all():
            text_bits.append(force_unicode(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':
                    u' '.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)