class Good(models.Model): class Meta: ordering = ( '-price', 'name', ) unique_together = ( 'category', 'name', 'price', ) verbose_name = 'good' verbose_name_plural = 'goods' name = models.CharField(max_length=50, unique=True, verbose_name='Name') description = models.TextField() category = models.ForeignKey(Category, null=True, blank=True, on_delete=models.SET_NULL, related_name='goods') in_stock = models.BooleanField(default=True, db_index=True, verbose_name='In stock') price = models.FloatField() tags = TaggableManager(blank=True) slug = AutoSlugField( populate_from=get_slug, unique=True ) objects = GoodQuerySet.as_manager() def __str__(self): return self.name if self.in_stock else f'{self.name} (out of stock)' def get_in_stock(self): return '+' if self.in_stock else '' def get_absolute_url(self): return reverse('goods:detail', kwargs={'slug': self.slug})
class Post(models.Model): # pragma pylint: disable=R0903 class Meta: ordering = ('-created', ) verbose_name = 'Blog Post' verbose_name_plural = 'Blog Posts' # pragma pylint: enable=R0903 user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) title = models.CharField(max_length=100, unique_for_date='created') body = MarkdownField() slug = AutoSlugField(populate_from='title', unique=True) is_commentable = models.BooleanField(default=True) tags = TaggableManager(blank=True) created = models.DateTimeField(auto_now_add=True, db_index=True) published = models.DateTimeField(null=True, blank=True) updated = models.DateTimeField(auto_now=True) objects = PostQuerySet.as_manager() def __str__(self): return self.title def get_absolute_url(self): return reverse('post:detail', kwargs={'slug': self.slug})
class Model3D(models.Model): creation_date = models.DateTimeField(default=timezone.now) search_img = models.ForeignKey('Image', on_delete=models.DO_NOTHING, related_name='search_img', null=True) description = models.CharField(max_length=500, default="Empty", unique=True) name = models.CharField(max_length=200) # tags mechanism tags = TaggableManager(blank=True) class Meta: verbose_name_plural = "3DModels" ordering = ("-pk", ) permissions = (("can_tag", "Allow normal user to tag model"), ) def __str__(self): return self.description def get_absolute_url(self): return reverse( "gallery:single_model", kwargs={"pk": self.pk}, ) def delete(self, *args, **kwargs): """Custom delete method to all images of this model""" self.search_img = None self.save() for i in Image.objects.filter(linked_model=self.pk): i.delete() super().delete(*args, **kwargs)
class Issue(TimeStampedModel): number = models.PositiveSmallIntegerField(_("number"), unique=True) pub_date = models.DateTimeField(_("publication datetime")) description = models.TextField(_("description")) creator = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_("creator"), on_delete=models.SET_NULL, null=True, ) tags = TaggableManager(verbose_name=_("tags"), through=TaggedItem, blank=True) class Meta: ordering = ["-number"] verbose_name = _("issue") verbose_name_plural = _("issues") def __str__(self): return "{}".format(self.number) def get_absolute_url(self): return reverse("favorites:issue_detail", kwargs={"number": self.number})
class Blog(models.Model): title = models.CharField(max_length=255) slug = models.SlugField(editable=True, max_length=255, unique=True) body = models.TextField() date = models.DateTimeField(auto_now_add=True) tags = TaggableManager() def __str__(self): return self.title
class Task(TimeStampedModel, MP_Node): ROOT_USERNAME = '******' labels = TaggableManager(blank=True) title = models.TextField() info = HTMLField("info", max_length=2000, blank=True) complete = models.DateTimeField(blank=True, null=True) duration = models.TextField(null=True, default=None) priority = models.PositiveSmallIntegerField( default=1, validators=[MaxValueValidator(3), MinValueValidator(1)]) expanded = models.BooleanField(default=True) # move: https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.move @classmethod def ordered_tasks(cls): tasks = list(Task.objects.all()) #filter(expanded=True)) tree = {} task: Task for task in tasks: pass return tasks @property def start_iso(self): if self.complete is None: return None start = self.complete - datetime.timedelta(minutes=int(self.duration)) return start.isoformat() @property def end_iso(self): if self.complete is None: return None end: datetime.datetime = self.complete return end.isoformat() def __unicode__(self): return self.complete.isoformat()
class Post(ModelMeta, TimeStampModel): author = models.ForeignKey('users.User') title = models.CharField(max_length=200) slug = AutoSlugField(_('slug'), max_length=50, editable=True, populate_from=('title', )) text = MarkdownxField() publish = models.BooleanField(default=False) tags = TaggableManager() _metadata = {'title': 'title', 'author': 'author', 'image': 'get_img_url'} class Meta: verbose_name_plural = "posts" @property def formatted_markdown(self): return markdownify(self.text) def get_absolute_url(self): return reverse('blog:detail', kwargs={'pk': self.pk}) def create_meta_description(self): pass # TODO # description = self.text.split(' ') # word_list = description[0:10] # string = '\s'.join(e for e in word_list) # clean_description = re.sub('[^A-Za-z0-9\s]+', '', string45r) # return clean_description def get_img_url(self): regex = "\!\[.*\]\(([^)]+)" urls = re.findall(regex, self.text) if len(urls) > 0: return urls[0] else: return "" def __str__(self): return self.title def publish_post(self): self.publish = True self.save()
class Todo(models.Model): class Meta: ordering = ('-created_at', ) title = models.CharField(max_length=200) text = MarkdownField() created_at = models.DateTimeField(auto_now_add=True, blank=True) tags = TaggableManager(blank=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) objects = TodoQuerySet.as_manager() def __str__(self): return self.title def get_absolute_url(self): return reverse('todos:detail', kwargs={'pk': self.pk})
class Listing(models.Model): description = models.CharField(max_length=500, blank=True) name = models.CharField(max_length=100, blank=False) link = models.URLField(max_length=500, unique=True, blank=True) # tags mechanism tags = TaggableManager(blank=True) class Meta: verbose_name_plural = "Listings" permissions = (("can_tag", "Allow normal user to tag listing"), ) def __str__(self): return self.link + '(' + self.description + ')' def get_absolute_url(self): return reverse( "listing:single", kwargs={"pk": self.pk}, )
class Post(models.Model): title = models.CharField(max_length=250, verbose_name='Tytuł') slug = models.SlugField(unique=True, max_length=250) body = models.TextField( verbose_name='Treść', help_text= 'Aby inaczej sformatować tekst, zaznacz fragment tekstu, który chcesz zmienić i kliknij wybraną ikonę.' ) author = models.ForeignKey(User, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) tags = TaggableManager() active = models.BooleanField(default=False) image = models.ImageField( upload_to='post_image', blank=True, verbose_name='Miniatura postu', help_text= 'Aby nie łamać praw autorskich, warto skorzystać z darmowych zdjęć na stocksnap.io, unsplash.com lub pexels.com. Warto jednak pamiętać o rozdzielczości' ) class Meta: ordering = ['-created_at'] def __str__(self): return self.title def get_absolute_url(self): return reverse('blog:tresc_postu', args=[self.slug]) def _get_unique_slug(self): slug = slugify(self.title) unique_slug = slug num = 1 while Post.objects.filter(slug=unique_slug).exists(): unique_slug = '{}-{}'.format(slug, num) num += 1 return unique_slug def save(self, *args, **kwargs): if not self.slug: self.slug = self._get_unique_slug() super().save()
class Favorite(TimeStampedModel): issue = models.ForeignKey( Issue, verbose_name=_("issue"), on_delete=models.SET_NULL, null=True, related_name="favorites", ) title = models.CharField(_("title"), max_length=200) description = models.TextField(_("description")) url = models.URLField(_("url")) rank = models.IntegerField(_("rank"), default=0) tags = TaggableManager(verbose_name=_("tags"), through=TaggedItem, blank=True) class Meta: ordering = ["rank", "-created_at"] verbose_name = _("favorite") verbose_name_plural = _("favorites") def __str__(self): return self.title
class Tag(models.Model): post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='tag') tags = TaggableManager(blank=True, help_text=None, verbose_name='')
class Profile(models.Model): GENDER_CHOICES = ( ('male', 'Male'), ('female', 'Female'), ('other', 'Other'), ) YEAR_OF_ENTRANCE = ( ('2015', '2015'), ('2016', '2016'), ('2017', '2017'), ('2018', '2018'), ('2019', '2019'), ) YEAR_OF_GRADUATION = ( ('2020', '2020'), ('2021', '2021'), ('2022', '2022'), ('2023', '2023'), ('2024', '2024'), ) user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') gender = models.CharField(max_length=10, choices=GENDER_CHOICES) profile_photo = models.ImageField(upload_to='profile_pictures/', blank=True) bio = models.CharField(max_length=145, blank=True) interests = TaggableManager(verbose_name='Interests', help_text='football, programming, photography') phone_number = models.CharField(max_length=20, blank=True) department = models.CharField(max_length=30, blank=True) level = models.IntegerField(blank=True, null=True) staff_status = models.BooleanField(default=False) verified = models.BooleanField(default=False) year_of_entrance = models.CharField(max_length=10, default='2020', choices=YEAR_OF_ENTRANCE, blank=True) year_of_graduation = models.CharField(max_length=10, blank=True, null=True, choices=YEAR_OF_GRADUATION) email_confirmed = models.BooleanField(default=False) secret_code = models.CharField(null=True, blank=True, max_length=32, unique=True) def __str__(self): return f'profile for user {self.user.username}' def save(self, *args, **kwargs): if self.profile_photo: self.thumbnail = make_thumbnail(self.profile_photo, size=(90, 90)) if not self.secret_code: profiles = Profile.objects.all() codes = [code.secret_code for code in profiles] confirm_new_secret = False while not confirm_new_secret: new_secret = random.randint(1000000000000000000000000000000, 99999999999999999999999999999999) if new_secret not in codes: self.secret_code = new_secret confirm_new_secret = True super().save(*args, **kwargs)
class Post(RulesModelMixin, auto_prefetch.Model, metaclass=RulesModelBase): """Post Model A model for Blog Posts Args: RulesModelMixin ([type]): [description] auto_prefetch ([type]): [description] metaclass ([type], optional): [description]. Defaults to RulesModelBase. Returns: Post: A model for Post """ PUBLICATION_CHOICES = [ ("P", "Published"), ("W", "Withdrawn"), ("D", "Draft"), ] FEATURING_CHOICES = [ ("F", "Featured"), ("FB", "Featured Big"), ("N", "Not Featured"), ] LANGUAGE_CHOICES = [ ("EN", "English"), ("FR", "French"), ("ML", "Multi Linguistic"), ("OL", "Other Language"), ("NS", "Not Specified"), ] author = auto_prefetch.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="post_author", help_text="Post author", ) title = models.CharField(max_length=200, help_text="Post title") featured_title = models.CharField(max_length=27, default="", blank=True, help_text="Featured post title") body = MarkdownxField(help_text="Post main content", blank=True) image = models.ImageField(null=True, blank=True, upload_to="images/post/", help_text="Post image") description = models.TextField(help_text="Post description") slug = models.SlugField(unique=True, max_length=200, help_text="Post slug") categories = models.ManyToManyField("blog.Category", through="PostCatsLink", help_text="Post categories") tags = TaggableManager(blank=True, through=CustomTaggedItem) series = auto_prefetch.ForeignKey( "blog.Series", on_delete=models.CASCADE, related_name="post_series", help_text="Post series", blank=True, null=True, ) order_in_series = models.PositiveIntegerField( default=0, help_text="Post order in its series") created_date = models.DateTimeField(default=timezone.now, help_text="Creation date") mod_date = models.DateTimeField(auto_now=True, help_text="Last modification") pub_date = models.DateTimeField(blank=True, null=True, help_text="Publication date") publication_state = models.CharField( max_length=25, verbose_name="Publication", choices=PUBLICATION_CHOICES, default="D", help_text="Post publication state", ) withdrawn = models.BooleanField(default=False, help_text="Is Post withdrawn") featuring_state = models.CharField( max_length=25, verbose_name="Featuring", choices=FEATURING_CHOICES, default="N", help_text="Featuring state", ) language = models.CharField( max_length=25, verbose_name="Language", choices=LANGUAGE_CHOICES, default="EN", help_text="What's the main language", ) is_outdated = models.BooleanField(default=False, help_text="Is Post content's outdated") url_to_article = models.URLField( default="", blank=True, help_text="Url to page that inspired the Post") url_to_article_title = models.CharField( max_length=200, default="", blank=True, help_text="What will be shown as url name", ) clicks = models.IntegerField( default=0, help_text="How many times the Post has been seen") rnd_choice = models.IntegerField( default=0, help_text="How many times the Post has been randomly chosen") history = HistoricalRecords() is_removed = models.BooleanField("is removed", default=False, db_index=True, help_text=("Soft delete")) needs_reviewing = models.BooleanField(default=False, help_text=("Needs reviewing")) enable_comments = models.BooleanField(default=True) objects: PostManager = PostManager() class Meta: """Meta class for Post Model""" ordering = ["-pub_date"] get_latest_by = ["id"] rules_permissions = { "add": rules.is_superuser, "update": rules.is_superuser, } def __str__(self): return self.title def save(self, *args, **kwargs): if not self.slug: max_length = Post._meta.get_field("slug").max_length self.slug = orig = slugify(self.title)[:max_length] for x in itertools.count(2): if (self.pk and Post.objects.filter( Q(slug=self.slug), Q(author=self.author), Q(id=self.pk), ).exists()): break if not Post.objects.filter(slug=self.slug).exists(): break # Truncate & Minus 1 for the hyphen. self.slug = "%s-%d" % (orig[:max_length - len(str(x)) - 1], x) return super().save(*args, **kwargs) def save_without_historical_record(self, *args, **kwargs): self.skip_history_when_saving = True try: ret = self.save(*args, **kwargs) finally: del self.skip_history_when_saving return ret def get_absolute_url(self): return reverse("blog:post_detail", kwargs={"slug": self.slug}) def get_absolute_update_url(self): return reverse("blog:post_edit", kwargs={"slug": self.slug}) def get_absolute_needs_review_url(self): return reverse("blog:post_needs_review", kwargs={"slug": self.slug}) def get_absolute_delete_url(self): return reverse("blog:post_remove", kwargs={"slug": self.slug}) def get_absolute_publish_url(self): return reverse("blog:post_publish", kwargs={"slug": self.slug}) def get_absolute_publish_withdrawn_url(self): return reverse("blog:post_publish_withdrawn", kwargs={"slug": self.slug}) def get_absolute_clone_url(self): return reverse("blog:clone_post", kwargs={"slug": self.slug}) def get_absolute_admin_update_url(self): return reverse("admin:blog_post_change", kwargs={"object_id": self.pk}) def publish(self): self.pub_date = timezone.now() self.publication_state = "P" self.withdrawn = False self.save() def publish_withdrawn(self): self.pub_date = timezone.now() self.publication_state = "W" self.withdrawn = True self.save() def needs_review(self): self.needs_reviewing = True self.save() def clicked(self): self.clicks += 1 self.save_without_historical_record(update_fields=["clicks"]) def rnd_chosen(self): self.rnd_choice += 1 self.save_without_historical_record(update_fields=["rnd_choice"]) def was_published_recently(self): now = timezone.now() return now - datetime.timedelta(days=1) <= self.pub_date <= now def remove(self): self.is_removed = True self.save() # Create a property that returns the markdown instead @property def formatted_markdown(self): return markdownify(self.body) @property def is_scheduled(self) -> bool: now = timezone.now() if not self.pub_date: return False return self.pub_date >= now @property def author_name(self): return self.author.username @property def get_tags(self): return self.tags.filter(withdrawn=False) @property def get_admin_tags(self): return self.tags.all() @property def get_featured_cat(self): for post_cat in PostCatsLink.objects.filter( post_id=self.pk, category__is_removed=False, featured_cat=True).select_related("post", "category"): return post_cat @property def featured_cat_title(self): for post_cat in PostCatsLink.objects.filter( post_id=self.pk, category__is_removed=False, featured_cat=True).select_related("post", "category"): return post_cat.category.full_title def get_index_view_url(self): content_type = ContentType.objects.get_for_model(self.__class__) return reverse("%s:%s_list" % (content_type.app_label, content_type.model))
class Photo(index.Indexed, models.Model): REVIEW_STATUS_SUBMITTING = -1 REVIEW_STATUS_SUBMITTED = 0 REVIEW_STATUS_PUBLIC = 1 REVIEW_STATUS_ARCHIVED = 2 REVIEW_STATUSES = ( (REVIEW_STATUS_SUBMITTED, 'To be reviewed (not public)'), (REVIEW_STATUS_PUBLIC, 'Public'), (REVIEW_STATUS_ARCHIVED, 'Archived (not public)'), (REVIEW_STATUS_SUBMITTING, 'Incomplete submission'), ) FEELING_POSITIVE = -1 FEELING_NEUTRAL = 0 FEELING_NEGATIVE = 1 FEELINGS = ( (FEELING_POSITIVE, 'Positive'), (FEELING_NEUTRAL, 'Neutral'), (FEELING_NEGATIVE, 'Negative'), ) MONTHS = [(i + 1, name) for i, name in enumerate(calendar.month_name[1:])] photographer = models.ForeignKey( Photographer, on_delete=models.SET_NULL, null=True, blank=True, default=None ) # public = models.BooleanField(default=False) # image = models.ImageField(upload_to='photos', blank=True, null=True) image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.CASCADE, related_name='+' ) legacy_categories = models.TextField( 'Legacy categories, for reference only', blank=True, default='' ) description = models.TextField(blank=True, default='') comments = models.TextField( 'Internal comments', blank=True, null=True ) created_at = models.DateTimeField( auto_now_add=True ) updated_at = models.DateTimeField(auto_now=True) taken_year = models.IntegerField( 'Year (photo content)', blank=True, null=True, default=None) taken_month = models.IntegerField( 'Month (photo content)', choices=MONTHS, blank=True, null=True, default=None ) taken_day = models.IntegerField( 'Day (photo content)', blank=True, null=True, default=None ) location = models.PointField(blank=True, null=True) review_status = models.IntegerField(choices=REVIEW_STATUSES, default=0) subcategories = models.ManyToManyField(PhotoSubcategory, blank=True) # data captured by the submission form author_focus_keywords = models.CharField( max_length=150, blank=True, null=True, default=None, help_text='Author\'s main focus in a few keywords' ) author_focus = models.TextField( blank=True, default='', help_text='Author\'s main focus' ) author_feeling_category = models.IntegerField( choices=FEELINGS, blank=True, null=True, default=None, help_text='Author\'s feeling about this photo' ) author_feeling_keywords = models.CharField( max_length=100, blank=True, default='', help_text='Keyword describing author\'s feelings about this photo' ) author_reason = models.TextField( 'Motivation', blank=True, default='', help_text='Why did the author take this picture?', max_length=500, ) reference_number = models.CharField( 'Reference number', max_length=20, blank=True, default='', ) tags = TaggableManager(through=PhotoTag, blank=True) panels = [ SnippetChooserPanel('photographer'), FieldPanel('review_status'), FieldPanel('number'), ImageChooserPanel('image'), FieldPanel('subcategories'), FieldPanel('description'), FieldPanel('comments'), FieldPanel('date'), FieldPanel('location'), FieldPanel('tags'), ] search_fields = [ index.FilterField('id'), index.FilterField('review_status'), # index.FilterField('subcategories__pk'), # index.SearchField('subcategories__pk'), # index.FilterField('photosubcategory_id'), index.FilterField('image_id'), index.SearchField('description'), index.SearchField('tags__name'), # index.RelatedFields('subcategories', [ # index.FilterField('id'), # ]), index.RelatedFields('tags', [ index.SearchField('name'), ]), # two filters with the content... # this one is mandatory for all types of backends (filter()) index.FilterField('photosubcategory_id'), # this one is mandatory for default backend (facet()) index.FilterField('subcategories__pk'), ] def photosubcategory_id(self): """ https://stackoverflow.com/questions/43082438/how-do-you-filter-search-results-in-wagtail-based-on-a-manytomanyfield """ return list(self.subcategories.all().values_list('id', flat=True)) def subcategories__pk(self): return self.photosubcategory_id() class Meta: ordering = ['-created_at'] # def __init__(self, *args, **kwargs): # super(Photo, self).__init__(*args, **kwargs) # print(self.pk) def __str__(self): # TODO: find a better way to show pictures on the snippet list page # than embedding html tag here. return mark_safe('[{}] {}: {}<br>{}'.format( self.review_status_label, self.photographer, self.title, self.get_image_tag('height-50') )) def save(self, *args, **kwargs): if not self.reference_number: import time self.reference_number = ('%X' % int(time.time())).replace('0', 'X') super().save(*args, **kwargs) def location_nw(self): return get_nw_string_from_point(self.location) @property def taken_month_name(self): from calendar import month_name ret = month_name[self.taken_month] if self.taken_month else '' return ret @property def review_status_label(self): ret = dict(self.REVIEW_STATUSES)[self.review_status] return ret @property def title(self): '''Derives a title from self.description''' ret = self.description or '' max_len = 50 if len(ret) > max_len: # heuristics to keep smallest meaningful beginning of the desc. ret = re.sub(r'\(.*?\)', '', ret) ret = re.sub(r'( -|--).*$', '', ret) ret = re.sub(r'[.,;:].*$', '', ret) ret = re.sub(r'(([A-Z]\w*\b\s*){2,}).*$', r'\1', ret) ret = ret.strip() if len(ret) > max_len: # still too long, truncate and add the ellipsis ret = ret[:max_len] + '...' return ret def get_image_tag(self, specs='height-500'): ''' Returns an html <image> tag for the related image. See Wagtail get_rendition_or_not_found() for valid specs. ''' ret = '' if self.image: rendition = get_rendition_or_not_found(self.image, specs) # Remove height and width to keep things responsive. # they will be set by CSS. ret = re.sub(r'(?i)(height|width)=".*?"', '', rendition.img_tag()) return ret def get_json_dic(self, imgspecs=None, geo_only=False): '''Returns a python dictionary representing this instance This dictionary will be converted into javascript by the web API ''' p = self # TODO: should be dynamic, based on client (smaller for mobile devices) type_slug = 'photos' location = None if p.location: location = [p.location.y, p.location.x] if geo_only: ret = OrderedDict([ ['id', str(p.pk)], ['type', type_slug], ['attributes', { 'location': location, }] ]) else: ret = OrderedDict([ ['id', str(p.pk)], ['type', type_slug], ['attributes', { 'title': p.title, 'description': p.description, 'location': location, 'image': self.get_image_tag(imgspecs), # TODO: don't hard-code this! 'url': '/{}/{}'.format(type_slug, p.pk), 'taken_year': p.taken_year, 'taken_month': p.taken_month, 'taken_month_name': p.taken_month_name, 'taken_day': p.taken_day, 'created_at': p.created_at, }] ]) return ret def get_absolute_url(self): from django.urls import reverse return reverse('photo-view', args=[str(self.pk)])
class PageModel(models.Model): class_name = models.CharField(max_length=200, blank=True) title = models.CharField(max_length=200) content = RichTextField(null=True, blank=True) slug = models.SlugField(unique=True, db_index=True, blank=True, null=True) site = models.ForeignKey(Site, related_name="site_page", on_delete=models.CASCADE, null=True, blank=True) owner = models.ForeignKey(Member, null=True, blank=True, on_delete=models.CASCADE, related_name="page_owner", verbose_name='owner') created_date = models.DateTimeField(db_index=True, default=datetime.datetime.now) is_published = models.BooleanField(default=False, db_index=True) is_preview = models.BooleanField(default=False, db_index=True) banner_image_1 = models.ImageField( upload_to='cp/user_uploads/banner_images/', null=True, blank=True) banner_image_2 = models.ImageField( upload_to='cp/user_uploads/banner_images/', null=True, blank=True) banner_image_3 = models.ImageField( upload_to='cp/user_uploads/banner_images/', null=True, blank=True) meta_description = models.CharField(max_length=1000, default="", blank=True) meta_keyword = TaggableManager(blank=True) page_view = models.IntegerField(default=0) def __str__(self): return self.title.title() def get_banner_image_1_url(self): return ("/media/%s" % self.banner_image_1) def get_banner_image_2_url(self): return ("/media/%s" % self.banner_image_2) def get_banner_image_3_url(self): return ("/media/%s" % self.banner_image_3) def get_page_url(self): return "/%s/" % (self.slug) def get_absolute_url(self): return "/%s/" % (self.slug) def get_edit_url(self): return "%s" % (reverse('cms:page_edit_delete', kwargs={ 'action': 'edit', 'pk': self.pk })) def get_delete_url(self): return "%s" % (reverse('cms:page_edit_delete', kwargs={ 'action': 'delete', 'pk': self.pk })) def get_class_name(self): return self.class_name
class File(models.Model): """Model representing a File """ name = models.CharField(max_length=200, verbose_name='Τίτλος') summary = models.TextField(max_length=1000, help_text='Περιγραφή του αρχείου', verbose_name='Περιγραφή') slug = AutoSlugField(populate_from='name', unique=True, null=True, default=None) # Foreign Key used because a file can only have one author, but authors can have multiple files # Author as a string rather than object because it hasn't been declared yet in the file # Date of uploading the file # dateCreated = models.DateTimeField(auto_now_add=True) category = models.ForeignKey(Category, help_text='Επιλέξτε κατηγορία', verbose_name='Κατηγορία', on_delete=models.CASCADE) area = models.ManyToManyField(Area, help_text='Επιλέξτε Επιστημονική κατηγορία', verbose_name='Επιστημονική ' 'κατηγορία') tags = TaggableManager(blank=True) file = models.FileField(upload_to='files', null=True, verbose_name='Αρχείο') thumbnail = models.ImageField(upload_to='thumbnail', null=True, verbose_name='Εικόνα αρχείου') uploader = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) author = models.CharField(max_length=100, help_text='Δημιουργός', verbose_name='Δημιουργός') author_email = models.CharField(max_length=100, help_text='email δημιουργού', verbose_name='Email δημιουργού') allow_comments = models.BooleanField('allow comments', default=True) ratings = GenericRelation(Rating, related_query_name='foos') def __str__(self): """String for representing the Model object.""" return self.name def get_absolute_url(self): return reverse('file_detail', kwargs={'slug': self.slug}) def get_comments(self): return Comment.objects.all().filter(object_pk=self.pk).count() def comm(self): aggregate = File.objects.aggregate(comment_count=File('comments')) return aggregate['comment_count'] + 1 class Meta: verbose_name = 'Αρχείο' verbose_name_plural = 'Αρχεία'
class Article(models.Model): class_name = models.CharField(max_length=200, blank=True) category = models.ManyToManyField(Category, related_name="article_category", blank=True) slug = models.SlugField(max_length=200, unique=True, db_index=True, blank=True, null=True) tags = TaggableManager(blank=True) title = models.CharField(max_length=200) lead_in = models.CharField(max_length=1000, default="", blank=True) content = RichTextField(null=True, blank=True) site = models.ForeignKey(Site, related_name="article_site", on_delete=models.CASCADE, null=True, blank=True) author = models.ForeignKey(Member, null=True, blank=True, on_delete=models.CASCADE, related_name="article_author", verbose_name='author') owner = models.ForeignKey(Member, null=True, blank=True, on_delete=models.CASCADE, related_name="article_owner", verbose_name='owner') created_date = models.DateTimeField(db_index=True, default=datetime.datetime.now) published_date = models.DateTimeField(db_index=True, null=True, blank=True) is_published = models.BooleanField(default=True, db_index=True) is_featured = models.BooleanField(default=False, db_index=True) is_preview = models.BooleanField(default=False, db_index=True) featured_image = models.ImageField( upload_to='cp/user_uploads/featured_images/', null=True, blank=True) page_view = models.PositiveIntegerField(default=0) def __str__(self): return self.title.title() def get_all_tags(self): return self.tags.all() def get_image_url(self): return "%s" % ("/media/%s" % self.featured_image) def get_article_url(self): return "%s" % (reverse('blog_detail', kwargs={ 'kategori': self.category.all()[0].slug, 'slug': self.slug })) def get_edit_url(self): return "%s" % (reverse('cms:article_edit_delete', kwargs={ 'action': 'edit', 'pk': self.pk })) def get_delete_url(self): return "%s" % (reverse('cms:article_edit_delete', kwargs={ 'action': 'delete', 'pk': self.pk })) def get_class_name(self): return self.class_name
class Document(ModelIndexable): """A unified document such as a letter or legal document that appears on one or more fragments.""" id = models.AutoField("PGPID", primary_key=True) fragments = models.ManyToManyField( Fragment, through="TextBlock", related_name="documents" ) description = models.TextField(blank=True) doctype = models.ForeignKey( DocumentType, blank=True, on_delete=models.SET_NULL, null=True, verbose_name="Type", help_text='Refer to <a href="%s" target="_blank">PGP Document Type Guide</a>' % settings.PGP_DOCTYPE_GUIDE, ) tags = TaggableManager(blank=True, related_name="tagged_document") languages = models.ManyToManyField( LanguageScript, blank=True, verbose_name="Primary Languages" ) secondary_languages = models.ManyToManyField( LanguageScript, blank=True, related_name="secondary_document" ) language_note = models.TextField( blank=True, help_text="Notes on diacritics, vocalisation, etc." ) notes = models.TextField(blank=True) created = models.DateTimeField(auto_now_add=True) last_modified = models.DateTimeField(auto_now=True) needs_review = models.TextField( blank=True, help_text="Enter text here if an administrator needs to review this document.", ) old_pgpids = ArrayField(models.IntegerField(), null=True, verbose_name="Old PGPIDs") PUBLIC = "P" STATUS_PUBLIC = "Public" SUPPRESSED = "S" STATUS_SUPPRESSED = "Suppressed" STATUS_CHOICES = ( (PUBLIC, STATUS_PUBLIC), (SUPPRESSED, STATUS_SUPPRESSED), ) PUBLIC_LABEL = "Public" #: status of record; currently choices are public or suppressed status = models.CharField( max_length=2, choices=STATUS_CHOICES, default=PUBLIC, help_text="Decide whether a document should be publicly visible", ) # preliminary date fields so dates can be pulled out from descriptions doc_date_original = models.CharField( "Date on document (original)", help_text="explicit date on the document, in original format", blank=True, max_length=255, ) CALENDAR_HIJRI = "h" CALENDAR_KHARAJI = "k" CALENDAR_SELEUCID = "s" CALENDAR_ANNOMUNDI = "am" CALENDAR_CHOICES = ( (CALENDAR_HIJRI, "Hijrī"), (CALENDAR_KHARAJI, "Kharājī"), (CALENDAR_SELEUCID, "Seleucid"), (CALENDAR_ANNOMUNDI, "Anno Mundi"), ) doc_date_calendar = models.CharField( "Calendar", max_length=2, choices=CALENDAR_CHOICES, help_text="Calendar according to which the document gives a date: " + "Hijrī (AH); Kharājī (rare - mostly for fiscal docs); " + "Seleucid (sometimes listed as Minyan Shetarot); Anno Mundi (Hebrew calendar)", blank=True, ) doc_date_standard = models.CharField( "Document date (standardized)", help_text="CE date (convert to Julian before 1582, Gregorian after 1582). " + "Use YYYY, YYYY-MM, YYYY-MM-DD format when possible", blank=True, max_length=255, ) footnotes = GenericRelation(Footnote, related_query_name="document") log_entries = GenericRelation(LogEntry, related_query_name="document") # NOTE: default ordering disabled for now because it results in duplicates # in django admin; see admin for ArrayAgg sorting solution class Meta: pass # abstract = False # ordering = [Least('textblock__fragment__shelfmark')] def __str__(self): return f"{self.shelfmark or '??'} (PGPID {self.id or '??'})" @staticmethod def get_by_any_pgpid(pgpid): """Find a document by current or old pgpid""" return Document.objects.filter( models.Q(id=pgpid) | models.Q(old_pgpids__contains=[pgpid]) ).first() @property def shelfmark(self): """shelfmarks for associated fragments""" # access via textblock so we follow specified order, # use dict keys to ensure unique return " + ".join( dict.fromkeys( block.fragment.shelfmark for block in self.textblock_set.all() if block.certain # filter locally instead of in the db ) ) @property def certain_join_shelfmarks(self): return list( dict.fromkeys( block.fragment.shelfmark for block in self.textblock_set.filter(certain=True) ).keys() ) # NOTE: not currently used; remove or revise if this remains unused @property def shelfmark_display(self): """First shelfmark plus join indicator for shorter display.""" # NOTE preliminary pending more discussion and implementation of #154: # https://github.com/Princeton-CDH/geniza/issues/154 certain = self.certain_join_shelfmarks if not certain: return None return certain[0] + (" + …" if len(certain) > 1 else "") @property def collection(self): """collection (abbreviation) for associated fragments""" # use set to ensure unique; sort for reliable output order return ", ".join( sorted( set( [ block.fragment.collection.abbrev for block in self.textblock_set.all() if block.fragment.collection ] ) ) ) def all_languages(self): return ", ".join([str(lang) for lang in self.languages.all()]) all_languages.short_description = "Language" def all_secondary_languages(self): return ",".join([str(lang) for lang in self.secondary_languages.all()]) all_secondary_languages.short_description = "Secondary Language" def all_tags(self): return ", ".join(t.name for t in self.tags.all()) all_tags.short_description = "tags" def alphabetized_tags(self): return self.tags.order_by(Lower("name")) def is_public(self): """admin display field indicating if doc is public or suppressed""" return self.status == self.PUBLIC is_public.short_description = "Public" is_public.boolean = True is_public.admin_order_field = "status" def get_absolute_url(self): return reverse("corpus:document", args=[str(self.id)]) @property def permalink(self): # generate permalink without language url so that all versions # have the same link and users will be directed preferred language # - get current active language, or default langue if not active lang = get_language() or settings.LANGUAGE_CODE return absolutize_url(self.get_absolute_url().replace(f"/{lang}/", "/")) def iiif_urls(self): """List of IIIF urls for images of the Document's Fragments.""" return list( dict.fromkeys( filter(None, [b.fragment.iiif_url for b in self.textblock_set.all()]) ) ) def iiif_images(self): """List of IIIF images and labels for images of the Document's Fragments.""" iiif_images = [] for b in self.textblock_set.all(): frag_images = b.fragment.iiif_images() if frag_images is not None: images, labels = frag_images iiif_images += [ {"image": img, "label": labels[i]} for i, img in enumerate(images) ] return iiif_images def fragment_urls(self): """List of external URLs to view the Document's Fragments.""" return list( dict.fromkeys( filter(None, [b.fragment.url for b in self.textblock_set.all()]) ) ) def has_transcription(self): """Admin display field indicating if document has a transcription.""" return any(note.has_transcription() for note in self.footnotes.all()) has_transcription.short_description = "Transcription" has_transcription.boolean = True has_transcription.admin_order_field = "footnotes__content" def has_image(self): """Admin display field indicating if document has a IIIF image.""" return any(self.iiif_urls()) has_image.short_description = "Image" has_image.boolean = True has_image.admin_order_field = "textblock__fragment__iiif_url" @property def title(self): """Short title for identifying the document, e.g. via search.""" return f"{self.doctype or _('Unknown type')}: {self.shelfmark or '??'}" def editions(self): """All footnotes for this document where the document relation includes edition; footnotes with content will be sorted first.""" return self.footnotes.filter(doc_relation__contains=Footnote.EDITION).order_by( "content", "source" ) def digital_editions(self): """All footnotes for this document where the document relation includes edition AND the footnote has content.""" return ( self.footnotes.filter(doc_relation__contains=Footnote.EDITION) .filter(content__isnull=False) .order_by("content", "source") ) def editors(self): """All unique authors of digital editions for this document.""" return Creator.objects.filter( source__footnote__doc_relation__contains=Footnote.EDITION, source__footnote__content__isnull=False, source__footnote__document=self, ).distinct() def sources(self): """All unique sources attached to footnotes on this document.""" return Source.objects.filter(footnote__document=self).distinct() def attribution(self): """Generate a tuple of three attribution components for use in IIIF manifests or wherever images/transcriptions need attribution.""" # keep track of unique attributions so we can include them all extra_attrs_set = set() for url in self.iiif_urls(): remote_manifest = IIIFPresentation.from_url(url) # CUDL attribution has some variation in tags; # would be nice to preserve tagged version, # for now, ignore tags so we can easily de-dupe try: extra_attrs_set.add(strip_tags(remote_manifest.attribution)) except AttributeError: # attribution is optional, so ignore if not present pass pgp = _("Princeton Geniza Project") # Translators: attribution for local IIIF manifests attribution = _("Compilation by %(pgp)s." % {"pgp": pgp}) if self.has_transcription(): # Translators: attribution for local IIIF manifests that include transcription attribution = _("Compilation and transcription by %(pgp)s." % {"pgp": pgp}) # Translators: manifest attribution note that content from other institutions may have restrictions additional_restrictions = _("Additional restrictions may apply.") return ( attribution, additional_restrictions, extra_attrs_set, ) @classmethod def total_to_index(cls): # quick count for parasolr indexing (don't do prefetching just to get the total!) return cls.objects.count() @classmethod def items_to_index(cls): """Custom logic for finding items to be indexed when indexing in bulk.""" # NOTE: some overlap with prefetching used for django admin return ( Document.objects.select_related("doctype") .prefetch_related( "tags", "languages", "footnotes", "footnotes__source", "footnotes__source__authorship", "footnotes__source__authorship__creator", "footnotes__source__source_type", "footnotes__source__languages", "log_entries", Prefetch( "textblock_set", queryset=TextBlock.objects.select_related( "fragment", "fragment__collection" ), ), ) .distinct() ) def index_data(self): """data for indexing in Solr""" index_data = super().index_data() # get fragments via textblocks for correct order # and to take advantage of prefetching fragments = [tb.fragment for tb in self.textblock_set.all()] index_data.update( { "pgpid_i": self.id, "type_s": str(self.doctype) if self.doctype else _("Unknown type"), "description_t": self.description, "notes_t": self.notes or None, "needs_review_t": self.needs_review or None, "shelfmark_ss": self.certain_join_shelfmarks, # library/collection possibly redundant? "collection_ss": [str(f.collection) for f in fragments], "tags_ss_lower": [t.name for t in self.tags.all()], "status_s": self.get_status_display(), "old_pgpids_is": self.old_pgpids, "language_code_ss": [lang.iso_code for lang in self.languages.all()], } ) # count scholarship records by type footnotes = self.footnotes.all() counts = defaultdict(int) transcription_texts = [] # dict of sets of relations; keys are each source attached to any footnote on this document source_relations = defaultdict(set) for fn in footnotes: # if this is an edition/transcription, try to get plain text for indexing if Footnote.EDITION in fn.doc_relation and fn.content: plaintext = fn.content_text() if plaintext: transcription_texts.append(plaintext) # add any doc relations to this footnote's source's set in source_relations source_relations[fn.source] = source_relations[fn.source].union( fn.doc_relation ) # flatten sets of relations by source into a list of relations for relation in list(chain(*source_relations.values())): # add one for each relation in the flattened list counts[relation] += 1 index_data.update( { "num_editions_i": counts[Footnote.EDITION], "num_translations_i": counts[Footnote.TRANSLATION], "num_discussions_i": counts[Footnote.DISCUSSION], # count each unique source as one scholarship record "scholarship_count_i": self.sources().count(), # preliminary scholarship record indexing # (may need splitting out and weighting based on type of scholarship) "scholarship_t": [fn.display() for fn in self.footnotes.all()], # text content of any transcriptions "transcription_t": transcription_texts, } ) last_log_entry = self.log_entries.last() if last_log_entry: index_data["input_year_i"] = last_log_entry.action_time.year # TODO: would be nice to use full date to display year # instead of indexing separately # (may require parasolr datetime conversion support? or implement # in local queryset?) index_data[ "input_date_dt" ] = last_log_entry.action_time.isoformat().replace("+00:00", "Z") return index_data # define signal handlers to update the index based on changes # to other models index_depends_on = { "fragments": { "post_save": DocumentSignalHandlers.related_save, "pre_delete": DocumentSignalHandlers.related_delete, }, "tags": { "post_save": DocumentSignalHandlers.related_save, "pre_delete": DocumentSignalHandlers.related_delete, }, "doctype": { "post_save": DocumentSignalHandlers.related_save, "pre_delete": DocumentSignalHandlers.related_delete, }, "textblock_set": { "post_save": DocumentSignalHandlers.related_save, "pre_delete": DocumentSignalHandlers.related_delete, } # footnotes and sources, when we include editors/translators # script+language when/if included in index data } def merge_with(self, merge_docs, rationale, user=None): """Merge the specified documents into this one. Combines all metadata into this document, adds the merged documents into list of old PGP IDs, and creates a log entry documenting the merge, including the rationale.""" # initialize old pgpid list if previously unset if self.old_pgpids is None: self.old_pgpids = [] # if user is not specified, log entry will be associated with # script and document will be flagged for review script = False if user is None: user = User.objects.get(username=settings.SCRIPT_USERNAME) script = True description_chunks = [self.description] language_notes = [self.language_note] if self.language_note else [] notes = [self.notes] if self.notes else [] needs_review = [self.needs_review] if self.needs_review else [] for doc in merge_docs: # add merge id to old pgpid list self.old_pgpids.append(doc.id) # add any tags from merge document tags to primary doc self.tags.add(*doc.tags.names()) # add description if set and not duplicated if doc.description and doc.description not in self.description: description_chunks.append( "Description from PGPID %s:\n%s" % (doc.id, doc.description) ) # add any notes if doc.notes: notes.append("Notes from PGPID %s:\n%s" % (doc.id, doc.notes)) if doc.needs_review: needs_review.append(doc.needs_review) # add languages and secondary languages for lang in doc.languages.all(): self.languages.add(lang) for lang in doc.secondary_languages.all(): self.secondary_languages.add(lang) if doc.language_note: language_notes.append(doc.language_note) # if there are any textblocks with fragments not already # asociated with this document, reassociate # (i.e., for newly discovered joins) # does not deal with discrepancies between text block fields or order for textblock in doc.textblock_set.all(): if textblock.fragment not in self.fragments.all(): self.textblock_set.add(textblock) self._merge_footnotes(doc) self._merge_logentries(doc) # combine text fields self.description = "\n".join(description_chunks) self.notes = "\n".join(notes) self.language_note = "; ".join(language_notes) # if merged via script, flag for review if script: needs_review.insert(0, "SCRIPTMERGE") self.needs_review = "\n".join(needs_review) # save current document with changes; delete merged documents self.save() merged_ids = ", ".join([str(doc.id) for doc in merge_docs]) for doc in merge_docs: doc.delete() # create log entry documenting the merge; include rationale doc_contenttype = ContentType.objects.get_for_model(Document) LogEntry.objects.log_action( user_id=user.id, content_type_id=doc_contenttype.pk, object_id=self.pk, object_repr=str(self), change_message="merged with %s: %s" % (merged_ids, rationale), action_flag=CHANGE, ) def _merge_footnotes(self, doc): # combine footnotes; footnote logic for merge_with for footnote in doc.footnotes.all(): # first, check for an exact match equiv_fn = self.footnotes.includes_footnote(footnote) # if there is no exact match, check again ignoring content if not equiv_fn: equiv_fn = self.footnotes.includes_footnote( footnote, include_content=False ) # if there's a partial match (everything but content) if equiv_fn: # if the new footnote has content, add it if footnote.content: self.footnotes.add(footnote) # if the partial match has no content, remove it # (if it has any content, then it is different from the new one # and should be preserved) if not equiv_fn.content: self.footnotes.remove(equiv_fn) # if neither an exact or partial match, add the new footnote else: self.footnotes.add(footnote) def _merge_logentries(self, doc): # reassociate log entries; logic for merge_with # make a list of currently associated log entries to skip duplicates current_logs = [ "%s_%s" % (le.user_id, le.action_time.isoformat()) for le in self.log_entries.all() ] for log_entry in doc.log_entries.all(): # check duplicate log entries, based on user id and time # (likely only applies to historic input & revision) if ( "%s_%s" % (log_entry.user_id, log_entry.action_time.isoformat()) in current_logs ): # skip if it's a duplicate continue # otherwise annotate and reassociate # - modify change message to document which object this event applied to log_entry.change_message = "%s [PGPID %d]" % ( log_entry.change_message, doc.pk, ) # - associate with the primary document log_entry.object_id = self.id log_entry.content_type_id = ContentType.objects.get_for_model(Document) log_entry.save()
class Page(auto_prefetch.Model): title = models.CharField(_("title"), max_length=200) content = models.TextField(_("content"), blank=True) description = models.TextField(blank=True, default="") page_head = models.TextField("Page head", blank=True) slug = models.SlugField(unique=True, default="", max_length=200) created_date = models.DateTimeField("Creation date", default=timezone.now) mod_date = models.DateTimeField("Last Updated", auto_now=True) main_page = models.BooleanField(default=False) enable_comments = models.BooleanField(_("enable comments"), default=True) withdrawn = models.BooleanField(default=False) template_name = models.CharField( _("template name"), max_length=70, blank=True, help_text=_( "Example: pages/contact_page.html”. If this isn’t provided, " "the system will use pages/default.html”."), ) registration_required = models.BooleanField( _("registration required"), help_text= _("If this is checked, only logged-in users will be able to view the page." ), default=False, ) sites = models.ManyToManyField(Site, verbose_name=_("sites")) tags = TaggableManager(blank=True, through=CustomTaggedItem) is_removed = models.BooleanField("is removed", default=False, db_index=True, help_text=("Soft delete")) history = HistoricalRecords() class Meta: """Meta class for Page Model""" ordering = ["pk"] def __str__(self): return self.title def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.title) return super().save(*args, **kwargs) def get_absolute_url(self): return reverse("pages:page_detail", kwargs={"slug": self.slug}) def get_absolute_update_url(self): return reverse("pages:page_edit", kwargs={"slug": self.slug}) def get_absolute_delete_url(self): return reverse("pages:page_delete", kwargs={"slug": self.slug}) def get_absolute_admin_update_url(self): return reverse("admin:pages_page_change", kwargs={"object_id": self.pk}) def remove(self): self.is_removed = True self.save() def get_index_view_url(self): content_type = ContentType.objects.get_for_model(self.__class__) return reverse("%s:index" % (content_type.app_label))
class TagsMixin(models.Model): """ Mixin for adding tags to a model. """ # Make association with tags optional. if TaggableManager is not None: tags = TaggableManager(blank=True) else: tags = None class Meta: abstract = True def similar_objects(self, num=None, **filters): """ Find similar objects using related tags. """ tags = self.tags if not tags: return [] content_type = ContentType.objects.get_for_model(self.__class__) filters['content_type'] = content_type # can't filter, see # - https://github.com/alex/django-taggit/issues/32 # - https://django-taggit.readthedocs.io/en/latest/api.html#TaggableManager.similar_objects # # Otherwise this would be possible: # return tags.similar_objects(**filters) lookup_kwargs = tags._lookup_kwargs() lookup_keys = sorted(lookup_kwargs) qs = tags.through.objects.values(*lookup_kwargs.keys()) qs = qs.annotate(n=models.Count('pk')) qs = qs.exclude(**lookup_kwargs) subq = tags.all() qs = qs.filter(tag__in=list(subq)) qs = qs.order_by('-n') # from https://github.com/alex/django-taggit/issues/32#issuecomment-1002491 if filters is not None: qs = qs.filter(**filters) if num is not None: qs = qs[:num] # Normal taggit code continues # TODO: This all feels like a bit of a hack. items = {} if len(lookup_keys) == 1: # Can we do this without a second query by using a select_related() # somehow? f = tags.through._meta.get_field_by_name(lookup_keys[0])[0] objs = f.rel.to._default_manager.filter(**{ "%s__in" % f.rel.field_name: [r["content_object"] for r in qs] }) for obj in objs: items[(getattr(obj, f.rel.field_name),)] = obj else: preload = {} for result in qs: preload.setdefault(result['content_type'], set()) preload[result["content_type"]].add(result["object_id"]) for ct, obj_ids in preload.items(): ct = ContentType.objects.get_for_id(ct) for obj in ct.model_class()._default_manager.filter(pk__in=obj_ids): items[(ct.pk, obj.pk)] = obj results = [] for result in qs: obj = items[ tuple(result[k] for k in lookup_keys) ] obj.similar_tags = result["n"] results.append(obj) return results
class Post(AbstractEntry): # Todo: 统一 Post 和 Material 的状态选项 STATUS_CHOICES = Choices( (1, "published", _("published")), (2, "draft", _("draft")), (3, "hidden", _("hidden")), ) status = models.PositiveSmallIntegerField(_("status"), choices=STATUS_CHOICES, default=STATUS_CHOICES.draft) pinned = models.BooleanField(_("pinned"), default=False) # Todo:移除封面字段 cover = models.ImageField(_("cover"), upload_to="covers/posts/", blank=True) cover_thumbnail = ImageSpecField( source="cover", processors=[ResizeToFill(60, 60)], format="JPEG", options={"quality": 90}, ) category = models.ForeignKey( Category, on_delete=models.CASCADE, verbose_name=_("category"), null=True, blank=True, ) tags = TaggableManager(verbose_name=_("tags"), through=TaggedItem, blank=True) # 模型管理器 objects = PostManager() index = IndexPostManager() class Meta: verbose_name = _("Posts") verbose_name_plural = _("Posts") ordering = ["-pub_date", "-created"] def __str__(self): return self.title def save(self, *args, **kwargs): if not self.excerpt: self.excerpt = strip_tags(self.body_html)[:150] if not self.pub_date and self.status == self.STATUS_CHOICES.published: self.pub_date = self.created super().save(*args, **kwargs) def get_absolute_url(self): return reverse("blog:detail", kwargs={"pk": self.pk}) @property def type(self): return "p"