prelom/django/journal/models.py

326 lines
11 KiB
Python

import collections
import math
import operator
from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils.html import strip_tags
from imagekit.models import ImageSpecField, ProcessedImageField
from imagekit.processors import ResizeToFill
from martor.models import MartorField
from martor.utils import markdownify
class Category(models.Model):
name = models.CharField(max_length=20)
slug = models.SlugField(max_length=20)
description = models.TextField()
content = MartorField()
formatted_content = models.TextField(editable=False)
tag_name = models.CharField(
max_length=12,
help_text="Singular word for what the tags refer to (e.g., theme)"
)
about_page = models.ForeignKey('cms.Page', on_delete=models.PROTECT, blank=True, null=True)
order_on_homepage = models.PositiveIntegerField(default=0)
icon = models.CharField(max_length=10,
help_text="Semantic UI icon used for the label on the article page")
archive_link_text = models.CharField(
max_length=20,
help_text="The text shown on the homepage for linking to this archive"
)
class Meta:
verbose_name_plural = 'categories'
ordering = ['order_on_homepage']
def get_latest_article(self):
return self.articles.filter(published=True).latest()
def get_articles(self):
return self.articles.filter(published=True).order_by('featured__order_on_homepage')
def get_absolute_url(self):
return reverse('category', args=[self.slug])
def save(self, *args, **kwargs):
# Parse markdown and cache it.
self.formatted_content = markdownify(self.content)
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField(max_length=50)
slug = models.SlugField(max_length=50)
description = models.TextField()
image = ProcessedImageField(
upload_to='tags',
processors=[ResizeToFill(540, 360)],
options={'quality': 100},
blank=True,
null=True,
help_text="Resized to 540x360",
)
category = models.ForeignKey(
Category,
on_delete=models.CASCADE,
related_name='tags', null=True)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_articles(self):
# only show articles whose category matches the tag category
return self.articles.filter(
category=self.category,
published=True
).order_by('-date')
def get_latest_article(self, existing_articles=None):
articles = self.articles.filter(published=True)
if existing_articles:
articles = articles.exclude(pk__in=existing_articles)
if articles.exists():
return articles.latest()
def get_date(self):
latest = self.get_latest_article()
if latest:
return latest.date
def get_absolute_url(self):
return reverse('tag', args=[self.slug])
class Author(models.Model):
name = models.CharField(max_length=100)
bio = MartorField()
formatted_bio = models.TextField(editable=False)
slug = models.SlugField(unique=True)
is_editor = models.BooleanField(default=False)
twitter = models.CharField(max_length=15, blank=True, help_text='Username without the @')
def __str__(self):
return self.name
def get_articles(self):
return self.articles.filter(published=True).order_by('-date')
def get_absolute_url(self):
return reverse('author', args=[self.slug])
def save(self, *args, **kwargs):
# Parse markdown and cache it.
self.formatted_bio = markdownify(self.bio)
super().save(*args, **kwargs)
class Issue(models.Model):
number = models.PositiveSmallIntegerField(unique=True)
title = models.CharField(max_length=50)
date = models.DateField(help_text='Day ignored')
slug = models.SlugField()
image = ProcessedImageField(
upload_to='issues',
processors=[ResizeToFill(540, 360)],
options={'quality': 100},
help_text='Cropped to 540x360'
)
content = MartorField()
formatted_content = models.TextField(editable=False)
published = models.BooleanField(default=False)
class Meta:
get_latest_by = 'date'
ordering = ['-date']
def save(self, *args, **kwargs):
self.formatted_content = markdownify(self.content)
super().save(*args, **kwargs)
def get_articles(self):
# If this issue isn't published, just return all the articles.
if self.published:
return self.articles.filter(published=True)
else:
return self.articles.all()
# Use h2 or h3 in footer depending on the length of the title.
def get_title_header(self):
if len(self.title) > 30:
return 'h3'
else:
return 'h2'
def get_absolute_url(self):
return reverse('issue', args=[self.slug])
def __str__(self):
return self.title
class Article(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE,
related_name='articles')
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
authors = models.ManyToManyField(Author, related_name='articles',
blank=True)
tags = models.ManyToManyField(Tag, related_name='articles', blank=True)
subtitle = models.TextField(blank=True)
content = MartorField()
formatted_content = models.TextField(editable=False)
# Store the formatted_content field with all tags removed (for related)
unformatted_content = models.TextField(editable=False)
date = models.DateField()
issue = models.ForeignKey(Issue, related_name='articles', null=True,
blank=True, on_delete=models.CASCADE)
order_in_issue = models.PositiveIntegerField(default=0)
image = ProcessedImageField(
upload_to='articles',
processors=[ResizeToFill(540, 360)],
format='JPEG',
options={'quality': 100},
help_text="Resized to 540x360.",
blank=True
)
image_credit = models.URLField(blank=True)
related_1 = models.ForeignKey("self", related_name='related_1_articles',
on_delete=models.CASCADE, blank=True, null=True)
related_2 = models.ForeignKey("self", related_name='related_2_articles',
on_delete=models.CASCADE, blank=True, null=True)
last_modified = models.DateField(auto_now=True)
published = models.BooleanField(default=False)
class Meta:
ordering = ['-date', 'order_in_issue']
get_latest_by = 'date'
def __str__(self):
return self.title
@property
def language(self):
return 'en'
def get_language_display(self):
return 'English'
def get_absolute_url(self):
return reverse('article', args=[self.slug])
def get_word_count(self):
return len(self.unformatted_content.split())
def get_ordered_authors(self):
return sorted(self.authors.all(), key=lambda s: s.name.split()[1] if len(s.name.split()) > 1 else s.name)
def save(self, *args, **kwargs):
# Parse markdown and cache it.
self.formatted_content = markdownify(self.content)
self.unformatted_content = strip_tags(self.formatted_content)
words = self.unformatted_content.split()
# Find the two most similar articles based on cosine similarity. Only
# do this if they're missing!
if not self.related_1 or not self.related_2:
this_counter = collections.Counter(words)
articles = []
for article in Article.objects.exclude(slug=self.slug):
other_counter = collections.Counter(article.unformatted_content.split())
intersection = set(this_counter.keys()) & set(other_counter.keys())
numerator = sum([this_counter[x] * other_counter[x] for x in intersection])
this_sum = sum([v**2 for v in this_counter.values()])
other_sum = sum([v**2 for v in this_counter.values()])
denominator = math.sqrt(this_sum) * math.sqrt(other_sum)
cosine = numerator / denominator if denominator else 0.0
articles.append((cosine, article))
articles.sort(key=operator.itemgetter(0), reverse=True)
if len(articles) > 1:
self.related_1 = articles[0][1]
if len(articles) > 1:
self.related_2 = articles[1][1]
super().save(*args, **kwargs)
# Use h3 or h4 in article thumbnail depending on the length of the title.
def get_title_header(self):
if len(self.title) > 50:
return 'h4'
else:
return 'h3'
def get_related(self):
# Limited to 2. Currently just gets the latest articles.
related = []
if self.related_1:
related.append(self.related_1)
if self.related_2:
related.append(self.related_2)
return related
class FeaturedArticle(models.Model):
"""For featured articles on the homepage. Can be full width or thumbnail."""
article = models.OneToOneField(
Article,
related_name="featured",
on_delete=models.CASCADE,
primary_key=True
)
is_thumb = models.BooleanField(
help_text="Check this if you want the box to be small, rather than taking up the whole container"
)
order_on_homepage = models.PositiveIntegerField(
unique=True,
help_text="For determining the order of articles on the homepage. 1, 2, 3, etc. Note that large (non-thumb) articles will always be shown first."
)
def __str__(self):
return self.article.title
class Meta:
ordering = ['order_on_homepage']
class ArticleTranslation(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE,
related_name='translations')
language = models.CharField(max_length=2, choices=settings.LANGUAGES)
title = models.CharField(max_length=255)
subtitle = models.TextField()
content = MartorField()
formatted_content = models.TextField(editable=False)
# Store the formatted_content field with all tags removed (for description)
unformatted_content = models.TextField(editable=False)
# The slug should really have uniqueness checks but, too hard tbh
slug = models.SlugField(max_length=50)
last_modified = models.DateField(auto_now=True)
class Meta:
unique_together = ('article', 'language')
def __str__(self):
return "{}{}".format(self.article.title, self.get_language_display())
def save(self, *args, **kwargs):
# Parse markdown and cache it.
self.formatted_content = markdownify(self.content)
self.unformatted_content = strip_tags(self.formatted_content)
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('article', args=[self.slug])