Exemple #1
0
class Membership(models.Model):
    person = models.ForeignKey(Person,
                               models.CASCADE,
                               verbose_name=_("person"))
    team = models.ForeignKey(Team, models.CASCADE, verbose_name=_("team"))
    title = models.CharField(max_length=100, verbose_name=_("title"))
    image = ImageField(
        _("image"),
        blank=True,
        null=True,
        upload_to="people/",
        auto_add_fields=True,
        formats={
            "square": ["default", ("crop", (900, 900))],
        },
    )
    order = models.IntegerField(default=0)

    class Meta:
        ordering = ["order"]
        verbose_name = _("membership")
        verbose_name_plural = _("memberships")

    def __str__(self):
        return f"{self.person}@{self.team}"
Exemple #2
0
class CVDocumentPhoto(models.Model):
    id_name = models.CharField(
        max_length=255, verbose_name=gettext_lazy("Photo identification name"))

    source = ImageField(verbose_name=gettext_lazy("Image"),
                        height_field='height',
                        width_field='width',
                        ppoi_field='ppoi',
                        upload_to='cv-photo/',
                        formats={'full': [
                            'default',
                        ]})

    width = models.PositiveIntegerField(
        verbose_name=gettext_lazy("Image width"))

    height = models.PositiveIntegerField(
        verbose_name=gettext_lazy("Image height"))

    ppoi = PPOIField('Image PPOI')

    def __str__(self):
        return self.id_name

    class Meta:
        verbose_name = gettext_lazy("Photo")
        verbose_name_plural = gettext_lazy("Photos")
        db_table = 'app_owner_cv_doc_photo'
class AbstractModel(models.Model):
    image = ImageField(
        _("image"),
        upload_to="images",
        width_field="width",
        height_field="height",
        ppoi_field="ppoi",
        formats={
            "thumb": ["default", ("crop", (300, 300))],
            "desktop": ["default", ("thumbnail", (300, 225))],
        },
        # Should have no effect, but not hurt either:
        auto_add_fields=True,
    )
    width = models.PositiveIntegerField(_("image width"),
                                        blank=True,
                                        null=True,
                                        editable=False)
    height = models.PositiveIntegerField(_("image height"),
                                         blank=True,
                                         null=True,
                                         editable=False)
    ppoi = PPOIField(_("primary point of interest"))

    class Meta:
        abstract = True
Exemple #4
0
class Image(models.Model):
    """
    Image plugin
    """
    image = ImageField(
        _('image'),
        upload_to='images/%Y/%m',
        width_field='width',
        height_field='height',
        ppoi_field='ppoi',

        # NOTE! You probably want to use auto_add_fields=True in your own
        # models and not worry about setting the *_field vars above.
    )
    width = models.PositiveIntegerField(
        _('image width'),
        blank=True,
        null=True,
        editable=False,
    )
    height = models.PositiveIntegerField(
        _('image height'),
        blank=True,
        null=True,
        editable=False,
    )
    ppoi = PPOIField(_('primary point of interest'))

    class Meta:
        abstract = True
        verbose_name = _('image')
        verbose_name_plural = _('images')

    def __str__(self):
        return self.image.name
Exemple #5
0
class Testimonial(models.Model):
    campaign = models.ForeignKey(Campaign, models.CASCADE)

    first_name = models.CharField(_("first name"), max_length=180)
    last_name = models.CharField(_("last name"), max_length=180)
    email = models.EmailField(_("e-mail"))
    title = models.CharField(_("title"), max_length=180)
    created_at = models.DateTimeField(auto_now_add=True)

    statement = models.TextField(_("statement"))

    image = ImageField(
        _("image"),
        auto_add_fields=True,
        formats={"square": ["default", ("crop", (660, 660))],},
    )

    validated = models.BooleanField(_("validate"), default=False)
    public = models.BooleanField(_("public"), default=False)

    def __str__(self):
        return f"{self.first_name} {self.last_name}"

    class Meta:
        verbose_name = _("testimonial")
        verbose_name_plural = _("testimonials")
        ordering = ["-created_at"]
Exemple #6
0
class ImageMixin(models.Model):
    image_file = ImageField(
        _("image"),
        upload_to=UPLOAD_TO,
        width_field="image_width",
        height_field="image_height",
        ppoi_field="image_ppoi",
        blank=True,
        max_length=1000,
    )
    image_width = models.PositiveIntegerField(_("image width"),
                                              blank=True,
                                              null=True,
                                              editable=False)
    image_height = models.PositiveIntegerField(_("image height"),
                                               blank=True,
                                               null=True,
                                               editable=False)
    image_ppoi = PPOIField(_("primary point of interest"))
    image_alt_text = models.CharField(_("alternative text"),
                                      max_length=1000,
                                      blank=True)

    class Meta:
        abstract = True
        verbose_name = _("image")
        verbose_name_plural = _("images")

    def accept_file(self, value):
        if upload_is_image(value):
            self.image_file = value
            return True
Exemple #7
0
class Candidature(models.Model):
    person = models.ForeignKey(Person,
                               models.CASCADE,
                               verbose_name=_("person"))
    candidate_list = models.ForeignKey(CandidateList,
                                       models.CASCADE,
                                       verbose_name=_("list"),
                                       related_name="candidates")
    image = ImageField(
        _("image"),
        blank=True,
        null=True,
        upload_to="people/",
        auto_add_fields=True,
        formats={
            "square": ["default", ("crop", (900, 900))],
        },
    )

    list_number = models.CharField(max_length=10, blank=True)
    modifier = models.CharField(max_length=30, blank=True)
    slogan = models.TextField(blank=True)
    url = models.URLField(_("more information"), blank=True)

    order = models.PositiveIntegerField(default=0)

    def __str__(self):
        return f"{self.candidate_list.name} - {self.person}"
class WebsafeImage(models.Model):
    image = ImageField(
        _("image"),
        upload_to="images",
        auto_add_fields=True,
        fallback="python-logo.tiff",
        formats={"preview": websafe(["default", ("crop", (300, 300))])},
    )
        class ModelWithOptional(models.Model):
            image = ImageField(
                auto_add_fields=True,
                formats={"thumb": ["default", ("crop", (300, 300))]},
            )

            class Meta:
                app_label = "testapp"
Exemple #10
0
class SlowStorageImage(models.Model):
    image = ImageField(
        _("image"),
        upload_to="images",
        auto_add_fields=True,
        storage=slow_storage,
        formats={"thumb": ["default", ("crop", (20, 20))]},
    )
Exemple #11
0
class Category(TranslationMixin, MetaMixin, TreeNode):
    name = models.CharField(max_length=200, verbose_name=_("name"))
    slug = models.SlugField(verbose_name=_("slug"))
    color = models.CharField(max_length=7, verbose_name=_("color"), blank=True,)

    header_image = ImageField(
        _("header image"),
        formats={
            "full": ["default", "darken", ("crop", (1920, 900))],
            "square": ["default", ("crop", (900, 900))],
            "card": ["default", ("crop", (900, 600))],
            "mobile": ["default", ("crop", (740, 600))],
            "some": ["default", ("crop", (1200, 630))],
        },
        auto_add_fields=True,
        blank=True,
        null=True,
    )

    class Meta:
        verbose_name = _("category")
        verbose_name_plural = _("categories")
        indexes = [models.Index(fields=["slug"])]
        constraints = [
            models.UniqueConstraint(
                fields=["slug", "language_code"], name="unique_slug_for_language"
            )
        ]

    @property
    def title(self):
        return self.name

    def get_absolute_url(self):
        site = current_site()
        try:
            return reverse_app(
                (str(site.id) + "-categories",),
                "category-detail",
                languages=[self.language_code],
                kwargs={"slug": self.slug},
            )
        except NoReverseMatch:
            return "#"

    def get_header_image(self):
        if self.header_image:
            return self.header_image
        if self.parent:
            return self.parent.get_header_image()
        return None

    def __str__(self):
        return f"{self.name} ({self.language_code})"
Exemple #12
0
class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    image = ImageField(default="default.jpg",
                       upload_to="profile_pics",
                       auto_add_fields=True)
    bio = models.CharField(max_length=150, blank=True)
    # bio is a field in this Post model. it specifies a class attribute Charfield and represents a database column
    first_name = models.CharField(max_length=30, blank=True)
    last_name = models.CharField(max_length=30, blank=True)

    def __str__(self):
        return f"{self.user.username} Profile"
Exemple #13
0
class Presidium(models.Model):
    name = models.CharField(max_length=100)
    description = models.CharField(max_length=100)
    alt_text = models.CharField(max_length=100)
    image = ImageField(
        _('image'),
        upload_to='images/%Y/%m',
        blank=True,
        auto_add_fields=True,
    )

    def __str__(self):
        return self.name
Exemple #14
0
class Teacher(User):
    name = models.CharField(max_length=256)
    img = ImageField(upload_to='students',
                     formats=img_formats,
                     auto_add_fields=True,
                     blank=True,
                     null=True)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = 'Teacher'
Exemple #15
0
class Article(LanguageMixin, TemplateMixin, MetaMixin):
    TEMPLATES = get_template_list('blog', (('default', ('main', )), ))

    namespace = models.ForeignKey(Namespace,
                                  models.PROTECT,
                                  verbose_name=_("namespace"))

    title = models.CharField(max_length=200, verbose_name=_("title"))
    slug = models.SlugField(verbose_name=_("slug"), max_length=180)

    publication_date = models.DateTimeField(default=timezone.now,
                                            verbose_name=_("publication date"))

    created_date = models.DateTimeField(auto_now_add=True,
                                        verbose_name=_("created at"))

    edited_date = models.DateTimeField(auto_now=True,
                                       verbose_name=_("edited at"))

    image = ImageField(_("header image"),
                       formats={
                           'large': ['default', ('crop', (1920, 900))],
                           'square': ['default', ('crop', (900, 900))],
                           'card': ['default', ('crop', (900, 600))],
                           'mobile': ['default', ('crop', (740, 600))],
                           'preview': ['default', ('crop'), (1200, 600)],
                       },
                       auto_add_fields=True,
                       blank=True,
                       null=True)

    category = models.ForeignKey(Category,
                                 models.SET_NULL,
                                 blank=True,
                                 null=True,
                                 verbose_name=_("category"))

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse_app((f'blog-{self.namespace.slug}', ),
                           'article-detail',
                           kwargs={'slug': self.slug},
                           languages=[self.language_code])

    class Meta:
        ordering = ['-publication_date']
        get_latest_by = 'publication_date'
Exemple #16
0
class ProjectGalleryImage(models.Model):
    order = models.PositiveIntegerField(default=0)

    gallery = models.ForeignKey(
        'ProjectGallery',
        on_delete=models.CASCADE,
    )

    image = ImageField(verbose_name=gettext_lazy("Image"),
                       height_field='height',
                       width_field='width',
                       ppoi_field='ppoi',
                       upload_to='project_gallery/',
                       formats={
                           'thumb': ['default', ('thumbnail', (250, 180))],
                           'thumb_webp': thumb_webp_processor_spec,
                           'full': [
                               'default',
                           ],
                           'full_webp': webp_processor_spec
                       })

    width = models.PositiveIntegerField(
        verbose_name=gettext_lazy("Image width"))

    height = models.PositiveIntegerField(
        verbose_name=gettext_lazy("Image height"))

    ppoi = PPOIField('Image PPOI')

    alt = models.CharField(max_length=255, default='image')

    def toJson(self):
        return {
            'width': self.width,
            'height': self.height,
            'url': self.image.thumb
        }

    def __str__(self):
        return ''

    class Meta:
        verbose_name = gettext_lazy("Gallery Image")
        verbose_name_plural = gettext_lazy("Gallery Images")
        db_table = 'app_projects_project_gallery_image'
        ordering = ['order']
Exemple #17
0
class NullableImage(models.Model):
    image = ImageField(
        _("image"),
        upload_to="images",
        blank=True,
        null=True,
        formats={"thumb": ["default", ("crop", (20, 20))]},
        auto_add_fields=False,
        width_field="image_width",
        height_field="image_height",
    )
    image_width = models.PositiveIntegerField(blank=True,
                                              null=True,
                                              editable=False)
    image_height = models.PositiveIntegerField(blank=True,
                                               null=True,
                                               editable=False)
Exemple #18
0
class Student(User):
    classname = models.ForeignKey(ClassName,
                                  related_name='students',
                                  on_delete=models.CASCADE)
    name = models.CharField(max_length=256)
    img = ImageField(upload_to='students',
                     formats=img_formats,
                     auto_add_fields=True,
                     blank=True,
                     null=True)
    avg_mark = models.FloatField(default=5)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = 'Student'
Exemple #19
0
class Pledge(models.Model):
    first_name = models.CharField(_('first name'), max_length=40)

    last_name = models.CharField(
        _('last name'),
        max_length=40,
    )

    uuid = models.UUIDField(default=uuid.uuid4, primary_key=True)

    image = ImageField(_("image"),
                       formats={
                           'large': ['default', ('crop', (760, 760))],
                           'preview': ['default', ('crop', (1200, 630))],
                       },
                       blank=True,
                       auto_add_fields=True)

    description = models.CharField(_("description"),
                                   max_length=140,
                                   blank=True)

    email = models.EmailField(_("e-mail"), unique=True)

    token = models.CharField(
        max_length=60,
        default=secrets.token_urlsafe,
        blank=True,
    )

    confirmed = models.BooleanField(default=False)

    confirmed_at = models.DateField(blank=True, null=True)

    public = models.BooleanField(default=True)

    created_at = models.DateTimeField(auto_now_add=True)

    def get_absolute_url(self):
        return reverse('pledge_list') + f"?featured={self.pk}"

    class Meta:
        ordering = ['-created_at']
Exemple #20
0
class Category(LanguageMixin):
    name = models.CharField(max_length=200, verbose_name=_("name"))
    slug = models.SlugField(verbose_name=_("slug"))

    image = ImageField(_("header image"),
                       formats={
                           'large': ['default', ('crop', (1920, 900))],
                           'square': ['default', ('crop', (900, 900))],
                           'card': ['default', ('crop', (900, 600))],
                           'mobile': ['default', ('crop', (740, 600))],
                           'preview': ['default', ('crop'), (1200, 600)],
                       },
                       auto_add_fields=True,
                       blank=True,
                       null=True)

    class Meta:
        verbose_name = _("category")
        verbose_name_plural = _("categories")
Exemple #21
0
class Location(MetaMixin, TranslationMixin):
    regions = [Region(key="images", title=_("images"))]
    name = models.CharField(max_length=200, verbose_name=_("name"))
    slug = models.SlugField(unique=True)

    street = models.CharField(max_length=200, verbose_name=_("street"))
    city = models.CharField(max_length=100, verbose_name=_("city"))
    zip_code = models.CharField(max_length=20, verbose_name=_("zip code"))
    country = models.CharField(max_length=200, verbose_name=_("country"))
    is_physical = models.BooleanField(
        verbose_name=_("is physical"),
        default=True,
    )

    header_image = ImageField(
        _("header image"),
        formats={
            "full": ["default", "darken", ("crop", (1920, 900))],
            "mobile": ["default", ("crop", (740, 600))],
        },
        auto_add_fields=True,
        blank=True,
        null=True,
    )

    section = models.ForeignKey(Section,
                                models.SET_NULL,
                                blank=True,
                                null=True,
                                verbose_name=_("section"))

    website = models.URLField(blank=True, verbose_name=_("website"))

    lng = models.FloatField(verbose_name=_("longitude"), default=0)
    lat = models.FloatField(verbose_name=_("latitude"), default=0)

    tags = TaggableManager(blank=True)

    def maps(self):
        return settings.MAPS_URL.format(location=self)

    def __str__(self):
        return self.name

    @property
    def title(self):
        return self.name

    @property
    def address(self):
        return f"{self.street}, {self.zip_code} {self.city}"

    class Meta:
        verbose_name = _("location")
        verbose_name_plural = _("locations")
        ordering = ["name"]

    def get_absolute_url(self):
        try:
            site = current_site()
            if not self.section or site == self.section.site:
                return reverse_app([f"{site.pk}-events"],
                                   "location-detail",
                                   kwargs={"slug": self.slug})
            with set_current_site(self.section.site):
                return ("//" + self.section.site.host + reverse_app(
                    [f"{self.section.site.id}-events"],
                    "location-detail",
                    urlconf=apps_urlconf(),
                    kwargs={"slug": self.slug},
                ))
        except NoReverseMatch:
            return "#"
Exemple #22
0
class Page(
        AppsMixin,
        TranslationMixin,
        MetaMixin,
        TemplateMixin,
        RedirectMixin,
        MenuMixin,
        AbstractPage,
):
    APPLICATIONS = [
        (
            "blog",
            _("blog"),
            {
                "urlconf":
                "juso.blog.urls",
                "app_instance_namespace":
                lambda page: "-".join((str(x) for x in [
                    page.site_id,
                    page.application,
                    page.blog_namespace.name if page.blog_namespace else None,
                    page.category,
                ] if x)),
            },
        ),
        (
            "people",
            _("people"),
            {
                "urlconf":
                "juso.people.urls",
                "app_instance_namespace":
                lambda page: "-".join((str(x) for x in [
                    page.site_id,
                    page.application,
                ] if x)),
            },
        ),
        (
            "events",
            _("events"),
            {
                "urlconf":
                "juso.events.urls",
                "app_instance_namespace":
                lambda page: "-".join((str(x) for x in [
                    page.site_id,
                    page.application,
                    page.category,
                ] if x)),
            },
        ),
        (
            "categories",
            _("categories"),
            {
                "urlconf":
                "juso.sections.urls",
                "app_instance_namespace":
                lambda page: str(page.site_id) + "-" + "categories",
            },
        ),
        (
            "glossary",
            _("glossary"),
            {
                "urlconf":
                "juso.glossary.urls",
                "app_instance_namespace":
                lambda page: str(page.site_id) + "-" + "glossary",
            },
        ),
        (
            "collection",
            _("collection"),
            {
                "urlconf":
                "juso.link_collections.urls",
                "required_fields": ["collection"],
                "app_instance_namespace":
                lambda page: str(page.slug) + "-collections",
            },
        ),
    ]

    MENUS = (
        ("main", _("main navigation")),
        ("top", _("top navigation")),
        ("buttons", _("button navigation")),
        ("footer", _("footer navigation")),
        ("quicklink", _("quickinks")),
    )

    TEMPLATES = get_template_list(
        "pages",
        (
            ("default", ("main", "footer")),
            ("feature_top", ("main", "sidebar", "feature")),
        ),
    )

    is_landing_page = models.BooleanField(
        default=False,
        verbose_name=_("is landing page"),
    )

    position = models.PositiveIntegerField(
        db_index=True,
        default=10,
        validators=[
            MinValueValidator(
                limit_value=1,
                message=_("Position is expected to be greater than zero."),
            )
        ],
    )

    blog_namespace = models.ForeignKey(
        "blog.NameSpace",
        models.SET_NULL,
        blank=True,
        null=True,
        verbose_name=_("namespace (blog)"),
    )

    sections = models.ManyToManyField(
        "sections.Section",
        verbose_name=_("sections"),
        blank=True,
    )

    category = models.ForeignKey(
        "sections.Category",
        models.SET_NULL,
        blank=True,
        null=True,
        verbose_name=_("category"),
    )

    collection = models.ForeignKey(
        "link_collections.Collection",
        models.CASCADE,
        blank=True,
        null=True,
        verbose_name=_("collection"),
    )

    header_image = ImageField(
        _("header image"),
        formats={
            "full": ["default", "darken", ("crop", (1920, 900))],
            "square": ["default", ("crop", (960, 960))],
            "card": ["default", ("crop", (900, 600))],
            "mobile": ["default", ("crop", (740, 600))],
        },
        auto_add_fields=True,
        blank=True,
        null=True,
    )

    featured_categories = models.ManyToManyField(
        "sections.Category",
        blank=True,
        verbose_name=_("featured categories"),
        related_name="featured",
    )

    in_meta = models.BooleanField(_("in meta menu"), default=False)

    is_navigation = models.BooleanField(_("display navigation"), default=False)

    lastmod = models.DateTimeField(_("lastmod"), auto_now=True)

    logo = models.TextField(_("logo"), blank=True)

    google_site_verification = models.CharField(max_length=60, blank=True)

    favicon = ImageField(
        _("favicon"),
        formats={
            "192": ["default", ("crop", (192, 192))],
            "512": ["default", ("crop", (512, 512))],
            "180": ["default", ("crop", (180, 180))],
            "128": ["default", ("crop", (128, 128))],
            "32": ["default", ("crop", (32, 32))],
            "16": ["default", ("crop", (16, 16))],
        },
        blank=True,
        auto_add_fields=True,
    )

    primary_color = models.CharField(_("primary color"),
                                     max_length=7,
                                     blank=True)

    css_vars = models.TextField(_("css vars"), blank=True)
    fonts = models.TextField(_("fonts"),
                             default="klima",
                             help_text=_("fonts loaded on the site"))

    @property
    def description(self):
        return self.meta_description or self.tagline[:300]

    @property
    def tagline(self):
        if RichText.objects.filter(parent=self).exists():
            return bleach.clean(
                RichText.objects.filter(parent=self)[0].text,
                strip=True,
                tags=[],
            )
        if self.meta_description:
            return self.meta_description
        return ""

    def get_fonts(self):
        for font in self.fonts.split("\n"):
            yield font.strip() + ".css"

    prefetches = models.TextField(
        _("prefetch"),
        default="""fonts/klima-regular-web.woff2:font
fonts/klima-regular-italic-web.woff2:font
fonts/klima-bold-web.woff2:font
fonts/klima-bold-italic-web.woff2:font""",
        help_text=_("files that should be preloaded"),
    )

    def get_prefeteches(self):
        for prefetch in self.prefetches.split("\n"):
            yield prefetch.strip().split(":")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._is_landing_page = self.is_landing_page

    @transaction.atomic
    def save(self, *args, **kwargs):
        if not self.is_landing_page or self._is_landing_page == self.is_landing_page:
            return super().save(*args, **kwargs)
        Page.objects.filter(
            is_landing_page=True,
            language_code=self.language_code,
            site=self.site,
        ).update(is_landing_page=False)
        return super().save(*args, **kwargs)

    def get_absolute_url(self, *args, **kwargs):

        if self.redirect_to_url or self.redirect_to_page:
            return self.redirect_to_url or self.redirect_to_page.get_absolute_url(
            )

        site = current_site()
        if site == self.site:
            return super().get_absolute_url(*args, **kwargs)
        return "//" + self.site.host + super().get_absolute_url()

    def get_category_color(self):
        return self.category.color if self.category else settings.DEFAULT_COLOR

    def get_header_image(self):
        header_image = None
        if self.header_image:
            header_image = self.header_image
        if self.parent:
            header_image = header_image or self.parent.get_header_image()
        if self.category:
            header_image = header_image or self.category.get_header_image()
        return header_image

    def top_page(self):
        return self.ancestors(include_self=True)[0]

    def get_translation_for(self, language_code):
        r = super().get_translation_for(language_code)
        if r:
            return r
        if self.parent:
            return self.parent.get_translation_for(language_code)
        return None

    class Meta:
        verbose_name = _("page")
        verbose_name_plural = _("pages")
        indexes = [
            models.Index(fields=[
                "path",
                "site_id",
                "language_code",
                "is_active",
            ]),
            models.Index(fields=[
                "is_landing_page",
                "site_id",
                "language_code",
            ]),
            models.Index(fields=["is_active", "menu", "language_code"]),
        ]

        constraints = [
            models.UniqueConstraint(fields=["path", "site_id"],
                                    name="unique_page_for_path")
        ]
class MetaMixin(models.Model):
    meta_title = models.CharField(
        _("title"),
        max_length=200,
        blank=True,
        help_text=_("Used for Open Graph and other meta tags."),
    )
    meta_description = models.TextField(
        _("description"),
        blank=True,
        help_text=_("Override the description for this page."),
    )
    meta_image = ImageField(
        _("image"),
        blank=True,
        auto_add_fields=True,
        upload_to="meta/%Y/%m",
        help_text=_("Set the Open Graph image."),
        formats={"recommended": ("default", ("crop", (1200, 630)))},
    )
    meta_canonical = models.URLField(
        _("canonical URL"),
        blank=True,
        help_text=_("If you need this you probably know."),
    )
    meta_author = models.CharField(
        _("author"),
        max_length=200,
        blank=True,
        help_text=_("Override the author meta tag."),
    )
    meta_robots = models.CharField(
        _("robots"),
        max_length=200,
        blank=True,
        help_text=_("Override the robots meta tag."),
    )

    class Meta:
        abstract = True

    @classmethod
    def admin_fieldset(cls, **kwargs):
        cfg = {
            "fields": (
                "meta_title",
                "meta_description",
                "meta_image",
                "meta_image_ppoi",
                "meta_canonical",
                "meta_author",
                "meta_robots",
            ),
            "classes": ("tabbed",),
        }
        cfg.update(kwargs)
        return (_("Meta tags"), cfg)

    def meta_dict(self):
        ctx = {
            "title": self.meta_title or getattr(self, "title", ""),
            "description": self.meta_description,
            "canonical": self.meta_canonical,
            # Override URL if canonical is set to a non-empty value (the empty
            # string will be skipped when merging this dictionary)
            "url": self.meta_canonical,
            "author": self.meta_author,
            "robots": self.meta_robots,
        }
        ctx.update(self.meta_images_dict())
        return ctx

    def meta_images_dict(self):
        if self.meta_image:
            return {
                "image": str(self.meta_image.recommended),
                "image:width": 1200,
                "image:height": 630,
            }

        elif getattr(self, "image", None):
            return {"image": self.image.url}

        return {"image": ""}
Exemple #24
0
class Account(models.Model):
    """
    An account for a user or organization.

    A personal account has a `user`, an organizational account does not.
    """

    tier = models.ForeignKey(
        "AccountTier",
        default=1,
        on_delete=models.DO_NOTHING,
        help_text="The tier of the account. Determines its quota limits.",
    )

    creator = models.ForeignKey(
        User,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="accounts_created",
        help_text="The user who created the account.",
    )

    created = models.DateTimeField(
        null=False,
        auto_now_add=True,
        help_text="The time the account was created.")

    user = models.OneToOneField(
        User,
        null=True,
        blank=True,
        # Cascade delete so that when the user is deleted, so is this account.
        # Avoid using `SET_NULL` here as that could result in a personal
        # account being treated as an organization if the user is deleted.
        on_delete=models.CASCADE,
        related_name="personal_account",
        help_text=
        "The user for this account. Only applies to personal accounts.",
    )

    name = models.SlugField(
        null=False,
        blank=False,
        unique=True,
        max_length=64,
        help_text=
        "Name of the account. Lowercase and no spaces or leading numbers. "
        "Will be used in URLS e.g. https://hub.stenci.la/awesome-org",
    )

    customer = models.OneToOneField(
        djstripe.models.Customer,
        null=True,
        blank=True,
        default=None,
        on_delete=models.SET_NULL,
        related_name="account",
        help_text=
        "The Stripe customer instance (if this account has ever been one)",
    )

    billing_email = models.EmailField(
        null=True,
        blank=True,
        help_text="The email to use for billing (e.g. sending invoices)",
    )

    image = ImageField(
        null=True,
        blank=True,
        storage=media_storage(),
        upload_to="accounts/images",
        formats={
            "small": ["default", ("crop", (20, 20))],
            "medium": ["default", ("crop", (50, 50))],
            "large": ["default", ("crop", (250, 250))],
        },
        auto_add_fields=True,
        help_text="Image for the account.",
    )

    display_name = models.CharField(
        null=True,
        blank=True,
        max_length=256,
        help_text="Name to display in account profile.",
    )

    location = models.CharField(
        null=True,
        blank=True,
        max_length=256,
        help_text="Location to display in account profile.",
    )

    website = models.URLField(
        null=True,
        blank=True,
        help_text="URL to display in account profile.",
    )

    email = models.EmailField(
        null=True,
        blank=True,
        help_text=
        "An email to display in account profile. Will not be used by Stencila to contact you.",
    )

    theme = models.TextField(
        null=True,
        blank=True,
        help_text=
        "The name of the theme to use as the default when generating content for this account."
        # In the future this may be a path to a Thema compatible theme hosted on the Hub or elsewhere.
        # Because of that, it is not a ChoiceField based on the list of names in `assets.thema.themes`.
    )

    extra_head = models.TextField(
        null=True,
        blank=True,
        help_text=
        "Content to inject into the <head> element of HTML served for this account.",
    )

    extra_top = models.TextField(
        null=True,
        blank=True,
        help_text=
        "Content to inject at the top of the <body> element of HTML served for this account.",
    )

    extra_bottom = models.TextField(
        null=True,
        blank=True,
        help_text=
        "Content to inject at the bottom of the <body> element of HTML served for this account.",
    )

    hosts = models.TextField(
        null=True,
        blank=True,
        help_text="A space separated list of valid hosts for the account. "
        "Used for setting Content Security Policy headers when serving content for this account.",
    )

    def __str__(self):
        """
        Get the string representation of the account.

        Example of where this is used: to generate the <select>
        <option> display text when choosing an account.
        """
        return self.name

    @property
    def is_personal(self):
        """Is this a personal account."""
        return self.user_id is not None

    @property
    def is_organization(self):
        """Is this an organizational account."""
        return self.user_id is None

    def get_url(self):
        """Get the URL for this account."""
        return reverse("ui-accounts-retrieve", args=[self.name])

    def get_meta(self) -> Meta:
        """
        Get the metadata to include in the head of the account's page.
        """
        return Meta(
            object_type="profile",
            extra_custom_props=[
                ("property", "profile.username", self.user.username),
                ("property", "profile.first_name", self.user.first_name),
                ("property", "profile.last_name", self.user.last_name),
            ] if self.user else [],
            title=self.display_name or self.name,
            image=self.image.large,
        )

    def save(self, *args, **kwargs):
        """
        Save this account.

        - Ensure that name is unique
        - If the account `is_personal` then make sure that the
          user's `username` is the same as `name`
        - If the account `is_customer` then update the Stripe
          `Customer` instance
        - Create an image if the account does not have one
        """
        self.name = unique_slugify(self.name, instance=self)

        if self.is_personal and self.user.username != self.name:
            self.user.username = self.name
            self.user.save()

        if self.is_customer:
            self.update_customer()

        if not self.image:
            self.set_image_from_name(should_save=False)

        return super().save(*args, **kwargs)

    # Methods related to billing of this account

    @property
    def is_customer(self) -> bool:
        """
        Is this account a customer (past or present).
        """
        return self.customer_id is not None

    def get_customer(self) -> djstripe.models.Customer:
        """
        Create a Stripe customer instance for this account (if necessary).

        Creates remote and local instances (instead of waiting for webhook notification).
        """
        if self.customer_id:
            return self.customer

        name = self.display_name or self.name or ""
        email = self.billing_email or self.email or ""

        if stripe.api_key != "sk_test_xxxx":
            try:
                customer = stripe.Customer.create(name=name, email=email)
                self.customer = djstripe.models.Customer.sync_from_stripe_data(
                    customer)
            except Exception:
                logger.exception("Error creating customer on Stripe")
        else:
            self.customer = djstripe.models.Customer.objects.create(
                id=shortuuid.uuid(), name=name, email=email)

        self.save()
        return self.customer

    def update_customer(self):
        """
        Update the Stripe customer instance for this account.

        Used when the account changes its name/s or email/s.
        Updates remote and local instances (instead of waiting for webhook notification).
        """
        customer = self.customer
        name = self.display_name or self.name or ""
        email = self.billing_email or self.email or ""

        if stripe.api_key != "sk_test_xxxx":
            try:
                stripe.Customer.modify(customer.id, name=name, email=email)
            except Exception:
                logger.exception("Error syncing customer with Stripe")

        customer.name = name
        customer.email = email
        customer.save()

    def get_customer_portal_session(self, request):
        """
        Create a customer portal session for the account.

        If the customer has no valid subscription then create one
        to the free account (this is necessary for the Stripe Customer Portal to
        work properly e.g. to be able to upgrade subscription).
        """
        customer = self.get_customer()

        has_subscription = False
        for subscription in customer.subscriptions.all():
            if subscription.is_valid():
                has_subscription = True
                break

        if not has_subscription:
            # At this point it would be good to subscribe the user to the selected product / tier
            # but with out a payment method, using anything other than the free product
            # raises the error 'This customer has no attached payment source or default payment method.'
            price = AccountTier.free_tier().product.prices.first()
            customer.subscribe(price=price)

        return stripe.billing_portal.Session.create(
            customer=customer.id,
            return_url=request.build_absolute_uri(
                reverse("ui-accounts-plan", kwargs={"account": self.name})),
        )

    # Methods to get "built-in" accounts
    # Optimized for frequent access by use of caching.

    @classmethod
    def get_stencila_account(cls) -> "Account":
        """
        Get the Stencila account object.
        """
        if not hasattr(cls, "_stencila_account"):
            cls._stencila_account = Account.objects.get(name="stencila")
        return cls._stencila_account

    @classmethod
    def get_temp_account(cls) -> "Account":
        """
        Get the 'temp' account object.

        This account owns all temporary projects.
        For compatability with templates and URL resolution
        it is easier and safer to use this temporary account
        than it is to allow `project.account` to be null.
        """
        if not hasattr(cls, "_temp_account"):
            cls._temp_account = Account.objects.get(name="temp")
        return cls._temp_account

    # Methods for setting the account image in various ways

    def image_is_identicon(self) -> bool:
        """
        Is the account image a default identicon.
        """
        filename = os.path.basename(self.image.name)
        return (filename.startswith("identicon")
                or re.match(r"[0-9a-f]{24}\.png", filename) is not None)

    def set_image_from_name(self, should_save: bool = False):
        """
        Set the image as an "identicon" based on the account name.

        Prefixes the file name of the image with identicon so that
        we can easily tell if it is a default and should be replaced by
        images that may be available from external accounts e.g. Google.
        """
        file = ContentFile(customidenticon.create(self.name, size=5))
        file.name = "identicon-" + shortuuid.uuid()
        self.image = file
        if should_save:
            self.save()

    def set_image_from_url(self, url: str):
        """
        Set the image from a URL.
        """
        response = httpx.get(url)
        if response.status_code == 200:
            file = ContentFile(response.content)
            file.name = "url-" + shortuuid.uuid()
            self.image = file
            self.save()

    def set_image_from_socialaccount(self,
                                     socialaccount: Union[str, SocialAccount]):
        """
        Set the image from a social account if possible.

        Does nothing for organizational accounts (where `self.user` is null).
        """
        if not self.image_is_identicon():
            return

        if not isinstance(socialaccount, SocialAccount):
            try:
                socialaccount = SocialAccount.objects.get(
                    user=self.user, provider=socialaccount)
            except SocialAccount.DoesNotExist:
                return

        url = None
        provider = socialaccount.provider
        data = socialaccount.extra_data
        if provider == "google":
            url = data.get("picture")
        elif provider == "github":
            url = data.get("avatar_url")
        elif provider == "twitter":
            url = data.get("profile_image_url")

        if url:
            self.set_image_from_url(url)

    def set_image_from_socialaccounts(self):
        """
        Set the image from any of the account's social accounts if possible.

        Does nothing for organizational accounts (where `self.user` is null)
        or if the image is already not an identicon.
        """
        if not self.image_is_identicon():
            return

        socialaccounts = SocialAccount.objects.filter(user=self.user)
        for socialaccount in socialaccounts:
            self.set_image_from_socialaccount(socialaccount)
            if not self.image_is_identicon():
                return
Exemple #25
0
class ContentMixin(TranslationMixin, MetaMixin, TemplateMixin):
    title = models.CharField(max_length=200, verbose_name=_("title"))
    slug = models.SlugField(verbose_name=_("slug"), max_length=180)
    author = models.ForeignKey(
        "people.Person",
        models.SET_NULL,
        verbose_name=_("author"),
        blank=True,
        null=True,
    )

    header_image = ImageField(
        _("header image"),
        formats={
            "full": ["default", "darken", ("crop", (1920, 900))],
            "square": ["default", ("crop", (920, 920))],
            "card": ["default", ("crop", (900, 600))],
            "mobile": ["default", ("crop", (740, 600))],
            "some": ["default", ("crop", (1200, 630))],
        },
        auto_add_fields=True,
        blank=True,
        null=True,
    )

    generated_meta_image = models.ImageField(
        _("generated meta image"), upload_to="meta", blank=True, null=True,
    )

    @property
    def image(self):
        try:
            if (
                settings.DEBUG or not self.generated_meta_image
            ) and self.get_header_image():
                orig = self.get_header_image()
                img = Image.open(
                    get_storage_class()().open(
                        self.get_header_image().some[1:].partition("/")[2]
                    )
                )
                draw = ImageDraw.Draw(img)
                font = ImageFont.truetype(
                    "juso/static/fonts/Montserrat-ExtraBold.ttf", int(1200 / 30)
                )
                color = ImageColor.getcolor(self.get_color(), "RGB")

                title = textwrap.wrap(self.title.upper(), 35, break_long_words=True)
                line = 0
                line_space = 10
                padding_top = 5
                padding_bottom = 14
                padding_side = 15
                line_height = int(1200 / 30) + line_space + padding_bottom + padding_top
                width = 1200
                height = 600

                text_top = height - len(title) * line_height - line_height / 2

                text_color = color
                fill_color = (255, 255, 255)
                border_color = color

                for text in title:
                    line += 1
                    size = font.getsize_multiline(text)
                    x = 30
                    y = text_top + line * line_height
                    draw.rectangle(
                        [
                            x - padding_side,
                            y - padding_top,
                            x + size[0] + padding_side,
                            y + size[1] + padding_bottom,
                        ],
                        fill=fill_color,
                        outline=border_color,
                        width=3,
                    )
                    draw.text(
                        (x, y), text, text_color, font=font,
                    )

                f = BytesIO()

                img.save(f, format="JPEG", quality=100)

                self.generated_meta_image.save(orig.some.split("/")[-1], files.File(f))
            return self.generated_meta_image
        except:  # Anything could happen, but it's not really a priority
            return None

    publication_date = models.DateTimeField(
        default=timezone.now, verbose_name=_("publication date")
    )

    created_date = models.DateTimeField(auto_now_add=True, verbose_name=_("created at"))

    edited_date = models.DateTimeField(auto_now=True, verbose_name=_("edited at"))

    category = models.ForeignKey(
        Category, models.SET_NULL, blank=True, null=True, verbose_name=_("category")
    )

    tags = TaggableManager(blank=True)

    section = models.ForeignKey(Section, models.CASCADE, verbose_name=_("section"),)

    def __str__(self):
        return self.title

    def get_header_image(self):
        if self.header_image:
            return self.header_image
        if self.category:
            return self.category.get_header_image()
        return None

    def get_color(self):
        if self.category:
            return self.category.color or settings.DEFAULT_COLOR
        return settings.DEFAULT_COLOR

    class Meta:
        abstract = True
        constraints = [
            models.UniqueConstraint(fields=["slug", "section"], name="slug_unique")
        ]
        indexes = [models.Index(fields=["slug"])]
        ordering = ["-publication_date"]
        get_latest_by = "publication_date"

    def meta_images_dict(self):
        if self.meta_image:
            return {
                "image": str(self.meta_image.recommended),
                "image:width": 1200,
                "image:height": 630,
            }
        if self.get_header_image():
            return {
                "image": str(self.get_header_image().some),
                "image:width": 1200,
                "image:height": 630,
            }
        return dict()
Exemple #26
0
class ModelWithOptional(models.Model):
    image = ImageField(_("image"),
                       upload_to="images",
                       blank=True,
                       auto_add_fields=True)
Exemple #27
0
class MetaMixin(models.Model):
    meta_title = models.CharField(
        _("title"),
        max_length=200,
        blank=True,
        help_text=_("Used for Open Graph and other meta tags."),
    )
    meta_description = models.TextField(
        _("description"),
        blank=True,
        help_text=_("Override the description for this page."),
    )
    meta_image = ImageField(
        _("image"),
        blank=True,
        auto_add_fields=True,
        upload_to="meta/%Y/%m",
        help_text=_("Set the Open Graph image."),
        formats={"recommended": ("default", ("crop", (1200, 630)))},
    )
    meta_video_url = models.URLField(
        _("video url"),
        blank=True,
        help_text=_("Set the Open Graph video to an url."),
    )
    meta_video = models.FileField(
        _("video"),
        blank=True,
        upload_to="meta/video/%Y/%m",
        help_text=_("Set the Open Graph video."),
        validators=[FileExtensionValidator(["mp4"])],
    )
    meta_video_width = models.IntegerField(
        _("video width"),
        default=1920,
    )
    meta_video_height = models.IntegerField(
        _("video height"),
        default=1080,
    )
    meta_card_type = models.CharField(
        _("twitter card type"),
        blank=True,
        max_length=50,
        choices=(
            ("summary", _("summary")),
            ("summary_large_image", _("summary large image")),
            ("player", "player"),
        ),
        help_text=_("Card type"),
    )
    meta_twitter_site = models.CharField(
        _("twitter site"),
        blank=True,
        max_length=30,
        help_text=_("The Twitter @username the card should be attributed to."),
    )
    meta_player_width = models.IntegerField(
        _("player width"),
        default=1920,
    )
    meta_player_height = models.IntegerField(
        _("player height"),
        default=1080,
    )
    meta_player = models.CharField(
        _("player url"),
        blank=True,
        max_length=600,
        help_text=_("HTTPS URL to iFrame player."),
    )
    meta_canonical = models.URLField(
        _("canonical URL"),
        blank=True,
        help_text=_("If you need this you probably know."),
    )
    meta_author = models.CharField(
        _("author"),
        max_length=200,
        blank=True,
        help_text=_("Override the author meta tag."),
    )
    meta_robots = models.CharField(
        _("robots"),
        max_length=200,
        blank=True,
        help_text=_("Override the robots meta tag."),
    )

    @property
    def description(self):
        return self.meta_description

    class Meta:
        abstract = True

    @classmethod
    def admin_fieldset(cls, **kwargs):
        cfg = {
            "fields": (
                "meta_title",
                "meta_description",
                "meta_image",
                "meta_image_ppoi",
                "meta_video",
                "meta_video_width",
                "meta_video_height",
                "meta_twitter_site",
                "meta_card_type",
                "meta_player",
                "meta_player_width",
                "meta_player_height",
                "meta_canonical",
                "meta_author",
                "meta_robots",
            ),
            "classes": ("tabbed", ),
        }
        cfg.update(kwargs)
        return (_("Meta tags"), cfg)

    def meta_dict(self):
        ctx = {
            "title": self.meta_title or getattr(self, "title", ""),
            "description": self.description,
            "canonical": self.meta_canonical,
            # Override URL if canonical is set to a non-empty value (the empty
            # string will be skipped when merging this dictionary)
            "url": self.meta_canonical,
            "author": self.meta_author,
            "robots": self.meta_robots,
            "twitter_site": self.meta_twitter_site,
            "card_type": self.meta_card_type,
        }
        ctx.update(self.meta_images_dict())
        ctx.update(self.meta_video_dict())
        ctx.update(self.meta_player_dict())
        return ctx

    def meta_images_dict(self):
        if self.meta_image:
            return {
                "image": str(self.meta_image.recommended),
                "image:width": 1200,
                "image:height": 630,
            }

        elif getattr(self, "image", None):
            return {"image": self.image.url}

        return {"image": ""}

    def meta_player_dict(self):
        if self.meta_player:
            return {
                "player": self.meta_player,
                "player_width": self.meta_player_width,
                "player_height": self.meta_player_height,
                "card_type": "player",
            }
        return {}

    def meta_video_dict(self):
        if self.meta_video:
            return {
                "video": self.meta_video.url,
                "video:width": self.meta_video_width,
                "video:height": self.meta_video_height,
            }
        if self.meta_video:
            return {
                "video": self.meta_video_url,
                "video:width": self.meta_video_width,
                "video:height": self.meta_video_height,
            }
        return {}
class Image(MediaBase):
    image_width = models.PositiveIntegerField(_("image width"),
                                              blank=True,
                                              null=True,
                                              editable=False)
    image_height = models.PositiveIntegerField(_("image height"),
                                               blank=True,
                                               null=True,
                                               editable=False)
    image_ppoi = PPOIField(_("primary point of interest"))
    # file = models.ImageField(_("Datei"))
    file = ImageField(
        _("image"),
        upload_to=UPLOAD_TO,
        width_field="image_width",
        height_field="image_height",
        ppoi_field="image_ppoi",
        blank=True,
    )

    logo_image = ImageSpecField(source='file',
                                processors=[ResizeToFit(150, 150)])

    thumbnail = ImageSpecField(
        source='file',
        processors=[Adjust(contrast=1.2, sharpness=1.1),
                    Thumbnail(100, 50)],
        format='JPEG',
        options={'quality': 90})

    square_image = ImageSpecField(source='file',
                                  processors=[ResizeToFill(800, 800)],
                                  format='JPEG',
                                  options={'quality': 90})

    small_article_image = ImageSpecField(source='file',
                                         processors=[ResizeToFit(400, 400)],
                                         format='JPEG',
                                         options={'quality': 90})

    article_image = ImageSpecField(source='file',
                                   processors=[ResizeToFit(800, 800)],
                                   format='JPEG',
                                   options={'quality': 90})

    gallery_image = ImageSpecField(source='file',
                                   processors=[ResizeToFit(1200, 1200)],
                                   format='JPEG',
                                   options={'quality': 90})

    gallery_image_thumbnail = ImageSpecField(
        source='file',
        processors=[
            Adjust(contrast=1.2, sharpness=1.1),
            # ResizeToFit(180, 120)
            ResizeToFit(220, 155)
        ],
        format='JPEG',
        options={'quality': 90})

    lightbox_image = ImageSpecField(source='file',
                                    processors=[ResizeToFit(1600, 1600)],
                                    format='JPEG',
                                    options={'quality': 90})
    highres_image = lightbox_image

    type = 'image'

    class Meta:
        verbose_name = _("Bild")
        verbose_name_plural = _("Bilder")
        ordering = ['imagegalleryrel__position']

    #
    # Accessors to GIF images
    # FIXME ImageKit should leave alone GIF images in the first place
    # TODO Need more robust method to get image type

    def gif_gallery_image_thumbnail(self,
                                    image_spec_name='gallery_image_thumbnail'):
        # Return gif image URLs without converting.
        name, ext = posixpath.splitext(self.file.name)
        if ext == '.gif':
            return self.file
        else:
            return getattr(self, image_spec_name)

    def gif_lightbox_image(self):
        return self.gif_gallery_image_thumbnail(
            image_spec_name='lightbox_image')
Exemple #29
0
class Page(
        AppsMixin,
        MetaMixin,
        TemplateMixin,
        RedirectMixin,
        MenuMixin,
        AbstractPage,
        LanguageMixin,
):
    APPLICATIONS = [('blog', _("blog"), {
        'urlconf':
        'blog.urls',
        'app_instance_namespace':
        lambda page: '-'.join(['blog', page.namespace.slug])
    }),
                    ('events', _("events"), {
                        'urlconf':
                        'events.urls',
                        'app_instance_namespace':
                        lambda page: '-'.join(['events'])
                    }),
                    ('collection', _("collection"), {
                        'urlconf':
                        "link_collections.urls",
                        "required_fields": ['collection'],
                        'app_instance_namespace':
                        lambda page: str(page.slug) + '-collections'
                    })]

    MENUS = (
        ('main', _("main")),
        ('footer', _("footer")),
        ('featured', _("featured")),
    )

    TEMPLATES = get_template_list('cms', (('default', ('main', 'footer')), ))

    namespace = models.ForeignKey('blog.Namespace',
                                  models.SET_NULL,
                                  blank=True,
                                  null=True,
                                  verbose_name=_("namespace"))

    collection = models.ForeignKey("link_collections.Collection",
                                   models.CASCADE,
                                   blank=True,
                                   null=True,
                                   verbose_name=_("collection"))

    image = ImageField(
        _("featured image"),
        upload_to='cms/',
        formats={
            'large': ['default', ('crop', (1920, 900))],
            'square': ['default', ('crop', (1024, 1024))],
            'preview': ['default', ('crop'), (1200, 600)],
        },
        auto_add_fields=True,
        blank=True,
        null=True,
    )

    def get_absolute_url(self, *args, **kwargs):
        site = current_site()
        if site == self.site:
            return super().get_absolute_url(*args, **kwargs)
        return '//' + self.site.host + super().get_absolute_url()

    class Meta:
        verbose_name = _("page")
        verbose_name_plural = _("page")
        ordering = ['position']
Exemple #30
0
class Person(models.Model):
    user = models.OneToOneField(
        User,
        models.SET_NULL,
        verbose_name=_("user"),
        blank=True,
        null=True,
    )

    sections = models.ManyToManyField(Section,
                                      blank=True,
                                      verbose_name=_("sections"))

    image = ImageField(
        _("image"),
        blank=True,
        null=True,
        upload_to="people/",
        auto_add_fields=True,
        formats={
            "square": ["default", ("crop", (900, 900))],
        },
    )

    first_name = models.CharField(max_length=100, verbose_name=_("first name"))
    last_name = models.CharField(max_length=100, verbose_name=_("last name"))

    email = models.EmailField(blank=True, verbose_name=_("e-mail"))
    homepage = models.URLField(blank=True, verbose_name=_("homepage"))
    phone = models.CharField(blank=True,
                             max_length=20,
                             verbose_name=_("phone"))

    facebook = models.URLField(blank=True, verbose_name=_("Facebook"))
    twitter = models.URLField(blank=True, verbose_name=_("Twitter"))
    instagram = models.URLField(blank=True, verbose_name=_("Instagram"))

    job = models.CharField(max_length=120, blank=True, verbose_name=_("job"))
    birthday = models.DateField(_("birthday"), blank=True, null=True)
    city = models.CharField(max_length=120, blank=True)

    bio = CleansedRichTextField(blank=True)

    def __str__(self):
        return self.first_name + " " + self.last_name

    class Meta:
        verbose_name = _("person")
        verbose_name_plural = _("people")

        ordering = ["last_name", "first_name"]

    def description(self):
        return bleach.clean(self.bio, strip=True, tags=[])

    def get_full_name(self):
        return f"{self.first_name} {self.last_name}"

    def get_absolute_url(self):
        try:
            site = current_site()
            return reverse_app([f"{site.id}-people"],
                               "person-detail",
                               kwargs={"pk": self.pk})
        except NoReverseMatch:
            return ""