diff --git a/entries/admin.py b/entries/admin.py index dac64fb..e42b7f0 100644 --- a/entries/admin.py +++ b/entries/admin.py @@ -8,12 +8,10 @@ class SyndicationInline(admin.TabularInline): class EntryAdmin(admin.ModelAdmin): - date_hierarchy = 'created' - list_display = ('title', 'id', 'kind', 'created') - list_filter = ('kind',) - inlines = ( - SyndicationInline, - ) + date_hierarchy = "created" + list_display = ("title", "id", "kind", "created") + list_filter = ("kind",) + inlines = (SyndicationInline,) admin.site.register(Cat) diff --git a/entries/apps.py b/entries/apps.py index 554d2a6..f34a177 100644 --- a/entries/apps.py +++ b/entries/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class EntriesConfig(AppConfig): - name = 'entries' + name = "entries" diff --git a/entries/from_url.py b/entries/from_url.py index d842b83..64cf56f 100644 --- a/entries/from_url.py +++ b/entries/from_url.py @@ -11,24 +11,24 @@ from .models import Entry def from_url(url: str) -> Entry: domain = Site.objects.get_current().domain if not url: - raise error.bad_req('url parameter required') - if '//' not in url: - url = '//' + url - parts = urlparse(url, scheme='https') - if parts.scheme not in ('http', 'https') or parts.netloc != domain: - raise error.bad_req('url does not point to this site') + raise error.bad_req("url parameter required") + if "//" not in url: + url = "//" + url + parts = urlparse(url, scheme="https") + if parts.scheme not in ("http", "https") or parts.netloc != domain: + raise error.bad_req("url does not point to this site") try: match = resolve(parts.path) except Resolver404: - raise error.bad_req('url does not point to a valid page on this site') + raise error.bad_req("url does not point to a valid page on this site") - if match.view_name != 'entries:entry': - raise error.bad_req('url does not point to an entry on this site') + if match.view_name != "entries:entry": + raise error.bad_req("url does not point to an entry on this site") try: - entry = Entry.objects.get(pk=match.kwargs['id']) + entry = Entry.objects.get(pk=match.kwargs["id"]) except Entry.DoesNotExist: - raise error.bad_req('url does not point to an existing entry') + raise error.bad_req("url does not point to an existing entry") return entry diff --git a/entries/jobs.py b/entries/jobs.py index 68a873c..eb4db30 100644 --- a/entries/jobs.py +++ b/entries/jobs.py @@ -7,16 +7,19 @@ from ronkyuu import webmention @job def ping_hub(*urls): for url in urls: - requests.post(settings.PUSH_HUB, data={ - 'hub.mode': 'publish', - 'hub.url': url, - }) + requests.post( + settings.PUSH_HUB, + data={ + "hub.mode": "publish", + "hub.url": url, + }, + ) @job def send_mentions(source, targets=None): if targets is None: - targets = webmention.findMentions(source)['refs'] + targets = webmention.findMentions(source)["refs"] for target in targets: status, endpoint = webmention.discoverEndpoint(target) if endpoint is not None and status == 200: diff --git a/entries/kinds.py b/entries/kinds.py index 4e25466..6378eeb 100644 --- a/entries/kinds.py +++ b/entries/kinds.py @@ -14,62 +14,62 @@ class Entry: return self.index_page() def index_page(self, page=0): - kwargs = {'kind': self} + kwargs = {"kind": self} if page > 1: - kwargs['page'] = page - return reverse('entries:index', kwargs=kwargs) + kwargs["page"] = page + return reverse("entries:index", kwargs=kwargs) @property def entry(self): - return self.plural + '_entry' + return self.plural + "_entry" @property def atom(self): - return reverse('entries:atom_by_kind', kwargs={'kind': self}) + return reverse("entries:atom_by_kind", kwargs={"kind": self}) @property def rss(self): - return reverse('entries:rss_by_kind', kwargs={'kind': self}) + return reverse("entries:rss_by_kind", kwargs={"kind": self}) Note = Entry( - id='note', - icon='fas fa-paper-plane', - plural='notes', + id="note", + icon="fas fa-paper-plane", + plural="notes", ) Article = Entry( - id='article', - icon='fas fa-file-alt', - plural='articles', + id="article", + icon="fas fa-file-alt", + plural="articles", slug=True, ) Photo = Entry( - id='photo', - icon='fas fa-camera', - plural='photos', + id="photo", + icon="fas fa-camera", + plural="photos", ) Reply = Entry( - id='reply', - icon='fas fa-comment', - plural='replies', + id="reply", + icon="fas fa-comment", + plural="replies", on_home=False, ) Like = Entry( - id='like', - icon='fas fa-heart', - plural='likes', + id="like", + icon="fas fa-heart", + plural="likes", on_home=False, ) Repost = Entry( - id='repost', - icon='fas fa-retweet', - plural='reposts', + id="repost", + icon="fas fa-retweet", + plural="reposts", ) all = (Note, Article, Photo) @@ -79,7 +79,7 @@ from_plural = {k.plural: k for k in all} class EntryKindConverter: - regex = '|'.join(k.plural for k in all) + regex = "|".join(k.plural for k in all) def to_python(self, plural): return from_plural[plural] diff --git a/entries/migrations/0001_initial.py b/entries/migrations/0001_initial.py index 1228631..0a230a4 100644 --- a/entries/migrations/0001_initial.py +++ b/entries/migrations/0001_initial.py @@ -8,7 +8,6 @@ import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ @@ -17,20 +16,41 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Entry', + name="Entry", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('kind', models.CharField(choices=[('note', 'Note'), ('article', 'Article')], default='note', max_length=30)), - ('name', models.CharField(blank=True, max_length=100)), - ('summary', models.TextField(blank=True)), - ('content', models.TextField()), - ('published', models.DateTimeField()), - ('updated', models.DateTimeField()), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "kind", + models.CharField( + choices=[("note", "Note"), ("article", "Article")], + default="note", + max_length=30, + ), + ), + ("name", models.CharField(blank=True, max_length=100)), + ("summary", models.TextField(blank=True)), + ("content", models.TextField()), + ("published", models.DateTimeField()), + ("updated", models.DateTimeField()), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name_plural': 'entries', - 'ordering': ['-published'], + "verbose_name_plural": "entries", + "ordering": ["-published"], }, ), ] diff --git a/entries/migrations/0002_syndication.py b/entries/migrations/0002_syndication.py index 415ceee..a91a9fd 100644 --- a/entries/migrations/0002_syndication.py +++ b/entries/migrations/0002_syndication.py @@ -7,23 +7,42 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('users', '0005_auto_20171023_0158'), - ('entries', '0001_initial'), + ("users", "0005_auto_20171023_0158"), + ("entries", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Syndication', + name="Syndication", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('url', models.CharField(max_length=255)), - ('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='syndications', to='entries.Entry')), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Profile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("url", models.CharField(max_length=255)), + ( + "entry", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="syndications", + to="entries.Entry", + ), + ), + ( + "profile", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="users.Profile" + ), + ), ], options={ - 'ordering': ['profile'], + "ordering": ["profile"], }, ), ] diff --git a/entries/migrations/0003_remove_entry_summary.py b/entries/migrations/0003_remove_entry_summary.py index aac7f18..d405062 100644 --- a/entries/migrations/0003_remove_entry_summary.py +++ b/entries/migrations/0003_remove_entry_summary.py @@ -6,14 +6,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('entries', '0002_syndication'), + ("entries", "0002_syndication"), ] operations = [ migrations.RemoveField( - model_name='entry', - name='summary', + model_name="entry", + name="summary", ), ] diff --git a/entries/migrations/0004_auto_20171027_0846.py b/entries/migrations/0004_auto_20171027_0846.py index f276c63..a267757 100644 --- a/entries/migrations/0004_auto_20171027_0846.py +++ b/entries/migrations/0004_auto_20171027_0846.py @@ -8,20 +8,28 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('entries', '0003_remove_entry_summary'), + ("entries", "0003_remove_entry_summary"), ] operations = [ migrations.AlterField( - model_name='entry', - name='author', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to=settings.AUTH_USER_MODEL), + model_name="entry", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="entries", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='entry', - name='kind', - field=models.CharField(choices=[('note', 'note'), ('article', 'article')], db_index=True, default='note', max_length=30), + model_name="entry", + name="kind", + field=models.CharField( + choices=[("note", "note"), ("article", "article")], + db_index=True, + default="note", + max_length=30, + ), ), ] diff --git a/entries/migrations/0005_auto_20171027_1557.py b/entries/migrations/0005_auto_20171027_1557.py index 85ec86b..2b90132 100644 --- a/entries/migrations/0005_auto_20171027_1557.py +++ b/entries/migrations/0005_auto_20171027_1557.py @@ -6,20 +6,24 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('entries', '0004_auto_20171027_0846'), + ("entries", "0004_auto_20171027_0846"), ] operations = [ migrations.AddField( - model_name='entry', - name='photo', - field=models.ImageField(blank=True, upload_to=''), + model_name="entry", + name="photo", + field=models.ImageField(blank=True, upload_to=""), ), migrations.AlterField( - model_name='entry', - name='kind', - field=models.CharField(choices=[('note', 'note'), ('article', 'article'), ('photo', 'photo')], db_index=True, default='note', max_length=30), + model_name="entry", + name="kind", + field=models.CharField( + choices=[("note", "note"), ("article", "article"), ("photo", "photo")], + db_index=True, + default="note", + max_length=30, + ), ), ] diff --git a/entries/migrations/0006_auto_20171102_1200.py b/entries/migrations/0006_auto_20171102_1200.py index af62ba0..c3f735e 100644 --- a/entries/migrations/0006_auto_20171102_1200.py +++ b/entries/migrations/0006_auto_20171102_1200.py @@ -8,34 +8,41 @@ import model_utils.fields class Migration(migrations.Migration): - dependencies = [ - ('entries', '0005_auto_20171027_1557'), + ("entries", "0005_auto_20171027_1557"), ] operations = [ migrations.AlterModelOptions( - name='entry', - options={'ordering': ['-created'], 'verbose_name_plural': 'entries'}, + name="entry", + options={"ordering": ["-created"], "verbose_name_plural": "entries"}, ), migrations.RenameField( - model_name='entry', - old_name='published', - new_name='created', + model_name="entry", + old_name="published", + new_name="created", ), migrations.RenameField( - model_name='entry', - old_name='updated', - new_name='modified', + model_name="entry", + old_name="updated", + new_name="modified", ), migrations.AlterField( - model_name='entry', - name='created', - field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), + model_name="entry", + name="created", + field=model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), ), migrations.AlterField( - model_name='entry', - name='modified', - field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), + model_name="entry", + name="modified", + field=model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), ), ] diff --git a/entries/migrations/0007_auto_20171113_0841.py b/entries/migrations/0007_auto_20171113_0841.py index 40fc4f4..d3e8a39 100644 --- a/entries/migrations/0007_auto_20171113_0841.py +++ b/entries/migrations/0007_auto_20171113_0841.py @@ -6,20 +6,31 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('entries', '0006_auto_20171102_1200'), + ("entries", "0006_auto_20171102_1200"), ] operations = [ migrations.AddField( - model_name='entry', - name='cite', + model_name="entry", + name="cite", field=models.CharField(blank=True, max_length=255), ), migrations.AlterField( - model_name='entry', - name='kind', - field=models.CharField(choices=[('note', 'note'), ('article', 'article'), ('photo', 'photo'), ('reply', 'reply'), ('like', 'like'), ('repost', 'repost')], db_index=True, default='note', max_length=30), + model_name="entry", + name="kind", + field=models.CharField( + choices=[ + ("note", "note"), + ("article", "article"), + ("photo", "photo"), + ("reply", "reply"), + ("like", "like"), + ("repost", "repost"), + ], + db_index=True, + default="note", + max_length=30, + ), ), ] diff --git a/entries/migrations/0008_auto_20171116_2116.py b/entries/migrations/0008_auto_20171116_2116.py index c062844..c7aee1b 100644 --- a/entries/migrations/0008_auto_20171116_2116.py +++ b/entries/migrations/0008_auto_20171116_2116.py @@ -6,25 +6,24 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('entries', '0007_auto_20171113_0841'), + ("entries", "0007_auto_20171113_0841"), ] operations = [ migrations.RenameField( - model_name='entry', - old_name='cite', - new_name='in_reply_to', + model_name="entry", + old_name="cite", + new_name="in_reply_to", ), migrations.AddField( - model_name='entry', - name='like_of', + model_name="entry", + name="like_of", field=models.CharField(blank=True, max_length=255), ), migrations.AddField( - model_name='entry', - name='repost_of', + model_name="entry", + name="repost_of", field=models.CharField(blank=True, max_length=255), ), ] diff --git a/entries/migrations/0009_tag.py b/entries/migrations/0009_tag.py index aa61053..48ddc01 100644 --- a/entries/migrations/0009_tag.py +++ b/entries/migrations/0009_tag.py @@ -6,21 +6,28 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('entries', '0008_auto_20171116_2116'), + ("entries", "0008_auto_20171116_2116"), ] operations = [ migrations.CreateModel( - name='Tag', + name="Tag", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, unique=True)), - ('slug', models.CharField(max_length=255, unique=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255, unique=True)), + ("slug", models.CharField(max_length=255, unique=True)), ], options={ - 'ordering': ('name',), + "ordering": ("name",), }, ), ] diff --git a/entries/migrations/0010_entry_tags.py b/entries/migrations/0010_entry_tags.py index bd36f61..f657ca3 100644 --- a/entries/migrations/0010_entry_tags.py +++ b/entries/migrations/0010_entry_tags.py @@ -6,15 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('entries', '0009_tag'), + ("entries", "0009_tag"), ] operations = [ migrations.AddField( - model_name='entry', - name='tags', - field=models.ManyToManyField(related_name='entries', to='entries.Tag'), + model_name="entry", + name="tags", + field=models.ManyToManyField(related_name="entries", to="entries.Tag"), ), ] diff --git a/entries/migrations/0011_auto_20171120_1108.py b/entries/migrations/0011_auto_20171120_1108.py index 296cacb..36acd85 100644 --- a/entries/migrations/0011_auto_20171120_1108.py +++ b/entries/migrations/0011_auto_20171120_1108.py @@ -9,17 +9,17 @@ class Migration(migrations.Migration): atomic = False dependencies = [ - ('entries', '0010_entry_tags'), + ("entries", "0010_entry_tags"), ] operations = [ migrations.RenameModel( - old_name='Tag', - new_name='Cat', + old_name="Tag", + new_name="Cat", ), migrations.RenameField( - model_name='entry', - old_name='tags', - new_name='cats', + model_name="entry", + old_name="tags", + new_name="cats", ), ] diff --git a/entries/migrations/0012_auto_20180628_2044.py b/entries/migrations/0012_auto_20180628_2044.py index 8769528..dabc60b 100644 --- a/entries/migrations/0012_auto_20180628_2044.py +++ b/entries/migrations/0012_auto_20180628_2044.py @@ -5,25 +5,25 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('entries', '0011_auto_20171120_1108'), + ("entries", "0011_auto_20171120_1108"), ] operations = [ migrations.AlterModelOptions( - name='syndication', - options={'ordering': ['domain']}, + name="syndication", + options={"ordering": ["domain"]}, ), migrations.RemoveField( - model_name='syndication', - name='profile', + model_name="syndication", + name="profile", ), migrations.AddField( - model_name='syndication', - name='domain', + model_name="syndication", + name="domain", field=computed_property.fields.ComputedCharField( - compute_from='calc_domain', default='', editable=False, max_length=255), + compute_from="calc_domain", default="", editable=False, max_length=255 + ), preserve_default=False, ), ] diff --git a/entries/migrations/0013_alter_entry_kind.py b/entries/migrations/0013_alter_entry_kind.py index 6d254e1..a50cf00 100644 --- a/entries/migrations/0013_alter_entry_kind.py +++ b/entries/migrations/0013_alter_entry_kind.py @@ -4,24 +4,19 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('entries', '0012_auto_20180628_2044'), + ("entries", "0012_auto_20180628_2044"), ] operations = [ migrations.AlterField( - model_name='entry', - name='kind', + model_name="entry", + name="kind", field=models.CharField( - choices=[ - ('note', 'note'), - ('article', 'article'), - ('photo', 'photo') - ], + choices=[("note", "note"), ("article", "article"), ("photo", "photo")], db_index=True, - default='note', - max_length=30 + default="note", + max_length=30, ), ), ] diff --git a/entries/models.py b/entries/models.py index e7419c9..e52469b 100644 --- a/entries/models.py +++ b/entries/models.py @@ -17,6 +17,7 @@ from users.models import Site from . import kinds from lemoncurry import requests, utils + ENTRY_KINDS = [(k.id, k.id) for k in kinds.all] @@ -32,38 +33,33 @@ class Cat(models.Model): slug = models.CharField(max_length=255, unique=True) def __str__(self): - return '#' + self.name + return "#" + self.name @property def url(self): - return reverse('entries:cat', args=(self.slug,)) + return reverse("entries:cat", args=(self.slug,)) class Meta: - ordering = ('name',) + 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')) + 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] + 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') + 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) @@ -71,7 +67,7 @@ class Entry(ModelMeta, TimeStampedModel): author = models.ForeignKey( get_user_model(), - related_name='entries', + related_name="entries", on_delete=models.CASCADE, ) @@ -79,10 +75,7 @@ class Entry(ModelMeta, TimeStampedModel): 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 - ) + return interpret(requests.mf2(self.in_reply_to).to_dict(), self.in_reply_to) @property def published(self): @@ -93,35 +86,29 @@ class Entry(ModelMeta, TimeStampedModel): return self.modified _metadata = { - 'description': 'excerpt', - 'image': 'image_url', - 'twitter_creator': 'twitter_creator', - 'og_profile_id': 'og_profile_id', + "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='…' - ) + 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 ' ' + return " " @property def paragraphs(self): lines = self.content.splitlines() - return [ - "\n".join(para) for k, para in groupby(lines, key=bool) if k - ] + return ["\n".join(para) for k, para in groupby(lines, key=bool) if k] @property def twitter_creator(self): @@ -136,31 +123,31 @@ class Entry(ModelMeta, TimeStampedModel): 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) + 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 + base = "https://" + DjangoSite.objects.get_current().domain return urljoin(base, self.url) @property def affected_urls(self): - base = 'https://' + DjangoSite.objects.get_current().domain + 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}), + 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') + reverse("home:index"), + reverse("entries:atom"), + reverse("entries:rss"), } return {urljoin(base, u) for u in urls} @@ -170,7 +157,7 @@ class Entry(ModelMeta, TimeStampedModel): args = [kind, self.id] if kind.slug: args.append(self.slug) - return reverse('entries:entry', args=args) + return reverse("entries:entry", args=args) @property def short_url(self): @@ -182,49 +169,48 @@ class Entry(ModelMeta, TimeStampedModel): @property def json_ld(self): - base = 'https://' + DjangoSite.objects.get_current().domain + 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, + "@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(), + "headline": self.title, + "description": self.excerpt, + "datePublished": self.created.isoformat(), + "dateModified": self.modified.isoformat(), } if self.photo: - posting['image'] = (urljoin(base, self.photo.url), ) + posting["image"] = (urljoin(base, self.photo.url),) return posting class Meta: - verbose_name_plural = 'entries' - ordering = ['-created'] + verbose_name_plural = "entries" + ordering = ["-created"] class Syndication(models.Model): entry = models.ForeignKey( - Entry, - related_name='syndications', - on_delete=models.CASCADE + Entry, related_name="syndications", on_delete=models.CASCADE ) url = models.CharField(max_length=255) domain = ComputedCharField( - compute_from='calc_domain', max_length=255, + compute_from="calc_domain", + max_length=255, ) def calc_domain(self): domain = urlparse(self.url).netloc - if domain.startswith('www.'): + if domain.startswith("www."): domain = domain[4:] return domain @@ -234,7 +220,7 @@ class Syndication(models.Model): try: return Site.objects.get(domain=d) except Site.DoesNotExist: - return Site(name=d, domain=d, icon='fas fa-newspaper') + return Site(name=d, domain=d, icon="fas fa-newspaper") class Meta: - ordering = ['domain'] + ordering = ["domain"] diff --git a/entries/tests/views/feeds.py b/entries/tests/views/feeds.py index 21f6cab..6dd80e3 100644 --- a/entries/tests/views/feeds.py +++ b/entries/tests/views/feeds.py @@ -3,27 +3,27 @@ import pytest @pytest.mark.django_db def test_atom(client): - res = client.get('/atom') + res = client.get("/atom") assert res.status_code == 200 - assert res['Content-Type'] == 'application/atom+xml; charset=utf-8' + assert res["Content-Type"] == "application/atom+xml; charset=utf-8" @pytest.mark.django_db def test_rss(client): - res = client.get('/rss') + res = client.get("/rss") assert res.status_code == 200 - assert res['Content-Type'] == 'application/rss+xml; charset=utf-8' + assert res["Content-Type"] == "application/rss+xml; charset=utf-8" @pytest.mark.django_db def test_atom_by_kind(client): - res = client.get('/notes/atom') + res = client.get("/notes/atom") assert res.status_code == 200 - assert res['Content-Type'] == 'application/atom+xml; charset=utf-8' + assert res["Content-Type"] == "application/atom+xml; charset=utf-8" @pytest.mark.django_db def test_rss_by_kind(client): - res = client.get('/notes/rss') + res = client.get("/notes/rss") assert res.status_code == 200 - assert res['Content-Type'] == 'application/rss+xml; charset=utf-8' + assert res["Content-Type"] == "application/rss+xml; charset=utf-8" diff --git a/entries/urls.py b/entries/urls.py index f40bceb..d015c29 100644 --- a/entries/urls.py +++ b/entries/urls.py @@ -3,47 +3,46 @@ from . import kinds from .views import feeds, lists, perma from lemoncurry import breadcrumbs as crumbs -register_converter(kinds.EntryKindConverter, 'kind') +register_converter(kinds.EntryKindConverter, "kind") def to_pat(*args): - return '^{0}$'.format(''.join(args)) + return "^{0}$".format("".join(args)) def prefix(route): - return app_name + ':' + route + return app_name + ":" + route -id = r'/(?P\d+)' -kind = r'(?P{0})'.format('|'.join(k.plural for k in kinds.all)) -page = r'(?:/page/(?P\d+))?' -slug = r'/(?P[^/]+)' +id = r"/(?P\d+)" +kind = r"(?P{0})".format("|".join(k.plural for k in kinds.all)) +page = r"(?:/page/(?P\d+))?" +slug = r"/(?P[^/]+)" -slug_opt = '(?:' + slug + ')?' +slug_opt = "(?:" + slug + ")?" -app_name = 'entries' +app_name = "entries" urlpatterns = ( - path('atom', feeds.AtomHomeEntries(), name='atom'), - path('rss', feeds.RssHomeEntries(), name='rss'), - path('cats/', lists.by_cat, name='cat'), - path('cats//page/', lists.by_cat, name='cat'), - path('', lists.by_kind, name='index'), - path('/page/', lists.by_kind, name='index'), - path('/atom', feeds.AtomByKind(), name='atom_by_kind'), - path('/rss', feeds.RssByKind(), name='rss_by_kind'), - - path('/', perma.entry, name='entry'), - path('//', perma.entry, name='entry'), + path("atom", feeds.AtomHomeEntries(), name="atom"), + path("rss", feeds.RssHomeEntries(), name="rss"), + path("cats/", lists.by_cat, name="cat"), + path("cats//page/", lists.by_cat, name="cat"), + path("", lists.by_kind, name="index"), + path("/page/", lists.by_kind, name="index"), + path("/atom", feeds.AtomByKind(), name="atom_by_kind"), + path("/rss", feeds.RssByKind(), name="rss_by_kind"), + path("/", perma.entry, name="entry"), + path("//", perma.entry, name="entry"), ) class IndexCrumb(crumbs.Crumb): def __init__(self): - super().__init__(prefix('index'), parent='home:index') + super().__init__(prefix("index"), parent="home:index") @property def kind(self): - return self.match.kwargs['kind'] + return self.match.kwargs["kind"] @property def label(self): @@ -51,9 +50,9 @@ class IndexCrumb(crumbs.Crumb): @property def url(self): - return reverse(prefix('index'), kwargs={'kind': self.kind}) + return reverse(prefix("index"), kwargs={"kind": self.kind}) -crumbs.add(prefix('cat'), parent='home:index') +crumbs.add(prefix("cat"), parent="home:index") crumbs.add(IndexCrumb()) -crumbs.add(prefix('entry'), parent=prefix('index')) +crumbs.add(prefix("entry"), parent=prefix("index")) diff --git a/entries/views/feeds.py b/entries/views/feeds.py index 2a0fac9..837239e 100644 --- a/entries/views/feeds.py +++ b/entries/views/feeds.py @@ -11,8 +11,8 @@ from ..models import Entry class Atom1FeedWithHub(Atom1Feed): def add_root_elements(self, handler): super().add_root_elements(handler) - handler.startElement('link', {'rel': 'hub', 'href': settings.PUSH_HUB}) - handler.endElement('link') + handler.startElement("link", {"rel": "hub", "href": settings.PUSH_HUB}) + handler.endElement("link") class EntriesFeed(Feed): @@ -79,7 +79,7 @@ class RssHomeEntries(EntriesFeed): return Site.objects.get_current().name def link(self): - return reverse('home:index') + return reverse("home:index") def description(self): return "content from {0}".format( diff --git a/entries/views/lists.py b/entries/views/lists.py index c177670..24a8c7e 100644 --- a/entries/views/lists.py +++ b/entries/views/lists.py @@ -5,32 +5,32 @@ from ..models import Entry, Cat from ..pagination import paginate -@render_to('entries/index.html') +@render_to("entries/index.html") def by_kind(request, kind, page=None): entries = Entry.objects.filter(kind=kind.id) entries = paginate(queryset=entries, reverse=kind.index_page, page=page) return { - 'entries': entries, - 'atom': kind.atom, - 'rss': kind.rss, - 'title': kind.plural, + "entries": entries, + "atom": kind.atom, + "rss": kind.rss, + "title": kind.plural, } -@render_to('entries/index.html') +@render_to("entries/index.html") def by_cat(request, slug, page=None): def url(page): - kwargs = {'slug': slug} + kwargs = {"slug": slug} if page > 1: - kwargs['page'] = page - return reverse('entries:cat', kwargs=kwargs) + kwargs["page"] = page + return reverse("entries:cat", kwargs=kwargs) cat = get_object_or_404(Cat, slug=slug) entries = cat.entries.all() entries = paginate(queryset=entries, reverse=url, page=page) return { - 'entries': entries, - 'title': '#' + cat.name, + "entries": entries, + "title": "#" + cat.name, } diff --git a/entries/views/perma.py b/entries/views/perma.py index bb4fbc9..10c5183 100644 --- a/entries/views/perma.py +++ b/entries/views/perma.py @@ -3,12 +3,12 @@ from django.shortcuts import redirect, get_object_or_404 from ..models import Entry -@render_to('entries/entry.html') +@render_to("entries/entry.html") def entry(request, kind, id, slug=None): entry = get_object_or_404(Entry, pk=id) if request.path != entry.url: return redirect(entry.url, permanent=True) return { - 'entry': entry, - 'title': entry.title, + "entry": entry, + "title": entry.title, } diff --git a/gunicorn.py b/gunicorn.py index e34ad90..82d6732 100644 --- a/gunicorn.py +++ b/gunicorn.py @@ -1,5 +1,5 @@ import multiprocessing -proc_name = 'lemoncurry' -worker_class = 'gevent' +proc_name = "lemoncurry" +worker_class = "gevent" workers = multiprocessing.cpu_count() * 2 + 1 diff --git a/home/sitemaps.py b/home/sitemaps.py index a3c33b0..23609be 100644 --- a/home/sitemaps.py +++ b/home/sitemaps.py @@ -3,10 +3,10 @@ from django.urls import reverse class HomeSitemap(sitemaps.Sitemap): - changefreq = 'daily' + changefreq = "daily" def items(self): - return ('home:index',) + return ("home:index",) def location(self, item): return reverse(item) diff --git a/home/urls.py b/home/urls.py index 1f14b68..f7e7e72 100644 --- a/home/urls.py +++ b/home/urls.py @@ -2,9 +2,9 @@ from django.urls import path from . import views -app_name = 'home' +app_name = "home" urlpatterns = [ - path('', views.index, name='index'), - path('page/', views.index, name='index'), - path('robots.txt', views.robots, name='robots.txt'), + path("", views.index, name="index"), + path("page/", views.index, name="index"), + path("robots.txt", views.robots, name="robots.txt"), ] diff --git a/home/views.py b/home/views.py index 056f6ef..b57dc22 100644 --- a/home/views.py +++ b/home/views.py @@ -8,34 +8,31 @@ from urllib.parse import urljoin from entries import kinds, pagination from lemoncurry import breadcrumbs, utils -breadcrumbs.add('home:index', 'home') +breadcrumbs.add("home:index", "home") -@render_to('home/index.html') +@render_to("home/index.html") def index(request, page=None): def url(page): - kwargs = {'page': page} if page != 1 else {} - return reverse('home:index', kwargs=kwargs) + kwargs = {"page": page} if page != 1 else {} + return reverse("home:index", kwargs=kwargs) user = request.user - if not hasattr(user, 'entries'): + if not hasattr(user, "entries"): user = get_object_or_404(User, pk=1) entries = user.entries.filter(kind__in=kinds.on_home) entries = pagination.paginate(queryset=entries, reverse=url, page=page) return { - 'user': user, - 'entries': entries, - 'atom': reverse('entries:atom'), - 'rss': reverse('entries:rss'), + "user": user, + "entries": entries, + "atom": reverse("entries:atom"), + "rss": reverse("entries:rss"), } def robots(request): base = utils.origin(request) - lines = ( - 'User-agent: *', - 'Sitemap: {0}'.format(urljoin(base, reverse('sitemap'))) - ) - return HttpResponse("\n".join(lines) + "\n", content_type='text/plain') + lines = ("User-agent: *", "Sitemap: {0}".format(urljoin(base, reverse("sitemap")))) + return HttpResponse("\n".join(lines) + "\n", content_type="text/plain") diff --git a/lemonauth/migrations/0001_initial.py b/lemonauth/migrations/0001_initial.py index aa17a22..e8c1aa1 100644 --- a/lemonauth/migrations/0001_initial.py +++ b/lemonauth/migrations/0001_initial.py @@ -7,25 +7,36 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True - dependencies = [ - ] # type: List[Tuple[str, str]] + dependencies = [] # type: List[Tuple[str, str]] operations = [ migrations.CreateModel( - name='IndieAuthCode', + name="IndieAuthCode", fields=[ - ('id', models.AutoField(auto_created=True, - primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=64, unique=True)), - ('me', models.CharField(max_length=255)), - ('client_id', models.CharField(max_length=255)), - ('redirect_uri', models.CharField(max_length=255)), - ('response_type', models.CharField(choices=[ - ('id', 'id'), ('code', 'code')], default='id', max_length=4)), - ('scope', models.CharField(blank=True, max_length=200)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("code", models.CharField(max_length=64, unique=True)), + ("me", models.CharField(max_length=255)), + ("client_id", models.CharField(max_length=255)), + ("redirect_uri", models.CharField(max_length=255)), + ( + "response_type", + models.CharField( + choices=[("id", "id"), ("code", "code")], + default="id", + max_length=4, + ), + ), + ("scope", models.CharField(blank=True, max_length=200)), ], ), ] diff --git a/lemonauth/migrations/0002_delete_indieauthcode.py b/lemonauth/migrations/0002_delete_indieauthcode.py index 69ad679..4bf2893 100644 --- a/lemonauth/migrations/0002_delete_indieauthcode.py +++ b/lemonauth/migrations/0002_delete_indieauthcode.py @@ -6,13 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('lemonauth', '0001_initial'), + ("lemonauth", "0001_initial"), ] operations = [ migrations.DeleteModel( - name='IndieAuthCode', + name="IndieAuthCode", ), ] diff --git a/lemonauth/migrations/0003_indieauthcode_token.py b/lemonauth/migrations/0003_indieauthcode_token.py index d09d74c..a3e1748 100644 --- a/lemonauth/migrations/0003_indieauthcode_token.py +++ b/lemonauth/migrations/0003_indieauthcode_token.py @@ -9,43 +9,112 @@ import randomslugfield.fields class Migration(migrations.Migration): - initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('lemonauth', '0002_delete_indieauthcode'), + ("lemonauth", "0002_delete_indieauthcode"), ] operations = [ migrations.CreateModel( - name='IndieAuthCode', + name="IndieAuthCode", fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('id', randomslugfield.fields.RandomSlugField(blank=True, editable=False, length=30, max_length=30, primary_key=True, serialize=False, unique=True)), - ('client_id', models.URLField()), - ('scope', models.TextField(blank=True)), - ('redirect_uri', models.URLField()), - ('response_type', model_utils.fields.StatusField(choices=[('id', 'id'), ('code', 'code')], default='id', max_length=100, no_check_for_status=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "id", + randomslugfield.fields.RandomSlugField( + blank=True, + editable=False, + length=30, + max_length=30, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("client_id", models.URLField()), + ("scope", models.TextField(blank=True)), + ("redirect_uri", models.URLField()), + ( + "response_type", + model_utils.fields.StatusField( + choices=[("id", "id"), ("code", "code")], + default="id", + max_length=100, + no_check_for_status=True, + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='Token', + name="Token", fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('id', randomslugfield.fields.RandomSlugField(blank=True, editable=False, length=30, max_length=30, primary_key=True, serialize=False, unique=True)), - ('client_id', models.URLField()), - ('scope', models.TextField(blank=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "id", + randomslugfield.fields.RandomSlugField( + blank=True, + editable=False, + length=30, + max_length=30, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("client_id", models.URLField()), + ("scope", models.TextField(blank=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), ] diff --git a/lemonauth/models.py b/lemonauth/models.py index 607d3cb..7a23244 100644 --- a/lemonauth/models.py +++ b/lemonauth/models.py @@ -17,6 +17,7 @@ class AuthSecret(TimeStampedModel): authorisation codes and tokens in IndieAuth - the two contain many identical fields, but just a few differences. """ + id = RandomSlugField(primary_key=True, length=30) client_id = models.URLField() user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) @@ -27,7 +28,7 @@ class AuthSecret(TimeStampedModel): return self.user.full_url def __contains__(self, scope): - return scope in self.scope.split(' ') + return scope in self.scope.split(" ") class Meta: abstract = True @@ -41,10 +42,11 @@ class IndieAuthCode(AuthSecret): Codes are single-use, and if unused will be expired automatically after thirty seconds. """ + redirect_uri = models.URLField() - RESPONSE_TYPE = Choices('id', 'code') - response_type = StatusField(choices_name='RESPONSE_TYPE') + RESPONSE_TYPE = Choices("id", "code") + response_type = StatusField(choices_name="RESPONSE_TYPE") @property def expired(self): @@ -56,4 +58,5 @@ class Token(AuthSecret): A Token grants a client long-term authorisation - it will not expire unless explicitly revoked by the user. """ + pass diff --git a/lemonauth/tokens.py b/lemonauth/tokens.py index 5e6640e..17d0e5b 100644 --- a/lemonauth/tokens.py +++ b/lemonauth/tokens.py @@ -3,17 +3,17 @@ from .models import IndieAuthCode, Token def auth(request) -> Token: - if 'HTTP_AUTHORIZATION' in request.META: - auth = request.META.get('HTTP_AUTHORIZATION').split(' ') - if auth[0] != 'Bearer': - raise error.bad_req('auth type {0} not supported'.format(auth[0])) + if "HTTP_AUTHORIZATION" in request.META: + auth = request.META.get("HTTP_AUTHORIZATION").split(" ") + if auth[0] != "Bearer": + raise error.bad_req("auth type {0} not supported".format(auth[0])) if len(auth) != 2: - raise error.bad_req('invalid Bearer auth format, must be Bearer ') + raise error.bad_req("invalid Bearer auth format, must be Bearer ") token = auth[1] - elif 'access_token' in request.POST: - token = request.POST.get('access_token') - elif 'access_token' in request.GET: - token = request.GET.get('access_token') + elif "access_token" in request.POST: + token = request.POST.get("access_token") + elif "access_token" in request.GET: + token = request.GET.get("access_token") else: raise error.unauthorized() @@ -28,11 +28,11 @@ def auth(request) -> Token: def gen_auth_code(req): code = IndieAuthCode() code.user = req.user - code.client_id = req.POST['client_id'] - code.redirect_uri = req.POST['redirect_uri'] - code.response_type = req.POST.get('response_type', 'id') - if 'scope' in req.POST: - code.scope = ' '.join(req.POST.getlist('scope')) + code.client_id = req.POST["client_id"] + code.redirect_uri = req.POST["redirect_uri"] + code.response_type = req.POST.get("response_type", "id") + if "scope" in req.POST: + code.scope = " ".join(req.POST.getlist("scope")) code.save() return code.id diff --git a/lemonauth/urls.py b/lemonauth/urls.py index e98672a..a2126b9 100644 --- a/lemonauth/urls.py +++ b/lemonauth/urls.py @@ -1,13 +1,17 @@ from django.urls import path from . import views -app_name = 'lemonauth' +app_name = "lemonauth" urlpatterns = [ - path('login', views.login, name='login'), - path('logout', views.logout, name='logout'), - path('indie', views.IndieView.as_view(), name='indie'), - path('indie/approve', views.indie_approve, name='indie_approve'), - path('token', views.TokenView.as_view(), name='token'), - path('tokens', views.TokensListView.as_view(), name='tokens'), - path('tokens/', views.TokensRevokeView.as_view(), name='tokens_revoke'), + path("login", views.login, name="login"), + path("logout", views.logout, name="logout"), + path("indie", views.IndieView.as_view(), name="indie"), + path("indie/approve", views.indie_approve, name="indie_approve"), + path("token", views.TokenView.as_view(), name="token"), + path("tokens", views.TokensListView.as_view(), name="tokens"), + path( + "tokens/", + views.TokensRevokeView.as_view(), + name="tokens_revoke", + ), ] diff --git a/lemonauth/views/indie.py b/lemonauth/views/indie.py index dbab249..ac2ddc3 100644 --- a/lemonauth/views/indie.py +++ b/lemonauth/views/indie.py @@ -12,120 +12,114 @@ from urllib.parse import urlencode, urljoin, urlunparse, urlparse from .. import tokens from ..models import IndieAuthCode -breadcrumbs.add('lemonauth:indie', parent='home:index') +breadcrumbs.add("lemonauth:indie", parent="home:index") def canonical(url): - if '//' not in url: - url = '//' + url + if "//" not in url: + url = "//" + url (scheme, netloc, path, params, query, fragment) = urlparse(url) - if not scheme or scheme == 'http': - scheme = 'https' + if not scheme or scheme == "http": + scheme = "https" if not path: - path = '/' + path = "/" return urlunparse((scheme, netloc, path, params, query, fragment)) -@method_decorator(csrf_exempt, name='dispatch') +@method_decorator(csrf_exempt, name="dispatch") class IndieView(TemplateView): - template_name = 'lemonauth/indie.html' - required_params = ('client_id', 'redirect_uri') + template_name = "lemonauth/indie.html" + required_params = ("client_id", "redirect_uri") @method_decorator(login_required) @method_decorator(render_to(template_name)) def get(self, request): params = request.GET.dict() - params.setdefault('response_type', 'id') + params.setdefault("response_type", "id") for param in self.required_params: if param not in params: - return utils.bad_req( - 'parameter {0} is required'.format(param) - ) + return utils.bad_req("parameter {0} is required".format(param)) me = request.user.full_url - if 'me' in params: - param_me = canonical(params['me']) + if "me" in params: + param_me = canonical(params["me"]) if me != param_me: return utils.forbid( - 'you are logged in as {}, not as {}'.format(me, param_me) + "you are logged in as {}, not as {}".format(me, param_me) ) - redirect_uri = urljoin(params['client_id'], params['redirect_uri']) + redirect_uri = urljoin(params["client_id"], params["redirect_uri"]) - type = params['response_type'] - if type not in ('id', 'code'): - return utils.bad_req( - 'unknown response_type: {0}'.format(type) - ) + type = params["response_type"] + if type not in ("id", "code"): + return utils.bad_req("unknown response_type: {0}".format(type)) scopes = () - if type == 'code': - if 'scope' not in params: - return utils.bad_req( - 'scopes required for code type' - ) - scopes = params['scope'].split(' ') + if type == "code": + if "scope" not in params: + return utils.bad_req("scopes required for code type") + scopes = params["scope"].split(" ") - client = requests.mf2(params['client_id']) - rels = (client.to_dict()['rel-urls'] - .get(redirect_uri, {}) - .get('rels', ())) - verified = 'redirect_uri' in rels + client = requests.mf2(params["client_id"]) + rels = client.to_dict()["rel-urls"].get(redirect_uri, {}).get("rels", ()) + verified = "redirect_uri" in rels try: - app = client.to_dict(filter_by_type='h-x-app')[0]['properties'] + app = client.to_dict(filter_by_type="h-x-app")[0]["properties"] except IndexError: app = None return { - 'app': app, - 'me': me, - 'redirect_uri': redirect_uri, - 'verified': verified, - 'params': params, - 'scopes': scopes, - 'title': 'indieauth from {client_id}'.format(**params), + "app": app, + "me": me, + "redirect_uri": redirect_uri, + "verified": verified, + "params": params, + "scopes": scopes, + "title": "indieauth from {client_id}".format(**params), } def post(self, request): post = request.POST.dict() try: - code = IndieAuthCode.objects.get(pk=post.get('code')) + code = IndieAuthCode.objects.get(pk=post.get("code")) except IndieAuthCode.DoesNotExist: # if anything at all goes wrong when decoding the auth code, bail # out immediately. - return utils.forbid('invalid auth code') + return utils.forbid("invalid auth code") code.delete() if code.expired: - return utils.forbid('invalid auth code') + return utils.forbid("invalid auth code") - if code.response_type != 'id': - return utils.bad_req( - 'this endpoint only supports response_type=id' - ) - if code.client_id != post.get('client_id'): - return utils.forbid('client id did not match') - if code.redirect_uri != post.get('redirect_uri'): - return utils.forbid('redirect uri did not match') + if code.response_type != "id": + return utils.bad_req("this endpoint only supports response_type=id") + if code.client_id != post.get("client_id"): + return utils.forbid("client id did not match") + if code.redirect_uri != post.get("redirect_uri"): + return utils.forbid("redirect uri did not match") # If we got here, it's valid! Yay! - return utils.choose_type(request, {'me': code.me}, { - 'application/x-www-form-urlencoded': utils.form_encoded_response, - 'application/json': JsonResponse, - }) + return utils.choose_type( + request, + {"me": code.me}, + { + "application/x-www-form-urlencoded": utils.form_encoded_response, + "application/json": JsonResponse, + }, + ) @login_required @require_POST def approve(request): params = { - 'me': urljoin(utils.origin(request), request.user.url), - 'code': tokens.gen_auth_code(request), + "me": urljoin(utils.origin(request), request.user.url), + "code": tokens.gen_auth_code(request), } - if 'state' in request.POST: - params['state'] = request.POST['state'] + if "state" in request.POST: + params["state"] = request.POST["state"] - uri = request.POST['redirect_uri'] - sep = '&' if '?' in uri else '?' + uri = request.POST["redirect_uri"] + sep = "&" if "?" in uri else "?" return redirect(uri + sep + urlencode(params)) diff --git a/lemonauth/views/login.py b/lemonauth/views/login.py index 5e4b192..cffc18b 100644 --- a/lemonauth/views/login.py +++ b/lemonauth/views/login.py @@ -2,11 +2,11 @@ import django.contrib.auth.views from otp_agents.forms import OTPAuthenticationForm from lemoncurry import breadcrumbs -breadcrumbs.add(route='lemonauth:login', label='log in', parent='home:index') +breadcrumbs.add(route="lemonauth:login", label="log in", parent="home:index") login = django.contrib.auth.views.LoginView.as_view( authentication_form=OTPAuthenticationForm, - extra_context={'title': 'log in'}, - template_name='lemonauth/login.html', + extra_context={"title": "log in"}, + template_name="lemonauth/login.html", redirect_authenticated_user=True, ) diff --git a/lemonauth/views/token.py b/lemonauth/views/token.py index 251c016..3884fa1 100644 --- a/lemonauth/views/token.py +++ b/lemonauth/views/token.py @@ -7,41 +7,42 @@ from ..models import IndieAuthCode from lemoncurry import utils -@method_decorator(csrf_exempt, name='dispatch') +@method_decorator(csrf_exempt, name="dispatch") class TokenView(View): def get(self, req): token = tokens.auth(req) res = { - 'me': token.me, - 'client_id': token.client_id, - 'scope': token.scope, + "me": token.me, + "client_id": token.client_id, + "scope": token.scope, } return utils.choose_type(req, res) def post(self, req): post = req.POST try: - code = IndieAuthCode.objects.get(pk=post.get('code')) + code = IndieAuthCode.objects.get(pk=post.get("code")) except IndieAuthCode.DoesNotExist: - return utils.forbid('invalid auth code') + return utils.forbid("invalid auth code") code.delete() if code.expired: - return utils.forbid('invalid auth code') + return utils.forbid("invalid auth code") - if code.response_type != 'code': - return utils.bad_req( - 'this endpoint only supports response_type=code' - ) - if 'client_id' in post and code.client_id != post['client_id']: - return utils.forbid('client id did not match') - if code.redirect_uri != post.get('redirect_uri'): - return utils.forbid('redirect uri did not match') + if code.response_type != "code": + return utils.bad_req("this endpoint only supports response_type=code") + if "client_id" in post and code.client_id != post["client_id"]: + return utils.forbid("client id did not match") + if code.redirect_uri != post.get("redirect_uri"): + return utils.forbid("redirect uri did not match") - if 'me' in post and code.me != post['me']: - return utils.forbid('me did not match') + if "me" in post and code.me != post["me"]: + return utils.forbid("me did not match") - return utils.choose_type(req, { - 'access_token': tokens.gen_token(code), - 'me': code.me, - 'scope': code.scope, - }) + return utils.choose_type( + req, + { + "access_token": tokens.gen_token(code), + "me": code.me, + "scope": code.scope, + }, + ) diff --git a/lemonauth/views/tokens/list.py b/lemonauth/views/tokens/list.py index dba4eb3..52e0760 100644 --- a/lemonauth/views/tokens/list.py +++ b/lemonauth/views/tokens/list.py @@ -20,15 +20,15 @@ class Client: self.id = client_id self.count = 0 self.scopes = set() - apps = mf2(self.id).to_dict(filter_by_type='h-x-app') + apps = mf2(self.id).to_dict(filter_by_type="h-x-app") try: - self.app = apps[0]['properties'] + self.app = apps[0]["properties"] except IndexError: self.app = None class TokensListView(LoginRequiredMixin, TemplateView): - template_name = 'lemonauth/tokens.html' + template_name = "lemonauth/tokens.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -36,6 +36,6 @@ class TokensListView(LoginRequiredMixin, TemplateView): for token in self.request.user.token_set.all(): client = clients[token.client_id] client.count += 1 - client.scopes |= set(token.scope.split(' ')) - context.update({'clients': clients, 'title': 'tokens'}) + client.scopes |= set(token.scope.split(" ")) + context.update({"clients": clients, "title": "tokens"}) return context diff --git a/lemoncurry/breadcrumbs.py b/lemoncurry/breadcrumbs.py index bd88b35..cba1a00 100644 --- a/lemoncurry/breadcrumbs.py +++ b/lemoncurry/breadcrumbs.py @@ -14,7 +14,7 @@ class Crumb: return self._label def __eq__(self, other): - if hasattr(other, 'route'): + if hasattr(other, "route"): return self.route == other.route return self.route == other diff --git a/lemoncurry/debug.py b/lemoncurry/debug.py index d518f52..7e3420f 100644 --- a/lemoncurry/debug.py +++ b/lemoncurry/debug.py @@ -2,6 +2,6 @@ from debug_toolbar.middleware import show_toolbar as core_show_toolbar def show_toolbar(request): - if request.path.endswith('/amp'): + if request.path.endswith("/amp"): return False return core_show_toolbar(request) diff --git a/lemoncurry/jinja2/__init__.py b/lemoncurry/jinja2/__init__.py index 0182a8f..2379b89 100644 --- a/lemoncurry/jinja2/__init__.py +++ b/lemoncurry/jinja2/__init__.py @@ -22,18 +22,22 @@ def environment(**options): lstrip_blocks=True, **options ) - env.filters.update({ - 'ago': ago, - 'friendly_url': friendly_url, - 'markdown': markdown, - }) - env.globals.update({ - 'entry_kinds': entry_kinds, - 'favicons': favicons, - 'package': load_package_json(), - 'settings': settings, - 'static': staticfiles_storage.url, - 'theme_color': theme_color, - 'url': reverse, - }) + env.filters.update( + { + "ago": ago, + "friendly_url": friendly_url, + "markdown": markdown, + } + ) + env.globals.update( + { + "entry_kinds": entry_kinds, + "favicons": favicons, + "package": load_package_json(), + "settings": settings, + "static": staticfiles_storage.url, + "theme_color": theme_color, + "url": reverse, + } + ) return env diff --git a/lemoncurry/jinja2/ago.py b/lemoncurry/jinja2/ago.py index 4794b0e..c061619 100644 --- a/lemoncurry/jinja2/ago.py +++ b/lemoncurry/jinja2/ago.py @@ -6,4 +6,4 @@ def ago(dt: datetime) -> str: # We have to convert the datetime we get to local time first, because ago # just strips the timezone from a timezone-aware datetime. dt = dt.astimezone() - return human(dt, precision=1, past_tense='{}', abbreviate=True) + return human(dt, precision=1, past_tense="{}", abbreviate=True) diff --git a/lemoncurry/jinja2/bleach.py b/lemoncurry/jinja2/bleach.py index 628f3f2..401e9ed 100644 --- a/lemoncurry/jinja2/bleach.py +++ b/lemoncurry/jinja2/bleach.py @@ -3,13 +3,13 @@ from bleach.linkifier import LinkifyFilter from jinja2 import pass_eval_context from markupsafe import Markup -TAGS = ['cite', 'code', 'details', 'p', 'pre', 'img', 'span', 'summary'] +TAGS = ["cite", "code", "details", "p", "pre", "img", "span", "summary"] TAGS.extend(ALLOWED_TAGS) ATTRIBUTES = { - 'a': ['href', 'title', 'class'], - 'details': ['open'], - 'img': ['alt', 'src', 'title'], - 'span': ['class'], + "a": ["href", "title", "class"], + "details": ["open"], + "img": ["alt", "src", "title"], + "span": ["class"], } cleaner = Cleaner(tags=TAGS, attributes=ATTRIBUTES, filters=(LinkifyFilter,)) diff --git a/lemoncurry/jinja2/markdown.py b/lemoncurry/jinja2/markdown.py index 02709c4..37cbb68 100644 --- a/lemoncurry/jinja2/markdown.py +++ b/lemoncurry/jinja2/markdown.py @@ -3,12 +3,14 @@ from markdown import Markdown from .bleach import bleach -md = Markdown(extensions=( - 'extra', - 'sane_lists', - 'smarty', - 'toc', -)) +md = Markdown( + extensions=( + "extra", + "sane_lists", + "smarty", + "toc", + ) +) @pass_eval_context diff --git a/lemoncurry/middleware.py b/lemoncurry/middleware.py index 02221bc..d6d3f3e 100644 --- a/lemoncurry/middleware.py +++ b/lemoncurry/middleware.py @@ -8,7 +8,9 @@ class ResponseException(Exception): class ResponseExceptionMiddleware(MiddlewareMixin): - def process_exception(self, request: HttpRequest, exception: Exception) -> HttpResponse: + def process_exception( + self, request: HttpRequest, exception: Exception + ) -> HttpResponse: if isinstance(exception, ResponseException): return exception.response raise exception diff --git a/lemoncurry/requests.py b/lemoncurry/requests.py index f4fc5eb..a365f88 100644 --- a/lemoncurry/requests.py +++ b/lemoncurry/requests.py @@ -11,7 +11,7 @@ from mf2py import Parser class DjangoCache(BaseCache): @classmethod def key(cls, url): - return 'req:' + sha256(url.encode('utf-8')).hexdigest() + return "req:" + sha256(url.encode("utf-8")).hexdigest() def get(self, url): key = self.key(url) @@ -45,4 +45,4 @@ def get(url): def mf2(url): r = get(url) - return Parser(doc=r.text, url=url, html_parser='html5lib') + return Parser(doc=r.text, url=url, html_parser="html5lib") diff --git a/lemoncurry/settings/base.py b/lemoncurry/settings/base.py index 9b8cdbd..b2c5ba5 100644 --- a/lemoncurry/settings/base.py +++ b/lemoncurry/settings/base.py @@ -16,7 +16,7 @@ from typing import List APPEND_SLASH = False ADMINS = [ - ('dani', 'dani@00dani.me'), + ("dani", "dani@00dani.me"), ] BASE_DIR = path.dirname(path.dirname(path.dirname(path.abspath(__file__)))) @@ -26,13 +26,13 @@ BASE_DIR = path.dirname(path.dirname(path.dirname(path.abspath(__file__)))) # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '6riil57g@r^wprf7mdy((+bs&(6l*phcn9&fd$l0@t-kzj+xww' +SECRET_KEY = "6riil57g@r^wprf7mdy((+bs&(6l*phcn9&fd$l0@t-kzj+xww" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS: List[str] = [] -INTERNAL_IPS = ['127.0.0.1', '::1'] +INTERNAL_IPS = ["127.0.0.1", "::1"] # Settings to tighten up security - these can safely be on in dev mode too, # since I dev using a local HTTPS server. @@ -50,7 +50,7 @@ CSRF_COOKIE_SECURE = True # Miscellanous headers to protect against attacks. SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_BROWSER_XSS_FILTER = True -X_FRAME_OPTIONS = 'DENY' +X_FRAME_OPTIONS = "DENY" # This technically isn't needed, since nginx doesn't let the app be accessed # over insecure HTTP anyway. Just for completeness! @@ -58,109 +58,106 @@ SECURE_SSL_REDIRECT = True # We run behind nginx, so we need nginx to tell us whether we're using HTTPS or # not. -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Application definition INSTALLED_APPS = [ - 'lemoncurry', - 'pyup_django', - - 'django.contrib.admin', - 'django.contrib.admindocs', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.humanize', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.sitemaps', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - 'analytical', - 'annoying', - 'compressor', - 'computed_property', - 'corsheaders', - 'debug_toolbar', - 'django_activeurl', - 'django_agent_trust', - 'django_extensions', - 'django_otp', - 'django_otp.plugins.otp_static', - 'django_otp.plugins.otp_totp', - 'django_rq', - 'meta', - - 'entries', - 'home', - 'lemonauth', - 'lemonshort', - 'micropub', - 'users', - 'webmention', - 'wellknowns', + "lemoncurry", + "pyup_django", + "django.contrib.admin", + "django.contrib.admindocs", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.humanize", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.sitemaps", + "django.contrib.messages", + "django.contrib.staticfiles", + "analytical", + "annoying", + "compressor", + "computed_property", + "corsheaders", + "debug_toolbar", + "django_activeurl", + "django_agent_trust", + "django_extensions", + "django_otp", + "django_otp.plugins.otp_static", + "django_otp.plugins.otp_totp", + "django_rq", + "meta", + "entries", + "home", + "lemonauth", + "lemonshort", + "micropub", + "users", + "webmention", + "wellknowns", ] MIDDLEWARE = [ - 'debug_toolbar.middleware.DebugToolbarMiddleware', - 'django.middleware.http.ConditionalGetMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.admindocs.middleware.XViewMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django_otp.middleware.OTPMiddleware', - 'django_agent_trust.middleware.AgentMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.contrib.sites.middleware.CurrentSiteMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'lemoncurry.middleware.ResponseExceptionMiddleware', + "debug_toolbar.middleware.DebugToolbarMiddleware", + "django.middleware.http.ConditionalGetMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.admindocs.middleware.XViewMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django_otp.middleware.OTPMiddleware", + "django_agent_trust.middleware.AgentMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.contrib.sites.middleware.CurrentSiteMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "lemoncurry.middleware.ResponseExceptionMiddleware", ] -ROOT_URLCONF = 'lemoncurry.urls' +ROOT_URLCONF = "lemoncurry.urls" -SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' +SESSION_ENGINE = "django.contrib.sessions.backends.cached_db" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.jinja2.Jinja2', - 'APP_DIRS': True, - 'OPTIONS': { - 'environment': 'lemoncurry.jinja2.environment', + "BACKEND": "django.template.backends.jinja2.Jinja2", + "APP_DIRS": True, + "OPTIONS": { + "environment": "lemoncurry.jinja2.environment", }, }, { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'lemoncurry.wsgi.application' +WSGI_APPLICATION = "lemoncurry.wsgi.application" # Cache # https://docs.djangoproject.com/en/1.11/ref/settings/#std:setting-CACHES CACHES = { - 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': 'redis://127.0.0.1:6380/0', - 'KEY_PREFIX': 'lemoncurry', - 'OPTIONS': { - 'PARSER_CLASS': 'redis.connection.HiredisParser', - 'SERIALIZER': 'lemoncurry.msgpack.MSGPackModernSerializer', + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6380/0", + "KEY_PREFIX": "lemoncurry", + "OPTIONS": { + "PARSER_CLASS": "redis.connection.HiredisParser", + "SERIALIZER": "lemoncurry.msgpack.MSGPackModernSerializer", }, - 'VERSION': 2, + "VERSION": 2, } } @@ -168,51 +165,51 @@ CACHES = { # https://docs.djangoproject.com/en/1.11/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': environ.get('POSTGRES_DB', 'lemoncurry'), - 'USER': environ.get('POSTGRES_USER'), - 'PASSWORD': environ.get('POSTGRES_PASSWORD'), - 'HOST': environ.get('POSTGRES_HOST', 'localhost'), + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": environ.get("POSTGRES_DB", "lemoncurry"), + "USER": environ.get("POSTGRES_USER"), + "PASSWORD": environ.get("POSTGRES_PASSWORD"), + "HOST": environ.get("POSTGRES_HOST", "localhost"), } } -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" -AUTH_USER_MODEL = 'users.User' +AUTH_USER_MODEL = "users.User" # Password hashers # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators PASSWORD_HASHERS = [ - 'django.contrib.auth.hashers.Argon2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', - 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', - 'django.contrib.auth.hashers.BCryptPasswordHasher', + "django.contrib.auth.hashers.Argon2PasswordHasher", + "django.contrib.auth.hashers.PBKDF2PasswordHasher", + "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", + "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", + "django.contrib.auth.hashers.BCryptPasswordHasher", ] # Password validation # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators -PW_VALIDATOR_MODULE = 'django.contrib.auth.password_validation' +PW_VALIDATOR_MODULE = "django.contrib.auth.password_validation" AUTH_PASSWORD_VALIDATORS = [ - {'NAME': PW_VALIDATOR_MODULE + '.UserAttributeSimilarityValidator'}, - {'NAME': PW_VALIDATOR_MODULE + '.MinimumLengthValidator'}, - {'NAME': PW_VALIDATOR_MODULE + '.CommonPasswordValidator'}, - {'NAME': PW_VALIDATOR_MODULE + '.NumericPasswordValidator'}, + {"NAME": PW_VALIDATOR_MODULE + ".UserAttributeSimilarityValidator"}, + {"NAME": PW_VALIDATOR_MODULE + ".MinimumLengthValidator"}, + {"NAME": PW_VALIDATOR_MODULE + ".CommonPasswordValidator"}, + {"NAME": PW_VALIDATOR_MODULE + ".NumericPasswordValidator"}, ] -LOGIN_URL = 'lemonauth:login' -LOGIN_REDIRECT_URL = 'home:index' +LOGIN_URL = "lemonauth:login" +LOGIN_REDIRECT_URL = "home:index" LOGOUT_REDIRECT_URL = LOGIN_REDIRECT_URL # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ -LANGUAGE_CODE = 'en-au' +LANGUAGE_CODE = "en-au" -TIME_ZONE = 'Australia/Sydney' +TIME_ZONE = "Australia/Sydney" USE_I18N = True @@ -224,23 +221,21 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ -STATIC_URL = '/static/' -STATIC_ROOT = path.join(BASE_DIR, 'static') +STATIC_URL = "/static/" +STATIC_ROOT = path.join(BASE_DIR, "static") STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'compressor.finders.CompressorFinder', -) -STATICFILES_STORAGE = ( - 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + "compressor.finders.CompressorFinder", ) +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage" COMPRESS_PRECOMPILERS = ( - ('text/stylus', 'npx stylus -u ./lemoncurry/static/lemoncurry/css/theme'), + ("text/stylus", "npx stylus -u ./lemoncurry/static/lemoncurry/css/theme"), ) -MEDIA_URL = STATIC_URL + 'media/' -MEDIA_ROOT = path.join(STATIC_ROOT, 'media') +MEDIA_URL = STATIC_URL + "media/" +MEDIA_ROOT = path.join(STATIC_ROOT, "media") # django-contrib-sites # https://docs.djangoproject.com/en/dev/ref/contrib/sites/ @@ -252,25 +247,25 @@ AGENT_COOKIE_SECURE = True # django-cors-headers CORS_ORIGIN_ALLOW_ALL = True -CORS_URLS_REGEX = r'^/(?!admin|auth/(?:login|logout|indie)).*$' +CORS_URLS_REGEX = r"^/(?!admin|auth/(?:login|logout|indie)).*$" # lemonshort -SHORT_BASE_URL = '/s/' +SHORT_BASE_URL = "/s/" SHORTEN_MODELS = { - 'e': 'entries.entry', + "e": "entries.entry", } # django-meta # https://django-meta.readthedocs.io/en/latest/settings.html -META_SITE_PROTOCOL = 'https' +META_SITE_PROTOCOL = "https" META_USE_SITES = True META_USE_OG_PROPERTIES = True META_USE_TWITTER_PROPERTIES = True # django-push # https://django-push.readthedocs.io/en/latest/publisher.html -PUSH_HUB = 'https://00dani.superfeedr.com/' +PUSH_HUB = "https://00dani.superfeedr.com/" # django-rq # https://github.com/ui/django-rq -RQ_QUEUES = {'default': {'USE_REDIS_CACHE': 'default'}} +RQ_QUEUES = {"default": {"USE_REDIS_CACHE": "default"}} diff --git a/lemoncurry/settings/dev.py b/lemoncurry/settings/dev.py index 999387d..1c4edc1 100644 --- a/lemoncurry/settings/dev.py +++ b/lemoncurry/settings/dev.py @@ -1,7 +1,7 @@ from .base import * -ALLOWED_HOSTS = ['*'] -META_SITE_DOMAIN = '00dani.lo' -META_FB_APPID = '142105433189339' -STATIC_URL = 'https://static.00dani.lo/' -MEDIA_URL = 'https://media.00dani.lo/' +ALLOWED_HOSTS = ["*"] +META_SITE_DOMAIN = "00dani.lo" +META_FB_APPID = "142105433189339" +STATIC_URL = "https://static.00dani.lo/" +MEDIA_URL = "https://media.00dani.lo/" diff --git a/lemoncurry/settings/prod.py b/lemoncurry/settings/prod.py index 31f7c84..a4407ca 100644 --- a/lemoncurry/settings/prod.py +++ b/lemoncurry/settings/prod.py @@ -4,19 +4,19 @@ from os.path import join from .base import * from .base import BASE_DIR, DATABASES -ALLOWED_HOSTS = ['00dani.me'] +ALLOWED_HOSTS = ["00dani.me"] DEBUG = False -SECRET_KEY = environ['DJANGO_SECRET_KEY'] -SERVER_EMAIL = 'lemoncurry@00dani.me' +SECRET_KEY = environ["DJANGO_SECRET_KEY"] +SERVER_EMAIL = "lemoncurry@00dani.me" # Authenticate as an app-specific Postgres user in production. -DATABASES['default']['USER'] = 'lemoncurry' +DATABASES["default"]["USER"] = "lemoncurry" -SHORT_BASE_URL = 'https://nya.as/' +SHORT_BASE_URL = "https://nya.as/" -STATIC_ROOT = join(BASE_DIR, '..', 'static') -MEDIA_ROOT = join(BASE_DIR, '..', 'media') -STATIC_URL = 'https://cdn.00dani.me/' -MEDIA_URL = STATIC_URL + 'm/' -META_SITE_DOMAIN = '00dani.me' -META_FB_APPID = '145311792869199' +STATIC_ROOT = join(BASE_DIR, "..", "static") +MEDIA_ROOT = join(BASE_DIR, "..", "media") +STATIC_URL = "https://cdn.00dani.me/" +MEDIA_URL = STATIC_URL + "m/" +META_SITE_DOMAIN = "00dani.me" +META_FB_APPID = "145311792869199" diff --git a/lemoncurry/settings/test.py b/lemoncurry/settings/test.py index c3eb440..c8dc698 100644 --- a/lemoncurry/settings/test.py +++ b/lemoncurry/settings/test.py @@ -1,8 +1,8 @@ from .base import * -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] SECURE_SSL_REDIRECT = False -STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" -MEDIA_URL = '/media/' -STATIC_ROOT = path.join(BASE_DIR, 'media') +MEDIA_URL = "/media/" +STATIC_ROOT = path.join(BASE_DIR, "media") diff --git a/lemoncurry/templatetags/absolute_url.py b/lemoncurry/templatetags/absolute_url.py index 592079b..9724e3c 100644 --- a/lemoncurry/templatetags/absolute_url.py +++ b/lemoncurry/templatetags/absolute_url.py @@ -8,5 +8,5 @@ register = template.Library() @register.simple_tag @register.filter(is_safe=True) def absolute_url(url): - base = 'https://' + Site.objects.get_current().domain + base = "https://" + Site.objects.get_current().domain return urljoin(base, url) diff --git a/lemoncurry/templatetags/bleach.py b/lemoncurry/templatetags/bleach.py index 7b9ffc3..cc410b4 100644 --- a/lemoncurry/templatetags/bleach.py +++ b/lemoncurry/templatetags/bleach.py @@ -5,13 +5,13 @@ from django.utils.safestring import mark_safe from bleach.sanitizer import Cleaner, ALLOWED_TAGS from bleach.linkifier import LinkifyFilter -tags = ['cite', 'code', 'details', 'p', 'pre', 'img', 'span', 'summary'] +tags = ["cite", "code", "details", "p", "pre", "img", "span", "summary"] tags.extend(ALLOWED_TAGS) attributes = { - 'a': ['href', 'title', 'class'], - 'details': ['open'], - 'img': ['alt', 'src', 'title'], - 'span': ['class'], + "a": ["href", "title", "class"], + "details": ["open"], + "img": ["alt", "src", "title"], + "span": ["class"], } register = template.Library() diff --git a/lemoncurry/templatetags/jsonify.py b/lemoncurry/templatetags/jsonify.py index 377ebb7..50864a1 100644 --- a/lemoncurry/templatetags/jsonify.py +++ b/lemoncurry/templatetags/jsonify.py @@ -11,5 +11,5 @@ register = template.Library() @register.filter def jsonify(value): if isinstance(value, QuerySet): - return mark_safe(serialize('json', value)) + return mark_safe(serialize("json", value)) return mark_safe(json.dumps(value, cls=DjangoJSONEncoder)) diff --git a/lemoncurry/templatetags/lemoncurry_tags.py b/lemoncurry/templatetags/lemoncurry_tags.py index 0968f43..bd865f0 100644 --- a/lemoncurry/templatetags/lemoncurry_tags.py +++ b/lemoncurry/templatetags/lemoncurry_tags.py @@ -1,4 +1,3 @@ - from django import template from django.conf import settings from django.contrib.sites.models import Site @@ -40,67 +39,71 @@ def site_name(): return Site.objects.get_current().name -@register.inclusion_tag('lemoncurry/tags/nav.html') +@register.inclusion_tag("lemoncurry/tags/nav.html") def nav_left(request): - items = (MenuItem( - label=k.plural, - icon=k.icon, - url=('entries:index', (k,)) - ) for k in kinds.all) - return {'items': items, 'request': request} + items = ( + MenuItem(label=k.plural, icon=k.icon, url=("entries:index", (k,))) + for k in kinds.all + ) + return {"items": items, "request": request} -@register.inclusion_tag('lemoncurry/tags/nav.html') +@register.inclusion_tag("lemoncurry/tags/nav.html") def nav_right(request): if request.user.is_authenticated: items = ( - MenuItem(label='admin', icon='fas fa-cog', url='admin:index'), - MenuItem(label='log out', icon='fas fa-sign-out-alt', - url='lemonauth:logout'), + MenuItem(label="admin", icon="fas fa-cog", url="admin:index"), + MenuItem( + label="log out", icon="fas fa-sign-out-alt", url="lemonauth:logout" + ), ) else: items = ( - MenuItem(label='log in', icon='fas fa-sign-in-alt', - url='lemonauth:login'), + MenuItem(label="log in", icon="fas fa-sign-in-alt", url="lemonauth:login"), ) - return {'items': items, 'request': request} + return {"items": items, "request": request} -@register.inclusion_tag('lemoncurry/tags/breadcrumbs.html', takes_context=True) +@register.inclusion_tag("lemoncurry/tags/breadcrumbs.html", takes_context=True) def nav_crumbs(context, route): crumbs = breadcrumbs.find(route) current = crumbs.pop() - item_list_element = [{ - '@type': 'ListItem', - 'position': i + 1, - 'item': { - '@id': context['origin'] + crumb.url, - '@type': 'WebPage', - 'name': crumb.label + item_list_element = [ + { + "@type": "ListItem", + "position": i + 1, + "item": { + "@id": context["origin"] + crumb.url, + "@type": "WebPage", + "name": crumb.label, + }, } - } for i, crumb in enumerate(crumbs)] - item_list_element.append({ - '@type': 'ListItem', - 'position': len(item_list_element) + 1, - 'item': { - 'id': context['uri'], - '@type': 'WebPage', - 'name': current.label or context.get('title'), + for i, crumb in enumerate(crumbs) + ] + item_list_element.append( + { + "@type": "ListItem", + "position": len(item_list_element) + 1, + "item": { + "id": context["uri"], + "@type": "WebPage", + "name": current.label or context.get("title"), + }, } - }) + ) breadcrumb_list = { - '@context': 'http://schema.org', - '@type': 'BreadcrumbList', - 'itemListElement': item_list_element + "@context": "http://schema.org", + "@type": "BreadcrumbList", + "itemListElement": item_list_element, } return { - 'breadcrumb_list': breadcrumb_list, - 'crumbs': crumbs, - 'current': current, - 'title': context.get('title'), + "breadcrumb_list": breadcrumb_list, + "crumbs": crumbs, + "current": current, + "title": context.get("title"), } diff --git a/lemoncurry/templatetags/markdown.py b/lemoncurry/templatetags/markdown.py index 1505987..3452a1f 100644 --- a/lemoncurry/templatetags/markdown.py +++ b/lemoncurry/templatetags/markdown.py @@ -3,12 +3,14 @@ from django import template from markdown import Markdown from .bleach import bleach -md = Markdown(extensions=( - 'extra', - 'sane_lists', - 'smarty', - 'toc', -)) +md = Markdown( + extensions=( + "extra", + "sane_lists", + "smarty", + "toc", + ) +) register = template.Library() diff --git a/lemoncurry/tests/breadcrumbs.py b/lemoncurry/tests/breadcrumbs.py index 28d8e14..8372ba9 100644 --- a/lemoncurry/tests/breadcrumbs.py +++ b/lemoncurry/tests/breadcrumbs.py @@ -6,43 +6,43 @@ from .. import breadcrumbs as b @pytest.fixture def nested_crumbs(): - x = b.Crumb('nc.x', label='x') - y = b.Crumb('nc.y', label='y', parent='nc.x') - z = b.Crumb('nc.z', label='z', parent='nc.y') + x = b.Crumb("nc.x", label="x") + y = b.Crumb("nc.y", label="y", parent="nc.x") + z = b.Crumb("nc.z", label="z", parent="nc.y") crumbs = (x, y, z) for crumb in crumbs: b.breadcrumbs[crumb.route] = crumb - yield namedtuple('NestedCrumbs', 'x y z')(*crumbs) + yield namedtuple("NestedCrumbs", "x y z")(*crumbs) for crumb in crumbs: del b.breadcrumbs[crumb.route] @pytest.fixture def crumb_match(nested_crumbs): - return namedtuple('Match', 'view_name')(nested_crumbs.z.route) + return namedtuple("Match", "view_name")(nested_crumbs.z.route) class TestAdd: def test_inserts_a_breadcrumb_without_parent(self): - route = 'tests.add.insert' + route = "tests.add.insert" assert route not in b.breadcrumbs - b.add(route, 'some label') + b.add(route, "some label") assert route in b.breadcrumbs assert b.breadcrumbs[route] == route route = b.breadcrumbs[route] - assert route.label == 'some label' + assert route.label == "some label" assert route.parent is None def test_inserts_a_breadcrumb_with_parent(self): - route = 'tests.add.with_parent' - parent = 'tests.add.insert' + route = "tests.add.with_parent" + parent = "tests.add.insert" assert route not in b.breadcrumbs - b.add(route, 'child label', parent) + b.add(route, "child label", parent) assert route in b.breadcrumbs assert b.breadcrumbs[route] == route route = b.breadcrumbs[route] - assert route.label == 'child label' + assert route.label == "child label" assert route.parent == parent diff --git a/lemoncurry/tests/utils.py b/lemoncurry/tests/utils.py index 5745087..550a49a 100644 --- a/lemoncurry/tests/utils.py +++ b/lemoncurry/tests/utils.py @@ -5,22 +5,22 @@ from .. import utils class TestOrigin: def test_simple_http(self): """should return the correct origin for a vanilla HTTP site""" - req = Mock(scheme='http', site=Mock(domain='lemoncurry.test')) - assert utils.origin(req) == 'http://lemoncurry.test' + req = Mock(scheme="http", site=Mock(domain="lemoncurry.test")) + assert utils.origin(req) == "http://lemoncurry.test" def test_simple_https(self): """should return the correct origin for a vanilla HTTPS site""" - req = Mock(scheme='https', site=Mock(domain='secure.lemoncurry.test')) - assert utils.origin(req) == 'https://secure.lemoncurry.test' + req = Mock(scheme="https", site=Mock(domain="secure.lemoncurry.test")) + assert utils.origin(req) == "https://secure.lemoncurry.test" class TestUri: def test_siteroot(self): """should return correct full URI for requests to the site root""" - req = Mock(scheme='https', path='/', site=Mock(domain='l.test')) - assert utils.uri(req) == 'https://l.test/' + req = Mock(scheme="https", path="/", site=Mock(domain="l.test")) + assert utils.uri(req) == "https://l.test/" def test_path(self): """should return correct full URI for requests with a path""" - req = Mock(scheme='https', path='/notes/23', site=Mock(domain='l.tst')) - assert utils.uri(req) == 'https://l.tst/notes/23' + req = Mock(scheme="https", path="/notes/23", site=Mock(domain="l.tst")) + assert utils.uri(req) == "https://l.tst/notes/23" diff --git a/lemoncurry/theme.py b/lemoncurry/theme.py index b282e13..437dc6e 100644 --- a/lemoncurry/theme.py +++ b/lemoncurry/theme.py @@ -4,12 +4,14 @@ from yaml import safe_load path = join( settings.BASE_DIR, - 'lemoncurry', 'static', - 'base16-materialtheme-scheme', 'material-darker.yaml', + "lemoncurry", + "static", + "base16-materialtheme-scheme", + "material-darker.yaml", ) -with open(path, 'r') as f: +with open(path, "r") as f: theme = safe_load(f) def color(i): - return '#' + theme['base0' + format(i, '1X')] + return "#" + theme["base0" + format(i, "1X")] diff --git a/lemoncurry/urls.py b/lemoncurry/urls.py index 0548105..e2d72bb 100644 --- a/lemoncurry/urls.py +++ b/lemoncurry/urls.py @@ -27,33 +27,37 @@ from entries.sitemaps import EntriesSitemap from home.sitemaps import HomeSitemap sections = { - 'entries': EntriesSitemap, - 'home': HomeSitemap, + "entries": EntriesSitemap, + "home": HomeSitemap, } -maps = {'sitemaps': sections} +maps = {"sitemaps": sections} urlpatterns = ( - path('', include('home.urls')), - path('', include('entries.urls')), - path('', include('users.urls')), - path('.well-known/', include('wellknowns.urls')), - path('admin/doc/', include('django.contrib.admindocs.urls')), - path('admin/', admin.site.urls), - path('auth/', include('lemonauth.urls')), - path('favicon.ico', RedirectView.as_view( - url=settings.MEDIA_URL + 'favicon/favicon.ico')), - path('micropub', include('micropub.urls')), - path('s/', include('lemonshort.urls')), - path('webmention', include('webmention.urls')), - - path('django-rq/', include('django_rq.urls')), - path('sitemap.xml', sitemap.index, maps, name='sitemap'), - path('sitemaps/
.xml', sitemap.sitemap, maps, - name='django.contrib.sitemaps.views.sitemap'), + path("", include("home.urls")), + path("", include("entries.urls")), + path("", include("users.urls")), + path(".well-known/", include("wellknowns.urls")), + path("admin/doc/", include("django.contrib.admindocs.urls")), + path("admin/", admin.site.urls), + path("auth/", include("lemonauth.urls")), + path( + "favicon.ico", + RedirectView.as_view(url=settings.MEDIA_URL + "favicon/favicon.ico"), + ), + path("micropub", include("micropub.urls")), + path("s/", include("lemonshort.urls")), + path("webmention", include("webmention.urls")), + path("django-rq/", include("django_rq.urls")), + path("sitemap.xml", sitemap.index, maps, name="sitemap"), + path( + "sitemaps/
.xml", + sitemap.sitemap, + maps, + name="django.contrib.sitemaps.views.sitemap", + ), ) # type: Tuple[URLPattern, ...] if settings.DEBUG: import debug_toolbar - urlpatterns += ( - path('__debug__/', include(debug_toolbar.urls)), - ) + + urlpatterns += (path("__debug__/", include(debug_toolbar.urls)),) diff --git a/lemoncurry/utils.py b/lemoncurry/utils.py index 1ac70d2..b4b2434 100644 --- a/lemoncurry/utils.py +++ b/lemoncurry/utils.py @@ -20,7 +20,7 @@ class PackageJson: def load(self) -> Dict[str, Any]: if self.data is None: - with open(join(settings.BASE_DIR, 'package.json')) as f: + with open(join(settings.BASE_DIR, "package.json")) as f: self.data = json.load(f) assert self.data is not None return self.data @@ -30,10 +30,10 @@ PACKAGE = PackageJson() def friendly_url(url): - if '//' not in url: - url = '//' + url + if "//" not in url: + url = "//" + url (scheme, netloc, path, params, q, fragment) = urlparse(url) - if path == '/': + if path == "/": return netloc return "{}\u200B{}".format(netloc, path) @@ -43,7 +43,7 @@ def load_package_json() -> Dict[str, Any]: def origin(request): - return '{0}://{1}'.format(request.scheme, request.site.domain) + return "{0}://{1}".format(request.scheme, request.site.domain) def absolute_url(request, url): @@ -56,19 +56,18 @@ def uri(request): def form_encoded_response(content): return HttpResponse( - urlencode(content), - content_type='application/x-www-form-urlencoded' + urlencode(content), content_type="application/x-www-form-urlencoded" ) REPS = { - 'application/x-www-form-urlencoded': form_encoded_response, - 'application/json': JsonResponse, + "application/x-www-form-urlencoded": form_encoded_response, + "application/json": JsonResponse, } def choose_type(request, content, reps=REPS): - accept = request.META.get('HTTP_ACCEPT', '*/*') + accept = request.META.get("HTTP_ACCEPT", "*/*") type = get_best_match(accept, reps.keys()) if type: return reps[type](content) @@ -76,11 +75,11 @@ def choose_type(request, content, reps=REPS): def bad_req(message): - return HttpResponseBadRequest(message, content_type='text/plain') + return HttpResponseBadRequest(message, content_type="text/plain") def forbid(message): - return HttpResponseForbidden(message, content_type='text/plain') + return HttpResponseForbidden(message, content_type="text/plain") def to_plain(md): diff --git a/lemonshort/apps.py b/lemonshort/apps.py index 4edb115..259f20a 100644 --- a/lemonshort/apps.py +++ b/lemonshort/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class LemonshortConfig(AppConfig): - name = 'lemonshort' + name = "lemonshort" diff --git a/lemonshort/convert.py b/lemonshort/convert.py index 936df44..d408610 100644 --- a/lemonshort/convert.py +++ b/lemonshort/convert.py @@ -6,8 +6,9 @@ from string import ascii_lowercase, ascii_uppercase chars = ascii_uppercase + ascii_lowercase conv = BaseConverter(chars) + class AbcIdConverter: - regex = '[a-zA-Z]+' + regex = "[a-zA-Z]+" def to_python(self, value: str) -> int: return int(conv.decode(value)) diff --git a/lemonshort/short_url.py b/lemonshort/short_url.py index 76d7c62..d66461b 100644 --- a/lemonshort/short_url.py +++ b/lemonshort/short_url.py @@ -11,7 +11,7 @@ def short_url(entity): if not prefixes: for k, m in settings.SHORTEN_MODELS.items(): prefixes[apps.get_model(m)] = k - base = '/' - if hasattr(settings, 'SHORT_BASE_URL'): + base = "/" + if hasattr(settings, "SHORT_BASE_URL"): base = settings.SHORT_BASE_URL return base + prefixes[type(entity)] + AbcIdConverter().to_url(entity.id) diff --git a/lemonshort/tests/convert.py b/lemonshort/tests/convert.py index 80d6299..033aa15 100644 --- a/lemonshort/tests/convert.py +++ b/lemonshort/tests/convert.py @@ -3,14 +3,14 @@ from .. import convert def test_to_python(): samples = { - 'A': 0, - 'B': 1, - 'Y': 24, - 'a': 26, - 'b': 27, - 'y': 50, - 'BA': 52, - 'BAB': 2705, + "A": 0, + "B": 1, + "Y": 24, + "a": 26, + "b": 27, + "y": 50, + "BA": 52, + "BAB": 2705, } converter = convert.AbcIdConverter() for abc, id in samples.items(): @@ -19,13 +19,13 @@ def test_to_python(): def test_id_to_abc(): samples = { - 1: 'B', - 24: 'Y', - 26: 'a', - 52: 'BA', - 78: 'Ba', - 104: 'CA', - 130: 'Ca', + 1: "B", + 24: "Y", + 26: "a", + 52: "BA", + 78: "Ba", + 104: "CA", + 130: "Ca", } converter = convert.AbcIdConverter() for id, abc in samples.items(): diff --git a/lemonshort/urls.py b/lemonshort/urls.py index 7fcf49c..385a256 100644 --- a/lemonshort/urls.py +++ b/lemonshort/urls.py @@ -4,10 +4,10 @@ from django.urls import path, register_converter from .convert import AbcIdConverter from .views import unshort -register_converter(AbcIdConverter, 'abc_id') +register_converter(AbcIdConverter, "abc_id") -app_name = 'lemonshort' +app_name = "lemonshort" urlpatterns = tuple( - path('{0!s}'.format(k), unshort, name=m, kwargs={'model': m}) + path("{0!s}".format(k), unshort, name=m, kwargs={"model": m}) for k, m in settings.SHORTEN_MODELS.items() ) diff --git a/micropub/apps.py b/micropub/apps.py index d5259ee..8732a39 100644 --- a/micropub/apps.py +++ b/micropub/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class MicropubConfig(AppConfig): - name = 'micropub' + name = "micropub" diff --git a/micropub/error.py b/micropub/error.py index a5e9373..20a745d 100644 --- a/micropub/error.py +++ b/micropub/error.py @@ -4,33 +4,35 @@ from typing import Optional def forbidden() -> ResponseException: - return res('forbidden', 403) + return res("forbidden", 403) def unauthorized() -> ResponseException: - return res('unauthorized', 401) + return res("unauthorized", 401) def bad_req(msg: str) -> ResponseException: - return res('invalid_request', msg=msg) + return res("invalid_request", msg=msg) def bad_type(type: str) -> ResponseException: - msg = 'unsupported request type {0}'.format(type) - return res('invalid_request', 415, msg) + msg = "unsupported request type {0}".format(type) + return res("invalid_request", 415, msg) def bad_scope(scope: str) -> ResponseException: - return res('insufficient_scope', 401, scope=scope) + return res("insufficient_scope", 401, scope=scope) -def res(error: str, - status: Optional[int]=400, - msg: Optional[str]=None, - scope: Optional[str]=None): - content = {'error': error} +def res( + error: str, + status: Optional[int] = 400, + msg: Optional[str] = None, + scope: Optional[str] = None, +): + content = {"error": error} if msg is not None: - content['error_description'] = msg + content["error_description"] = msg if scope: - content['scope'] = scope + content["scope"] = scope return ResponseException(JsonResponse(content, status=status)) diff --git a/micropub/urls.py b/micropub/urls.py index 546d82f..06acd70 100644 --- a/micropub/urls.py +++ b/micropub/urls.py @@ -2,8 +2,8 @@ from django.urls import path from .views import micropub from .views.media import media -app_name = 'micropub' +app_name = "micropub" urlpatterns = ( - path('', micropub, name='micropub'), - path('/media', media, name='media'), + path("", micropub, name="micropub"), + path("/media", media, name="media"), ) diff --git a/micropub/views/__init__.py b/micropub/views/__init__.py index 7ff7878..96d99e9 100644 --- a/micropub/views/__init__.py +++ b/micropub/views/__init__.py @@ -10,22 +10,22 @@ from .delete import delete from .query import query actions = { - 'create': create, - 'delete': delete, + "create": create, + "delete": delete, } @csrf_exempt -@require_http_methods(['GET', 'HEAD', 'POST']) +@require_http_methods(["GET", "HEAD", "POST"]) def micropub(request): request.token = tokens.auth(request) - if request.method in ('GET', 'HEAD'): + if request.method in ("GET", "HEAD"): return query(request) - action = request.POST.get('action', 'create') - if request.content_type == 'application/json': + action = request.POST.get("action", "create") + if request.content_type == "application/json": request.json = json.load(request) - action = request.json.get('action', 'create') + action = request.json.get("action", "create") if action not in actions: - raise error.bad_req('unknown action: {}'.format(action)) + raise error.bad_req("unknown action: {}".format(action)) return actions[action](request) diff --git a/micropub/views/create.py b/micropub/views/create.py index 23bd21e..35e1357 100644 --- a/micropub/views/create.py +++ b/micropub/views/create.py @@ -14,63 +14,62 @@ def form_to_mf2(request): properties = {} post = request.POST for key in post.keys(): - if key.endswith('[]'): + if key.endswith("[]"): key = key[:-2] - if key == 'access_token': + if key == "access_token": continue - properties[key] = post.getlist(key) + post.getlist(key + '[]') + properties[key] = post.getlist(key) + post.getlist(key + "[]") type = [] - if 'h' in properties: - type = ['h-' + p for p in properties['h']] - del properties['h'] - return {'type': type, 'properties': properties} + if "h" in properties: + type = ["h-" + p for p in properties["h"]] + del properties["h"] + return {"type": type, "properties": properties} def create(request): normalise = { - 'application/json': lambda r: r.json, - 'application/x-www-form-urlencoded': form_to_mf2, + "application/json": lambda r: r.json, + "application/x-www-form-urlencoded": form_to_mf2, } - if 'create' not in request.token: - raise error.bad_scope('create') + if "create" not in request.token: + raise error.bad_scope("create") if request.content_type not in normalise: raise error.unsupported_type(request.content_type) body = normalise[request.content_type](request) - if 'type' not in body: - raise error.bad_req('mf2 object type required') - if body['type'] != ['h-entry']: - raise error.bad_req('only h-entry supported') + if "type" not in body: + raise error.bad_req("mf2 object type required") + if body["type"] != ["h-entry"]: + raise error.bad_req("only h-entry supported") entry = Entry(author=request.token.user) - props = body.get('properties', {}) + props = body.get("properties", {}) kind = Note - if 'name' in props: - entry.name = '\n'.join(props['name']) + if "name" in props: + entry.name = "\n".join(props["name"]) kind = Article - if 'content' in props: - entry.content = '\n'.join( - c if isinstance(c, str) else c['html'] - for c in props['content'] + if "content" in props: + entry.content = "\n".join( + c if isinstance(c, str) else c["html"] for c in props["content"] ) - if 'in-reply-to' in props: - entry.in_reply_to = props['in-reply-to'] + if "in-reply-to" in props: + entry.in_reply_to = props["in-reply-to"] kind = Reply - if 'like-of' in props: - entry.like_of = props['like-of'] + if "like-of" in props: + entry.like_of = props["like-of"] kind = Like - if 'repost-of' in props: - entry.repost_of = props['repost-of'] + if "repost-of" in props: + entry.repost_of = props["repost-of"] kind = Repost - cats = [Cat.objects.from_name(c) for c in props.get('category', [])] + cats = [Cat.objects.from_name(c) for c in props.get("category", [])] entry.kind = kind.id entry.save() entry.cats.set(cats) entry.save() - for url in props.get('syndication', []): + for url in props.get("syndication", []): entry.syndications.create(url=url) base = utils.origin(request) @@ -80,6 +79,6 @@ def create(request): send_mentions.delay(perma) res = HttpResponse(status=201) - res['Location'] = perma - res['Link'] = '<{}>; rel="shortlink"'.format(short) + res["Location"] = perma + res["Link"] = '<{}>; rel="shortlink"'.format(short) return res diff --git a/micropub/views/delete.py b/micropub/views/delete.py index 035da7e..db6e13e 100644 --- a/micropub/views/delete.py +++ b/micropub/views/delete.py @@ -6,24 +6,25 @@ from entries.jobs import ping_hub, send_mentions from .. import error + def delete(request): normalise = { - 'application/json': lambda r: r.json.get('url'), - 'application/x-www-form-urlencoded': lambda r: r.POST.get('url'), + "application/json": lambda r: r.json.get("url"), + "application/x-www-form-urlencoded": lambda r: r.POST.get("url"), } - if 'delete' not in request.token: - raise error.bad_scope('delete') + if "delete" not in request.token: + raise error.bad_scope("delete") if request.content_type not in normalise: raise error.unsupported_type(request.content_type) url = normalise[request.content_type](request) entry = from_url(url) if entry.author != request.token.user: - raise error.forbid('entry belongs to another user') + raise error.forbid("entry belongs to another user") perma = entry.absolute_url pings = entry.affected_urls - mentions = webmention.findMentions(perma)['refs'] + mentions = webmention.findMentions(perma)["refs"] entry.delete() diff --git a/micropub/views/media.py b/micropub/views/media.py index db59972..6e77b93 100644 --- a/micropub/views/media.py +++ b/micropub/views/media.py @@ -11,9 +11,9 @@ from lemoncurry.utils import absolute_url from .. import error ACCEPTED_MEDIA_TYPES = ( - 'image/gif', - 'image/jpeg', - 'image/png', + "image/gif", + "image/jpeg", + "image/png", ) @@ -21,15 +21,13 @@ ACCEPTED_MEDIA_TYPES = ( @require_POST def media(request): token = tokens.auth(request) - if 'file' not in request.FILES: + if "file" not in request.FILES: raise error.bad_req( "a file named 'file' must be provided to the media endpoint" ) - file = request.FILES['file'] + file = request.FILES["file"] if file.content_type not in ACCEPTED_MEDIA_TYPES: - raise error.bad_req( - 'unacceptable file type {0}'.format(file.content_type) - ) + raise error.bad_req("unacceptable file type {0}".format(file.content_type)) mime = None sha = hashlib.sha256() @@ -40,14 +38,15 @@ def media(request): if mime != file.content_type: raise error.bad_req( - 'detected file type {0} did not match specified file type {1}' - .format(mime, file.content_type) + "detected file type {0} did not match specified file type {1}".format( + mime, file.content_type + ) ) - path = 'mp/{0[0]}/{2}.{1}'.format(*mime.split('/'), sha.hexdigest()) + path = "mp/{0[0]}/{2}.{1}".format(*mime.split("/"), sha.hexdigest()) path = store.save(path, file) url = absolute_url(request, store.url(path)) res = HttpResponse(status=201) - res['Location'] = url + res["Location"] = url return res diff --git a/micropub/views/query.py b/micropub/views/query.py index 6b5d2d8..4fc72a2 100644 --- a/micropub/views/query.py +++ b/micropub/views/query.py @@ -7,48 +7,47 @@ from lemoncurry.utils import absolute_url from .. import error - def config(request): config = syndicate_to(request) - config['media-endpoint'] = absolute_url(request, reverse('micropub:media')) + config["media-endpoint"] = absolute_url(request, reverse("micropub:media")) return config def source(request): - if 'url' not in request.GET: - raise error.bad_req('must specify url parameter for source query') - entry = from_url(request.GET['url']) + if "url" not in request.GET: + raise error.bad_req("must specify url parameter for source query") + entry = from_url(request.GET["url"]) props = {} - keys = set(request.GET.getlist('properties') + request.GET.getlist('properties[]')) - if not keys or 'content' in keys: - props['content'] = [entry.content] - if (not keys or 'category' in keys) and entry.cats.exists(): - props['category'] = [cat.name for cat in entry.cats.all()] - if (not keys or 'name' in keys) and entry.name: - props['name'] = [entry.name] - if (not keys or 'syndication' in keys) and entry.syndications.exists(): - props['syndication'] = [synd.url for synd in entry.syndications.all()] + keys = set(request.GET.getlist("properties") + request.GET.getlist("properties[]")) + if not keys or "content" in keys: + props["content"] = [entry.content] + if (not keys or "category" in keys) and entry.cats.exists(): + props["category"] = [cat.name for cat in entry.cats.all()] + if (not keys or "name" in keys) and entry.name: + props["name"] = [entry.name] + if (not keys or "syndication" in keys) and entry.syndications.exists(): + props["syndication"] = [synd.url for synd in entry.syndications.all()] - return {'type': ['h-entry'], 'properties': props} + return {"type": ["h-entry"], "properties": props} def syndicate_to(request): - return {'syndicate-to': []} + return {"syndicate-to": []} queries = { - 'config': config, - 'source': source, - 'syndicate-to': syndicate_to, + "config": config, + "source": source, + "syndicate-to": syndicate_to, } def query(request): - if 'q' not in request.GET: - raise error.bad_req('must specify q parameter') - q = request.GET['q'] + if "q" not in request.GET: + raise error.bad_req("must specify q parameter") + q = request.GET["q"] if q not in queries: - raise error.bad_req('unsupported query {0}'.format(q)) + raise error.bad_req("unsupported query {0}".format(q)) res = queries[q](request) return JsonResponse(res) diff --git a/users/admin.py b/users/admin.py index 23755e2..6cd53dd 100644 --- a/users/admin.py +++ b/users/admin.py @@ -4,7 +4,7 @@ from .models import PgpKey, Profile, Site, User class SiteAdmin(admin.ModelAdmin): - list_display = ('name', 'icon', 'domain', 'url_template') + list_display = ("name", "icon", "domain", "url_template") class PgpKeyInline(admin.TabularInline): @@ -19,7 +19,7 @@ class ProfileInline(admin.TabularInline): class UserAdmin(BaseUserAdmin): fieldsets = BaseUserAdmin.fieldsets + ( - ('Profile', {'fields': ('avatar', 'xmpp', 'note')}), + ("Profile", {"fields": ("avatar", "xmpp", "note")}), ) inlines = ( PgpKeyInline, diff --git a/users/apps.py b/users/apps.py index 54e430e..2ffced1 100644 --- a/users/apps.py +++ b/users/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class UsersConfig(AppConfig): - name = 'users' - verbose_name = 'Users and Profiles' + name = "users" + verbose_name = "Users and Profiles" diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index ffec4b2..ba895d4 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -9,40 +9,127 @@ import django.utils.timezone class Migration(migrations.Migration): - initial = True dependencies = [ - ('auth', '0008_alter_user_username_max_length'), + ("auth", "0008_alter_user_username_max_length"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('avatar', models.ImageField(upload_to='')), - ('note', models.TextField(blank=True)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=30, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=30, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ("avatar", models.ImageField(upload_to="")), + ("note", models.TextField(blank=True)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), ], options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, }, managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), ] diff --git a/users/migrations/0002_auto_20171023_0109.py b/users/migrations/0002_auto_20171023_0109.py index 8f521a7..27fbb8f 100644 --- a/users/migrations/0002_auto_20171023_0109.py +++ b/users/migrations/0002_auto_20171023_0109.py @@ -7,15 +7,14 @@ import users.models class Migration(migrations.Migration): - dependencies = [ - ('users', '0001_initial'), + ("users", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='user', - name='avatar', + model_name="user", + name="avatar", field=models.ImageField(upload_to=users.models.avatar_path), ), ] diff --git a/users/migrations/0003_key.py b/users/migrations/0003_key.py index c8e564a..168676e 100644 --- a/users/migrations/0003_key.py +++ b/users/migrations/0003_key.py @@ -8,19 +8,33 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('users', '0002_auto_20171023_0109'), + ("users", "0002_auto_20171023_0109"), ] operations = [ migrations.CreateModel( - name='Key', + name="Key", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('fingerprint', models.CharField(max_length=40)), - ('file', models.FileField(upload_to='keys')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='keys', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("fingerprint", models.CharField(max_length=40)), + ("file", models.FileField(upload_to="keys")), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="keys", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/users/migrations/0004_auto_20171023_0143.py b/users/migrations/0004_auto_20171023_0143.py index 6290295..f895b37 100644 --- a/users/migrations/0004_auto_20171023_0143.py +++ b/users/migrations/0004_auto_20171023_0143.py @@ -8,48 +8,67 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('users', '0003_key'), + ("users", "0003_key"), ] operations = [ migrations.CreateModel( - name='Profile', + name="Profile", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('username', models.CharField(max_length=100)), - ('display_name', models.CharField(blank=True, max_length=100)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("username", models.CharField(max_length=100)), + ("display_name", models.CharField(blank=True, max_length=100)), ], options={ - 'ordering': ('site', 'username'), + "ordering": ("site", "username"), }, ), migrations.CreateModel( - name='Site', + name="Site", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, unique=True)), - ('icon', models.CharField(max_length=100)), - ('url', models.CharField(max_length=100)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ("icon", models.CharField(max_length=100)), + ("url", models.CharField(max_length=100)), ], options={ - 'ordering': ('name',), + "ordering": ("name",), }, ), migrations.AddField( - model_name='profile', - name='site', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Site'), + model_name="profile", + name="site", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="users.Site" + ), ), migrations.AddField( - model_name='profile', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="profile", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), migrations.AddField( - model_name='user', - name='profiles', - field=models.ManyToManyField(through='users.Profile', to='users.Site'), + model_name="user", + name="profiles", + field=models.ManyToManyField(through="users.Profile", to="users.Site"), ), ] diff --git a/users/migrations/0005_auto_20171023_0158.py b/users/migrations/0005_auto_20171023_0158.py index 4f95d75..edf267e 100644 --- a/users/migrations/0005_auto_20171023_0158.py +++ b/users/migrations/0005_auto_20171023_0158.py @@ -8,19 +8,22 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('users', '0004_auto_20171023_0143'), + ("users", "0004_auto_20171023_0143"), ] operations = [ migrations.RemoveField( - model_name='user', - name='profiles', + model_name="user", + name="profiles", ), migrations.AlterField( - model_name='profile', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profiles', to=settings.AUTH_USER_MODEL), + model_name="profile", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="profiles", + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/users/migrations/0006_auto_20171031_1336.py b/users/migrations/0006_auto_20171031_1336.py index 1cf8516..ef9e1cb 100644 --- a/users/migrations/0006_auto_20171031_1336.py +++ b/users/migrations/0006_auto_20171031_1336.py @@ -6,21 +6,20 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0005_auto_20171023_0158'), + ("users", "0005_auto_20171023_0158"), ] operations = [ migrations.RenameField( - model_name='site', - old_name='url', - new_name='url_template', + model_name="site", + old_name="url", + new_name="url_template", ), migrations.AddField( - model_name='site', - name='domain', - field=models.CharField(default='', max_length=100), + model_name="site", + name="domain", + field=models.CharField(default="", max_length=100), preserve_default=False, ), ] diff --git a/users/migrations/0007_auto_20171031_1347.py b/users/migrations/0007_auto_20171031_1347.py index a28bb94..8c132e8 100644 --- a/users/migrations/0007_auto_20171031_1347.py +++ b/users/migrations/0007_auto_20171031_1347.py @@ -6,15 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0006_auto_20171031_1336'), + ("users", "0006_auto_20171031_1336"), ] operations = [ migrations.AlterField( - model_name='site', - name='domain', + model_name="site", + name="domain", field=models.CharField(blank=True, max_length=100), ), ] diff --git a/users/migrations/0008_auto_20171031_1357.py b/users/migrations/0008_auto_20171031_1357.py index eadb686..392d09f 100644 --- a/users/migrations/0008_auto_20171031_1357.py +++ b/users/migrations/0008_auto_20171031_1357.py @@ -6,14 +6,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('users', '0007_auto_20171031_1347'), + ("users", "0007_auto_20171031_1347"), ] operations = [ migrations.AlterModelOptions( - name='site', - options={'ordering': ('domain',)}, + name="site", + options={"ordering": ("domain",)}, ), ] diff --git a/users/migrations/0009_user_xmpp.py b/users/migrations/0009_user_xmpp.py index b554f08..7c707f4 100644 --- a/users/migrations/0009_user_xmpp.py +++ b/users/migrations/0009_user_xmpp.py @@ -6,15 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0008_auto_20171031_1357'), + ("users", "0008_auto_20171031_1357"), ] operations = [ migrations.AddField( - model_name='user', - name='xmpp', + model_name="user", + name="xmpp", field=models.EmailField(blank=True, max_length=254), ), ] diff --git a/users/migrations/0010_auto_20171206_2211.py b/users/migrations/0010_auto_20171206_2211.py index 04a0819..6d982b9 100644 --- a/users/migrations/0010_auto_20171206_2211.py +++ b/users/migrations/0010_auto_20171206_2211.py @@ -6,14 +6,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('users', '0009_user_xmpp'), + ("users", "0009_user_xmpp"), ] operations = [ migrations.AlterModelOptions( - name='site', - options={'ordering': ('name',)}, + name="site", + options={"ordering": ("name",)}, ), ] diff --git a/users/migrations/0011_auto_20180124_1311.py b/users/migrations/0011_auto_20180124_1311.py index 20d91e3..65a1661 100644 --- a/users/migrations/0011_auto_20180124_1311.py +++ b/users/migrations/0011_auto_20180124_1311.py @@ -6,15 +6,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('users', '0010_auto_20171206_2211'), + ("users", "0010_auto_20171206_2211"), ] operations = [ migrations.AlterModelManagers( - name='user', - managers=[ - ], + name="user", + managers=[], ), ] diff --git a/users/migrations/0012_auto_20180129_1614.py b/users/migrations/0012_auto_20180129_1614.py index f0f4a91..364642f 100644 --- a/users/migrations/0012_auto_20180129_1614.py +++ b/users/migrations/0012_auto_20180129_1614.py @@ -7,16 +7,15 @@ import users.models class Migration(migrations.Migration): - dependencies = [ - ('users', '0011_auto_20180124_1311'), + ("users", "0011_auto_20180124_1311"), ] operations = [ migrations.AlterModelManagers( - name='user', + name="user", managers=[ - ('objects', users.models.UserManager()), + ("objects", users.models.UserManager()), ], ), ] diff --git a/users/migrations/0013_auto_20180323_1200.py b/users/migrations/0013_auto_20180323_1200.py index 953b271..e0f1576 100644 --- a/users/migrations/0013_auto_20180323_1200.py +++ b/users/migrations/0013_auto_20180323_1200.py @@ -5,20 +5,31 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0012_auto_20180129_1614'), + ("users", "0012_auto_20180129_1614"), ] operations = [ migrations.AddField( - model_name='user', - name='email_md5', - field=computed_property.fields.ComputedCharField(compute_from='calc_email_md5', default='', editable=False, max_length=32, unique=True), + model_name="user", + name="email_md5", + field=computed_property.fields.ComputedCharField( + compute_from="calc_email_md5", + default="", + editable=False, + max_length=32, + unique=True, + ), ), migrations.AddField( - model_name='user', - name='email_sha256', - field=computed_property.fields.ComputedCharField(compute_from='calc_email_sha256', default='', editable=False, max_length=64, unique=True), + model_name="user", + name="email_sha256", + field=computed_property.fields.ComputedCharField( + compute_from="calc_email_sha256", + default="", + editable=False, + max_length=64, + unique=True, + ), ), ] diff --git a/users/migrations/0014_auto_20180711_1248.py b/users/migrations/0014_auto_20180711_1248.py index 2975e47..4d846e5 100644 --- a/users/migrations/0014_auto_20180711_1248.py +++ b/users/migrations/0014_auto_20180711_1248.py @@ -6,58 +6,79 @@ import users.models class Migration(migrations.Migration): - dependencies = [ - ('users', '0013_auto_20180323_1200'), + ("users", "0013_auto_20180323_1200"), ] operations = [ migrations.AlterField( - model_name='profile', - name='display_name', + model_name="profile", + name="display_name", field=models.CharField( - blank=True, help_text='overrides the username for display - useful for sites that use ugly IDs', max_length=100), + blank=True, + help_text="overrides the username for display - useful for sites that use ugly IDs", + max_length=100, + ), ), migrations.AlterField( - model_name='profile', - name='username', + model_name="profile", + name="username", field=models.CharField( - help_text="the user's actual handle or ID on the remote site", max_length=100), + help_text="the user's actual handle or ID on the remote site", + max_length=100, + ), ), migrations.AlterField( - model_name='user', - name='avatar', + model_name="user", + name="avatar", field=models.ImageField( - help_text='an avatar or photo that represents this user', upload_to=users.models.avatar_path), + help_text="an avatar or photo that represents this user", + upload_to=users.models.avatar_path, + ), ), migrations.AlterField( - model_name='user', - name='email_md5', + model_name="user", + name="email_md5", field=computed_property.fields.ComputedCharField( - compute_from='calc_email_md5', editable=False, help_text="MD5 hash of the user's email, used for Libravatar", max_length=32, unique=True), + compute_from="calc_email_md5", + editable=False, + help_text="MD5 hash of the user's email, used for Libravatar", + max_length=32, + unique=True, + ), ), migrations.AlterField( - model_name='user', - name='email_sha256', + model_name="user", + name="email_sha256", field=computed_property.fields.ComputedCharField( - compute_from='calc_email_sha256', editable=False, help_text="SHA-256 hash of the user's email, used for Libravatar", max_length=64, unique=True), + compute_from="calc_email_sha256", + editable=False, + help_text="SHA-256 hash of the user's email, used for Libravatar", + max_length=64, + unique=True, + ), ), migrations.AlterField( - model_name='user', - name='last_name', + model_name="user", + name="last_name", field=models.CharField( - blank=True, max_length=150, verbose_name='last name'), + blank=True, max_length=150, verbose_name="last name" + ), ), migrations.AlterField( - model_name='user', - name='note', + model_name="user", + name="note", field=models.TextField( - blank=True, help_text='a bio or short description provided by the user'), + blank=True, help_text="a bio or short description provided by the user" + ), ), migrations.AlterField( - model_name='user', - name='xmpp', + model_name="user", + name="xmpp", field=models.EmailField( - blank=True, help_text='an XMPP address through which the user may be reached', max_length=254), + blank=True, + help_text="an XMPP address through which the user may be reached", + max_length=254, + ), ), ] diff --git a/users/migrations/0015_user_openid_sha256.py b/users/migrations/0015_user_openid_sha256.py index 3070f11..2cd48a0 100644 --- a/users/migrations/0015_user_openid_sha256.py +++ b/users/migrations/0015_user_openid_sha256.py @@ -5,17 +5,22 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('users', '0014_auto_20180711_1248'), + ("users", "0014_auto_20180711_1248"), ] operations = [ migrations.AddField( - model_name='user', - name='openid_sha256', - field=computed_property.fields.ComputedCharField(compute_from='calc_openid_sha256', default='', editable=False, - help_text="SHA-256 hash of the user's OpenID URL, used for Libravatar", max_length=64, unique=True), + model_name="user", + name="openid_sha256", + field=computed_property.fields.ComputedCharField( + compute_from="calc_openid_sha256", + default="", + editable=False, + help_text="SHA-256 hash of the user's OpenID URL, used for Libravatar", + max_length=64, + unique=True, + ), preserve_default=False, ), ] diff --git a/users/migrations/0016_alter_user_first_name.py b/users/migrations/0016_alter_user_first_name.py index f5375ae..f3eccfa 100644 --- a/users/migrations/0016_alter_user_first_name.py +++ b/users/migrations/0016_alter_user_first_name.py @@ -4,19 +4,16 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0015_user_openid_sha256'), + ("users", "0015_user_openid_sha256"), ] operations = [ migrations.AlterField( - model_name='user', - name='first_name', + model_name="user", + name="first_name", field=models.CharField( - blank=True, - max_length=150, - verbose_name='first name' + blank=True, max_length=150, verbose_name="first name" ), ), ] diff --git a/users/migrations/0017_rename_key_pgpkey.py b/users/migrations/0017_rename_key_pgpkey.py index 9d12dc6..2effa47 100644 --- a/users/migrations/0017_rename_key_pgpkey.py +++ b/users/migrations/0017_rename_key_pgpkey.py @@ -4,14 +4,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('users', '0016_alter_user_first_name'), + ("users", "0016_alter_user_first_name"), ] operations = [ migrations.RenameModel( - old_name='Key', - new_name='PgpKey', + old_name="Key", + new_name="PgpKey", ), ] diff --git a/users/models.py b/users/models.py index 274b82c..9ddac65 100644 --- a/users/models.py +++ b/users/models.py @@ -10,7 +10,7 @@ from lemoncurry import utils def avatar_path(instance, name): - return 'avatars/{id}/{name}'.format(id=instance.id, name=name) + return "avatars/{id}/{name}".format(id=instance.id, name=name) class Site(models.Model): @@ -19,7 +19,7 @@ class Site(models.Model): domain = models.CharField(max_length=100, blank=True) url_template = models.CharField(max_length=100) - def format(self, username=''): + def format(self, username=""): return self.url_template.format(domain=self.domain, username=username) @property @@ -30,12 +30,14 @@ class Site(models.Model): return self.name class Meta: - ordering = ('name',) + ordering = ("name",) class UserManager(DjangoUserManager): def get_queryset(self): - return super(UserManager, self).get_queryset().prefetch_related('keys', 'profiles') + return ( + super(UserManager, self).get_queryset().prefetch_related("keys", "profiles") + ) class User(ModelMeta, AbstractUser): @@ -44,52 +46,56 @@ class User(ModelMeta, AbstractUser): generated based on all their associated information and may author as many h-entries (:model:`entries.Entry`) as they wish. """ + objects = UserManager() avatar = models.ImageField( - upload_to=avatar_path, - help_text='an avatar or photo that represents this user' + upload_to=avatar_path, help_text="an avatar or photo that represents this user" ) note = models.TextField( - blank=True, - help_text='a bio or short description provided by the user' + blank=True, help_text="a bio or short description provided by the user" ) xmpp = models.EmailField( - blank=True, - help_text='an XMPP address through which the user may be reached' + blank=True, help_text="an XMPP address through which the user may be reached" ) # This is gonna need to change if I ever decide to add multiple-user support ;) - url = '/' + url = "/" email_md5 = ComputedCharField( - compute_from='calc_email_md5', max_length=32, unique=True, - help_text="MD5 hash of the user's email, used for Libravatar" + compute_from="calc_email_md5", + max_length=32, + unique=True, + help_text="MD5 hash of the user's email, used for Libravatar", ) email_sha256 = ComputedCharField( - compute_from='calc_email_sha256', max_length=64, unique=True, - help_text="SHA-256 hash of the user's email, used for Libravatar" + compute_from="calc_email_sha256", + max_length=64, + unique=True, + help_text="SHA-256 hash of the user's email, used for Libravatar", ) openid_sha256 = ComputedCharField( - compute_from='calc_openid_sha256', max_length=64, unique=True, - help_text="SHA-256 hash of the user's OpenID URL, used for Libravatar" + compute_from="calc_openid_sha256", + max_length=64, + unique=True, + help_text="SHA-256 hash of the user's OpenID URL, used for Libravatar", ) @property def calc_email_md5(self): - return md5(self.email.lower().encode('utf-8')).hexdigest() + return md5(self.email.lower().encode("utf-8")).hexdigest() @property def calc_email_sha256(self): - return sha256(self.email.lower().encode('utf-8')).hexdigest() + return sha256(self.email.lower().encode("utf-8")).hexdigest() @property def calc_openid_sha256(self): - return sha256(self.full_url.encode('utf-8')).hexdigest() + return sha256(self.full_url.encode("utf-8")).hexdigest() @property def name(self): - return '{0} {1}'.format(self.first_name, self.last_name) + return "{0} {1}".format(self.first_name, self.last_name) def get_absolute_url(self): return self.absolute_url @@ -100,7 +106,7 @@ class User(ModelMeta, AbstractUser): @property def full_url(self): - base = 'https://' + DjangoSite.objects.get_current().domain + base = "https://" + DjangoSite.objects.get_current().domain return urljoin(base, self.url) @property @@ -114,45 +120,45 @@ class User(ModelMeta, AbstractUser): @cached_property def facebook_id(self): for p in self.profiles.all(): - if p.site.name == 'Facebook': + if p.site.name == "Facebook": return p.username return None @cached_property def twitter_username(self): for p in self.profiles.all(): - if p.site.name == 'Twitter': - return '@' + p.username + if p.site.name == "Twitter": + return "@" + p.username return None @property def json_ld(self): - base = 'https://' + DjangoSite.objects.get_current().domain + base = "https://" + DjangoSite.objects.get_current().domain return { - '@context': 'http://schema.org', - '@type': 'Person', - '@id': self.full_url, - 'url': self.full_url, - 'name': self.name, - 'email': self.email, - 'image': urljoin(base, self.avatar.url), - 'givenName': self.first_name, - 'familyName': self.last_name, - 'sameAs': [profile.url for profile in self.profiles.all()] + "@context": "http://schema.org", + "@type": "Person", + "@id": self.full_url, + "url": self.full_url, + "name": self.name, + "email": self.email, + "image": urljoin(base, self.avatar.url), + "givenName": self.first_name, + "familyName": self.last_name, + "sameAs": [profile.url for profile in self.profiles.all()], } _metadata = { - 'image': 'avatar_url', - 'description': 'description', - 'og_type': 'profile', - 'og_profile_id': 'facebook_id', - 'twitter_creator': 'twitter_username', + "image": "avatar_url", + "description": "description", + "og_type": "profile", + "og_profile_id": "facebook_id", + "twitter_creator": "twitter_username", } class ProfileManager(models.Manager): def get_queryset(self): - return super(ProfileManager, self).get_queryset().select_related('site') + return super(ProfileManager, self).get_queryset().select_related("site") class Profile(models.Model): @@ -163,26 +169,22 @@ class Profile(models.Model): representative h-card. Additionally, :model:`entries.Syndication` is tracked by linking each syndication to a particular profile. """ + objects = ProfileManager() - user = models.ForeignKey( - User, - related_name='profiles', - on_delete=models.CASCADE - ) + user = models.ForeignKey(User, related_name="profiles", on_delete=models.CASCADE) site = models.ForeignKey(Site, on_delete=models.CASCADE) username = models.CharField( - max_length=100, - help_text="the user's actual handle or ID on the remote site" + max_length=100, help_text="the user's actual handle or ID on the remote site" ) display_name = models.CharField( max_length=100, blank=True, - help_text="overrides the username for display - useful for sites that use ugly IDs" + help_text="overrides the username for display - useful for sites that use ugly IDs", ) def __str__(self): if self.site.domain: - return self.name + '@' + self.site.domain + return self.name + "@" + self.site.domain return self.name @property @@ -194,7 +196,7 @@ class Profile(models.Model): return self.site.format(username=self.username) class Meta: - ordering = ('site', 'username') + ordering = ("site", "username") class PgpKey(models.Model): @@ -203,13 +205,10 @@ class PgpKey(models.Model): key will be added to the user's h-card with rel="pgpkey", a format compatible with IndieAuth.com. """ - user = models.ForeignKey( - User, - related_name='keys', - on_delete=models.CASCADE - ) + + user = models.ForeignKey(User, related_name="keys", on_delete=models.CASCADE) fingerprint = models.CharField(max_length=40) - file = models.FileField(upload_to='keys') + file = models.FileField(upload_to="keys") @property def key_id(self): @@ -231,4 +230,4 @@ class PgpKey(models.Model): same way GnuPG does. This can make reading the fingerprint a little friendlier. """ - return " ".join(self.fingerprint[i:i+4] for i in range(0, 40, 4)) + return " ".join(self.fingerprint[i : i + 4] for i in range(0, 40, 4)) diff --git a/users/urls.py b/users/urls.py index b790e3a..ae4ec6b 100644 --- a/users/urls.py +++ b/users/urls.py @@ -2,7 +2,5 @@ from django.urls import re_path from .views import libravatar -app_name = 'users' -urlpatterns = ( - re_path('^avatar/(?P[a-z0-9]+)$', libravatar, name='libravatar'), -) +app_name = "users" +urlpatterns = (re_path("^avatar/(?P[a-z0-9]+)$", libravatar, name="libravatar"),) diff --git a/users/views.py b/users/views.py index 2e700ee..cc4dffb 100644 --- a/users/views.py +++ b/users/views.py @@ -8,16 +8,16 @@ from .models import User def try_libravatar_org(hash, get): - url = 'https://seccdn.libravatar.org/avatar/' + hash + url = "https://seccdn.libravatar.org/avatar/" + hash if get: - url += '?' + get.urlencode() + url += "?" + get.urlencode() return HttpResponseRedirect(url) @cache_page(60 * 15) def libravatar(request, hash): g = request.GET - size = g.get('s', g.get('size', 80)) + size = g.get("s", g.get("size", 80)) try: size = int(size) except ValueError: @@ -30,7 +30,7 @@ def libravatar(request, hash): elif len(hash) == 64: where = Q(email_sha256=hash) | Q(openid_sha256=hash) else: - return utils.bad_req('hash must be either md5 or sha256') + return utils.bad_req("hash must be either md5 or sha256") # If the user doesn't exist or lacks an avatar, see if libravatar.org has # one for them - libravatar.org falls back to Gravatar when possible (only @@ -51,6 +51,6 @@ def libravatar(request, hash): im = im.crop((0, 0, natural_size, natural_size)) im = im.resize((size, size), resample=Image.HAMMING) - response = HttpResponse(content_type='image/'+image_type.lower()) + response = HttpResponse(content_type="image/" + image_type.lower()) im.save(response, image_type) return response diff --git a/webmention/apps.py b/webmention/apps.py index 4d88f81..a2fee31 100644 --- a/webmention/apps.py +++ b/webmention/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class WebmentionConfig(AppConfig): - name = 'webmention' + name = "webmention" diff --git a/webmention/migrations/0001_initial.py b/webmention/migrations/0001_initial.py index ce76889..0c3d9f6 100644 --- a/webmention/migrations/0001_initial.py +++ b/webmention/migrations/0001_initial.py @@ -9,31 +9,71 @@ import model_utils.fields class Migration(migrations.Migration): - initial = True dependencies = [ - ('entries', '0011_auto_20171120_1108'), + ("entries", "0011_auto_20171120_1108"), ] operations = [ migrations.CreateModel( - name='Webmention', + name="Webmention", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('source', models.CharField(max_length=255)), - ('target', models.CharField(max_length=255)), - ('state', models.CharField(choices=[('p', 'pending'), ('v', 'valid'), ('i', 'invalid'), ('d', 'deleted')], default='p', max_length=1)), - ('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mentions', to='entries.Entry')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ("source", models.CharField(max_length=255)), + ("target", models.CharField(max_length=255)), + ( + "state", + models.CharField( + choices=[ + ("p", "pending"), + ("v", "valid"), + ("i", "invalid"), + ("d", "deleted"), + ], + default="p", + max_length=1, + ), + ), + ( + "entry", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="mentions", + to="entries.Entry", + ), + ), ], options={ - 'default_related_name': 'mentions', + "default_related_name": "mentions", }, ), migrations.AlterUniqueTogether( - name='webmention', - unique_together=set([('source', 'target')]), + name="webmention", + unique_together=set([("source", "target")]), ), ] diff --git a/webmention/models.py b/webmention/models.py index 47690ab..beb65b0 100644 --- a/webmention/models.py +++ b/webmention/models.py @@ -4,15 +4,15 @@ from model_utils.models import TimeStampedModel class State: - PENDING = 'p' - VALID = 'v' - INVALID = 'i' - DELETED = 'd' + PENDING = "p" + VALID = "v" + INVALID = "i" + DELETED = "d" CHOICES = ( - (PENDING, 'pending'), - (VALID, 'valid'), - (INVALID, 'invalid'), - (DELETED, 'deleted'), + (PENDING, "pending"), + (VALID, "valid"), + (INVALID, "invalid"), + (DELETED, "deleted"), ) @@ -20,12 +20,8 @@ class Webmention(TimeStampedModel): entry = models.ForeignKey(Entry, on_delete=models.CASCADE) source = models.CharField(max_length=255) target = models.CharField(max_length=255) - state = models.CharField( - choices=State.CHOICES, - default=State.PENDING, - max_length=1 - ) + state = models.CharField(choices=State.CHOICES, default=State.PENDING, max_length=1) class Meta: - default_related_name = 'mentions' - unique_together = ('source', 'target') + default_related_name = "mentions" + unique_together = ("source", "target") diff --git a/webmention/urls.py b/webmention/urls.py index ac307ca..27c01a5 100644 --- a/webmention/urls.py +++ b/webmention/urls.py @@ -1,8 +1,8 @@ from django.urls import path from . import views -app_name = 'webmention' +app_name = "webmention" urlpatterns = ( - path('s', views.accept, name='accept'), - path('s/', views.status, name='status') + path("s", views.accept, name="accept"), + path("s/", views.status, name="status"), ) diff --git a/webmention/views.py b/webmention/views.py index 5abd688..0207b4c 100644 --- a/webmention/views.py +++ b/webmention/views.py @@ -13,36 +13,36 @@ from .models import State, Webmention @csrf_exempt @require_POST def accept(request): - if 'source' not in request.POST: - return bad_req('missing source url') - source_url = request.POST['source'] + if "source" not in request.POST: + return bad_req("missing source url") + source_url = request.POST["source"] - if 'target' not in request.POST: - return bad_req('missing target url') - target_url = request.POST['target'] + if "target" not in request.POST: + return bad_req("missing target url") + target_url = request.POST["target"] source = urlparse(source_url) target = urlparse(target_url) - if source.scheme not in ('http', 'https'): - return bad_req('unsupported source scheme') - if target.scheme not in ('http', 'https'): - return bad_req('unsupported target scheme') + if source.scheme not in ("http", "https"): + return bad_req("unsupported source scheme") + if target.scheme not in ("http", "https"): + return bad_req("unsupported target scheme") if target.netloc != request.site.domain: - return bad_req('target not on this site') - origin = 'https://' + target.netloc + return bad_req("target not on this site") + origin = "https://" + target.netloc try: match = resolve(target.path) except Resolver404: - return bad_req('target not found') + return bad_req("target not found") - if match.view_name != 'entries:entry': - return bad_req('target does not accept webmentions') + if match.view_name != "entries:entry": + return bad_req("target does not accept webmentions") try: - entry = Entry.objects.get(pk=match.kwargs['id']) + entry = Entry.objects.get(pk=match.kwargs["id"]) except Entry.DoesNotExist: - return bad_req('target not found') + return bad_req("target not found") try: mention = Webmention.objects.get(source=source_url, target=target_url) @@ -54,10 +54,10 @@ def accept(request): mention.entry = entry mention.state = State.PENDING mention.save() - status_url = reverse('webmention:status', kwargs={'id': mention.id}) + status_url = reverse("webmention:status", kwargs={"id": mention.id}) res = HttpResponse(status=201) - res['Location'] = urljoin(origin, status_url) + res["Location"] = urljoin(origin, status_url) return res diff --git a/wellknowns/apps.py b/wellknowns/apps.py index 08ba250..949cc76 100644 --- a/wellknowns/apps.py +++ b/wellknowns/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class WellKnownsConfig(AppConfig): - name = 'wellknowns' + name = "wellknowns" diff --git a/wellknowns/favicons.py b/wellknowns/favicons.py index 27f7a5f..aa0a840 100644 --- a/wellknowns/favicons.py +++ b/wellknowns/favicons.py @@ -3,7 +3,7 @@ from django.core.files.storage import default_storage class Favicon: - def __init__(self, size, rel='icon', mime='image/png'): + def __init__(self, size, rel="icon", mime="image/png"): self.rel = rel self.mime = mime if not isinstance(size, tuple): @@ -12,18 +12,18 @@ class Favicon: @property def url(self): - return default_storage.url('favicon/' + self.filename) + return default_storage.url("favicon/" + self.filename) @property def filename(self): - return 'favicon-{0}.png'.format(*self.size) + return "favicon-{0}.png".format(*self.size) @property def sizes(self): - return 'x'.join(str(s) for s in self.size) + return "x".join(str(s) for s in self.size) -tile_sizes = {'small': 128, 'medium': 270, 'wide': (558, 270), 'large': 558} +tile_sizes = {"small": 128, "medium": 270, "wide": (558, 270), "large": 558} class Tile(Favicon): @@ -33,13 +33,17 @@ class Tile(Favicon): @property def filename(self): - return '{0}tile.png'.format(self.size_name) + return "{0}tile.png".format(self.size_name) sizes = (32, 57, 76, 96, 120, 128, 144, 180, 195, 228) -icons = tuple(chain( - (Favicon(s) for s in sizes), - (Tile(s) for s in tile_sizes.keys()), - (Favicon(152, rel='apple-touch-icon-precomposed'), - Favicon(196, rel='shortcut icon')) -)) +icons = tuple( + chain( + (Favicon(s) for s in sizes), + (Tile(s) for s in tile_sizes.keys()), + ( + Favicon(152, rel="apple-touch-icon-precomposed"), + Favicon(196, rel="shortcut icon"), + ), + ) +) diff --git a/wellknowns/tests/views/host_meta.py b/wellknowns/tests/views/host_meta.py index e8e332f..2a38522 100644 --- a/wellknowns/tests/views/host_meta.py +++ b/wellknowns/tests/views/host_meta.py @@ -5,22 +5,22 @@ import pytest @pytest.mark.django_db def test_host_meta_json(client): - res = client.get('/.well-known/host-meta.json') + res = client.get("/.well-known/host-meta.json") assert res.status_code == 200 - assert res['Content-Type'] == 'application/json' + assert res["Content-Type"] == "application/json" meta = json.loads(res.content) - assert meta.keys() == {'links', 'subject'} - assert meta['subject'] == 'https://example.com' - assert len(meta['links']) == 13 + assert meta.keys() == {"links", "subject"} + assert meta["subject"] == "https://example.com" + assert len(meta["links"]) == 13 @pytest.mark.django_db def test_host_meta_xml(client): - res = client.get('/.well-known/host-meta') + res = client.get("/.well-known/host-meta") assert res.status_code == 200 - assert res['Content-Type'] == 'application/xrd+xml' + assert res["Content-Type"] == "application/xrd+xml" root = etree.XML(res.content) - ns = '{http://docs.oasis-open.org/ns/xri/xrd-1.0}' - assert root.tag == (ns + 'XRD') - assert root.findtext(ns + 'Subject') == 'https://example.com' - assert len(root.findall(ns + 'Link')) == 13 + ns = "{http://docs.oasis-open.org/ns/xri/xrd-1.0}" + assert root.tag == (ns + "XRD") + assert root.findtext(ns + "Subject") == "https://example.com" + assert len(root.findall(ns + "Link")) == 13 diff --git a/wellknowns/tests/views/static.py b/wellknowns/tests/views/static.py index eb7beb2..9f9dcac 100644 --- a/wellknowns/tests/views/static.py +++ b/wellknowns/tests/views/static.py @@ -2,12 +2,12 @@ from ...views import static def test_redirect_to_static(rf): - res = static.redirect_to_static('abcd')(rf.get('/')) + res = static.redirect_to_static("abcd")(rf.get("/")) assert res.status_code == 302 - assert res.url == '/static/wellknowns/abcd' + assert res.url == "/static/wellknowns/abcd" def test_keybase(rf): - res = static.keybase(rf.get('/.well-knowns/keybase.txt')) + res = static.keybase(rf.get("/.well-knowns/keybase.txt")) assert res.status_code == 302 - assert res.url == '/static/wellknowns/keybase.txt' + assert res.url == "/static/wellknowns/keybase.txt" diff --git a/wellknowns/urls.py b/wellknowns/urls.py index 2b1b99f..35957c0 100644 --- a/wellknowns/urls.py +++ b/wellknowns/urls.py @@ -2,11 +2,11 @@ from django.urls import path from . import views -app_name = 'wellknowns' +app_name = "wellknowns" urlpatterns = [ - path('keybase.txt', views.keybase, name='keybase'), - path('host-meta', views.host_meta_xml, name='host-meta'), - path('host-meta.json', views.host_meta_json, name='host-meta.json'), - path('manifest.json', views.manifest, name='manifest'), - path('webfinger', views.webfinger, name='webfinger'), + path("keybase.txt", views.keybase, name="keybase"), + path("host-meta", views.host_meta_xml, name="host-meta"), + path("host-meta.json", views.host_meta_json, name="host-meta.json"), + path("manifest.json", views.manifest, name="manifest"), + path("webfinger", views.webfinger, name="webfinger"), ] diff --git a/wellknowns/views/host_meta.py b/wellknowns/views/host_meta.py index 5fc29f2..6d9050e 100644 --- a/wellknowns/views/host_meta.py +++ b/wellknowns/views/host_meta.py @@ -9,58 +9,55 @@ from xrd import XRD, Attribute, Element, Link def add_links(request, dest): base = origin(request) pkg = load_package_json() - webfinger = reverse('wellknowns:webfinger') + '?resource={uri}' - license = 'https://creativecommons.org/licenses/by-sa/4.0/' + webfinger = reverse("wellknowns:webfinger") + "?resource={uri}" + license = "https://creativecommons.org/licenses/by-sa/4.0/" links = ( Link( - href=urljoin(base, reverse('entries:atom')), - rel='alternate', type_='application/atom+xml', + href=urljoin(base, reverse("entries:atom")), + rel="alternate", + type_="application/atom+xml", ), Link( - href=urljoin(base, reverse('entries:rss')), - rel='alternate', type_='application/rss+xml', + href=urljoin(base, reverse("entries:rss")), + rel="alternate", + type_="application/rss+xml", ), Link( - href=urljoin(base, reverse('lemonauth:indie')), - rel='authorization_endpoint' + href=urljoin(base, reverse("lemonauth:indie")), rel="authorization_endpoint" ), - Link(href=pkg['repository'], type_='text/html', rel='code-repository'), - Link(href=settings.PUSH_HUB, rel='hub'), - Link(href=license, type_='text/html', rel='license'), - Link(href=license+'rdf', type_='application/rdf+xml', rel='license'), + Link(href=pkg["repository"], type_="text/html", rel="code-repository"), + Link(href=settings.PUSH_HUB, rel="hub"), + Link(href=license, type_="text/html", rel="license"), + Link(href=license + "rdf", type_="application/rdf+xml", rel="license"), Link( template=urljoin(base, webfinger), - type_='application/json', rel='lrdd', + type_="application/json", + rel="lrdd", ), Link( - href=urljoin(base, reverse('wellknowns:manifest')), - rel='manifest', type_='application/json', + href=urljoin(base, reverse("wellknowns:manifest")), + rel="manifest", + type_="application/json", ), - Link( - href=urljoin(base, reverse('micropub:micropub')), - rel='micropub' - ), - Link( - href=urljoin(base, reverse('lemonauth:token')), - rel='token_endpoint' - ), - Link(href='https://openid.indieauth.com/openid', rel='openid.server'), - Link(href=base, rel='openid.delegate'), + Link(href=urljoin(base, reverse("micropub:micropub")), rel="micropub"), + Link(href=urljoin(base, reverse("lemonauth:token")), rel="token_endpoint"), + Link(href="https://openid.indieauth.com/openid", rel="openid.server"), + Link(href=base, rel="openid.delegate"), ) dest.extend(links) def host_meta(request): - h = XRD(subject='https://' + request.site.domain) + h = XRD(subject="https://" + request.site.domain) add_links(request, h.links) return h def host_meta_xml(request): return HttpResponse( - host_meta(request).to_xml().toprettyxml(indent=' ', encoding='utf-8'), - content_type='application/xrd+xml', + host_meta(request).to_xml().toprettyxml(indent=" ", encoding="utf-8"), + content_type="application/xrd+xml", ) @@ -71,13 +68,15 @@ def host_meta_json(request): links = [] for l in meta.links: link = { - 'rel': l.rel, 'type': l.type, - 'href': l.href, 'template': l.template, + "rel": l.rel, + "type": l.type, + "href": l.href, + "template": l.template, } for k in list(link.keys()): if not link[k]: del link[k] links.append(link) - meta = {'links': links, 'subject': meta.subject} + meta = {"links": links, "subject": meta.subject} return JsonResponse(meta) diff --git a/wellknowns/views/manifest.py b/wellknowns/views/manifest.py index 2105f56..ae62175 100644 --- a/wellknowns/views/manifest.py +++ b/wellknowns/views/manifest.py @@ -8,23 +8,24 @@ from textwrap import shorten def manifest_icons(base): - return [{'src': i.url, 'type': i.mime, 'sizes': i.sizes} for i in sorted(icons, key=lambda i: i.size)] + return [ + {"src": i.url, "type": i.mime, "sizes": i.sizes} + for i in sorted(icons, key=lambda i: i.size) + ] def manifest(request): base = utils.origin(request) - start_url = reverse('home:index') + '?utm_source=homescreen' + start_url = reverse("home:index") + "?utm_source=homescreen" app = { - 'name': request.site.name, - 'short_name': shorten(request.site.name, width=20, placeholder=''), - 'icons': manifest_icons(base), - - 'display': 'browser', - 'start_url': urljoin(base, start_url), - - 'background_color': color(0), - 'theme_color': color(3), + "name": request.site.name, + "short_name": shorten(request.site.name, width=20, placeholder=""), + "icons": manifest_icons(base), + "display": "browser", + "start_url": urljoin(base, start_url), + "background_color": color(0), + "theme_color": color(3), } - return JsonResponse(app, content_type='application/manifest+json') + return JsonResponse(app, content_type="application/manifest+json") diff --git a/wellknowns/views/static.py b/wellknowns/views/static.py index e09e3c5..3bd8c03 100644 --- a/wellknowns/views/static.py +++ b/wellknowns/views/static.py @@ -3,7 +3,7 @@ from django.templatetags.static import static def redirect_to_static(file): - return RedirectView.as_view(url=static('wellknowns/' + file)) + return RedirectView.as_view(url=static("wellknowns/" + file)) -keybase = redirect_to_static('keybase.txt') +keybase = redirect_to_static("keybase.txt") diff --git a/wellknowns/views/webfinger.py b/wellknowns/views/webfinger.py index 05684a9..ef42687 100644 --- a/wellknowns/views/webfinger.py +++ b/wellknowns/views/webfinger.py @@ -3,9 +3,9 @@ from urllib.parse import urlencode, urlparse from users.models import User -AVATAR = 'http://webfinger.net/rel/avatar' -PROFILE_PAGE = 'http://webfinger.net/rel/profile-page' -BRIDGY_FED = 'https://fed.brid.gy/.well-known/webfinger' +AVATAR = "http://webfinger.net/rel/avatar" +PROFILE_PAGE = "http://webfinger.net/rel/profile-page" +BRIDGY_FED = "https://fed.brid.gy/.well-known/webfinger" def https_resource_matching(resource): @@ -15,10 +15,10 @@ def https_resource_matching(resource): resource, if a user with matching email or XMPP address exists locally. Will throw `User.DoesNotExist` if no such user exists. """ - if resource.scheme == 'mailto': - query = {'email': resource.path} + if resource.scheme == "mailto": + query = {"email": resource.path} else: - query = {'xmpp': resource.path} + query = {"xmpp": resource.path} return User.objects.get(**query).absolute_url @@ -41,19 +41,19 @@ def webfinger(request): original resource will be preserved in the redirect - and likely fail to find anything at Bridgy's end either. """ - if 'resource' not in request.GET: - return HttpResponseBadRequest('resource parameter missing') - resource = request.GET['resource'] + if "resource" not in request.GET: + return HttpResponseBadRequest("resource parameter missing") + resource = request.GET["resource"] try: res = urlparse(resource) except ValueError: - return HttpResponseBadRequest('resource parameter malformed') + return HttpResponseBadRequest("resource parameter malformed") - if res.scheme in ('mailto', 'xmpp'): + if res.scheme in ("mailto", "xmpp"): try: resource = https_resource_matching(res) except User.DoesNotExist: pass - query = urlencode({'resource': resource}) - return HttpResponseRedirect(BRIDGY_FED + '?' + query) + query = urlencode({"resource": resource}) + return HttpResponseRedirect(BRIDGY_FED + "?" + query)