from computed_property import ComputedCharField from django.contrib.auth import get_user_model from django.contrib.sites.models import Site as DjangoSite from django.db import models from django.urls import reverse from django.utils.functional import cached_property from itertools import groupby from mf2util import interpret from slugify import slugify from textwrap import shorten from urllib.parse import urljoin, urlparse from lemonshort.short_url import short_url from meta.models import ModelMeta from model_utils.models import TimeStampedModel from users.models import Site from . import kinds from lemoncurry import requests, utils ENTRY_KINDS = [(k.id, k.id) for k in kinds.all] class CatManager(models.Manager): def from_name(self, name): cat, created = self.get_or_create(name=name, slug=slugify(name)) return cat class Cat(models.Model): objects = CatManager() name = models.CharField(max_length=255, unique=True) slug = models.CharField(max_length=255, unique=True) def __str__(self): return '#' + self.name @property def url(self): return reverse('entries:cat', args=(self.slug,)) class Meta: ordering = ('name',) class EntryManager(models.Manager): def get_queryset(self): qs = super(EntryManager, self).get_queryset() return (qs .select_related('author') .prefetch_related('cats', 'syndications')) class Entry(ModelMeta, TimeStampedModel): objects = EntryManager() kind = models.CharField( max_length=30, choices=ENTRY_KINDS, db_index=True, default=ENTRY_KINDS[0][0] ) name = models.CharField(max_length=100, blank=True) photo = models.ImageField(blank=True) content = models.TextField() cats = models.ManyToManyField(Cat, related_name='entries') in_reply_to = models.CharField(max_length=255, blank=True) like_of = models.CharField(max_length=255, blank=True) repost_of = models.CharField(max_length=255, blank=True) author = models.ForeignKey( get_user_model(), related_name='entries', on_delete=models.CASCADE, ) @property def reply_context(self): if not self.in_reply_to: return None return interpret( requests.mf2(self.in_reply_to).to_dict(), self.in_reply_to ) @property def published(self): return self.created @property def updated(self): return self.modified _metadata = { 'description': 'excerpt', 'image': 'image_url', 'twitter_creator': 'twitter_creator', 'og_profile_id': 'og_profile_id', } @property def title(self): if self.name: return self.name return shorten( utils.to_plain(self.paragraphs[0]), width=100, placeholder='…' ) @property def excerpt(self): try: return utils.to_plain(self.paragraphs[0 if self.name else 1]) except IndexError: return ' ' @property def paragraphs(self): lines = self.content.splitlines() return [ "\n".join(para) for k, para in groupby(lines, key=bool) if k ] @property def twitter_creator(self): return self.author.twitter_username @property def og_profile_id(self): return self.author.facebook_id @property def image_url(self): return self.photo.url if self.photo else self.author.avatar_url def __str__(self): return '{0} {1}: {2}'.format(self.kind, self.id, self.title) def get_absolute_url(self): return self.absolute_url @property def absolute_url(self): base = 'https://' + DjangoSite.objects.get_current().domain return urljoin(base, self.url) @property def affected_urls(self): base = 'https://' + DjangoSite.objects.get_current().domain kind = kinds.from_id[self.kind] urls = { self.url, reverse('entries:index', kwargs={'kind': kind}), reverse('entries:atom_by_kind', kwargs={'kind': kind}), reverse('entries:rss_by_kind', kwargs={'kind': kind}), } | {cat.url for cat in self.cats.all()} if kind.on_home: urls |= { reverse('home:index'), reverse('entries:atom'), reverse('entries:rss') } return {urljoin(base, u) for u in urls} @property def url(self): kind = kinds.from_id[self.kind] args = [kind, self.id] if kind.slug: args.append(self.slug) return reverse('entries:entry', args=args) @property def short_url(self): return short_url(self) @property def slug(self): return slugify(self.name) @property def json_ld(self): base = 'https://' + DjangoSite.objects.get_current().domain url = urljoin(base, self.url) posting = { '@context': 'http://schema.org', '@type': 'BlogPosting', '@id': url, 'url': url, 'mainEntityOfPage': url, 'author': { '@type': 'Person', 'url': urljoin(base, self.author.url), 'name': self.author.name, }, 'headline': self.title, 'description': self.excerpt, 'datePublished': self.created.isoformat(), 'dateModified': self.modified.isoformat(), } if self.photo: posting['image'] = (urljoin(base, self.photo.url), ) return posting class Meta: verbose_name_plural = 'entries' ordering = ['-created'] class Syndication(models.Model): entry = models.ForeignKey( Entry, related_name='syndications', on_delete=models.CASCADE ) url = models.CharField(max_length=255) domain = ComputedCharField( compute_from='calc_domain', max_length=255, ) def calc_domain(self): domain = urlparse(self.url).netloc if domain.startswith('www.'): domain = domain[4:] return domain @cached_property def site(self): d = self.domain try: return Site.objects.get(domain=d) except Site.DoesNotExist: return Site(name=d, domain=d, icon='fas fa-newspaper') class Meta: ordering = ['domain']