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}"
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
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
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"]
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
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"
class SlowStorageImage(models.Model): image = ImageField( _("image"), upload_to="images", auto_add_fields=True, storage=slow_storage, formats={"thumb": ["default", ("crop", (20, 20))]}, )
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})"
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"
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
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'
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'
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']
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)
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'
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']
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")
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 "#"
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": ""}
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
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()
class ModelWithOptional(models.Model): image = ImageField(_("image"), upload_to="images", blank=True, auto_add_fields=True)
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')
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']
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 ""