Run Black over the whole codebase

This commit is contained in:
Danielle McLean 2023-08-10 16:52:37 +10:00
parent cd990e4e2f
commit 2e7d12b3e6
Signed by untrusted user: 00dani
GPG key ID: 52C059C3B22A753E
109 changed files with 1539 additions and 1209 deletions

View file

@ -8,12 +8,10 @@ class SyndicationInline(admin.TabularInline):
class EntryAdmin(admin.ModelAdmin): class EntryAdmin(admin.ModelAdmin):
date_hierarchy = 'created' date_hierarchy = "created"
list_display = ('title', 'id', 'kind', 'created') list_display = ("title", "id", "kind", "created")
list_filter = ('kind',) list_filter = ("kind",)
inlines = ( inlines = (SyndicationInline,)
SyndicationInline,
)
admin.site.register(Cat) admin.site.register(Cat)

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class EntriesConfig(AppConfig): class EntriesConfig(AppConfig):
name = 'entries' name = "entries"

View file

@ -11,24 +11,24 @@ from .models import Entry
def from_url(url: str) -> Entry: def from_url(url: str) -> Entry:
domain = Site.objects.get_current().domain domain = Site.objects.get_current().domain
if not url: if not url:
raise error.bad_req('url parameter required') raise error.bad_req("url parameter required")
if '//' not in url: if "//" not in url:
url = '//' + url url = "//" + url
parts = urlparse(url, scheme='https') parts = urlparse(url, scheme="https")
if parts.scheme not in ('http', 'https') or parts.netloc != domain: 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 does not point to this site")
try: try:
match = resolve(parts.path) match = resolve(parts.path)
except Resolver404: 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': if match.view_name != "entries:entry":
raise error.bad_req('url does not point to an entry on this site') raise error.bad_req("url does not point to an entry on this site")
try: try:
entry = Entry.objects.get(pk=match.kwargs['id']) entry = Entry.objects.get(pk=match.kwargs["id"])
except Entry.DoesNotExist: 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 return entry

View file

@ -7,16 +7,19 @@ from ronkyuu import webmention
@job @job
def ping_hub(*urls): def ping_hub(*urls):
for url in urls: for url in urls:
requests.post(settings.PUSH_HUB, data={ requests.post(
'hub.mode': 'publish', settings.PUSH_HUB,
'hub.url': url, data={
}) "hub.mode": "publish",
"hub.url": url,
},
)
@job @job
def send_mentions(source, targets=None): def send_mentions(source, targets=None):
if targets is None: if targets is None:
targets = webmention.findMentions(source)['refs'] targets = webmention.findMentions(source)["refs"]
for target in targets: for target in targets:
status, endpoint = webmention.discoverEndpoint(target) status, endpoint = webmention.discoverEndpoint(target)
if endpoint is not None and status == 200: if endpoint is not None and status == 200:

View file

@ -14,62 +14,62 @@ class Entry:
return self.index_page() return self.index_page()
def index_page(self, page=0): def index_page(self, page=0):
kwargs = {'kind': self} kwargs = {"kind": self}
if page > 1: if page > 1:
kwargs['page'] = page kwargs["page"] = page
return reverse('entries:index', kwargs=kwargs) return reverse("entries:index", kwargs=kwargs)
@property @property
def entry(self): def entry(self):
return self.plural + '_entry' return self.plural + "_entry"
@property @property
def atom(self): def atom(self):
return reverse('entries:atom_by_kind', kwargs={'kind': self}) return reverse("entries:atom_by_kind", kwargs={"kind": self})
@property @property
def rss(self): def rss(self):
return reverse('entries:rss_by_kind', kwargs={'kind': self}) return reverse("entries:rss_by_kind", kwargs={"kind": self})
Note = Entry( Note = Entry(
id='note', id="note",
icon='fas fa-paper-plane', icon="fas fa-paper-plane",
plural='notes', plural="notes",
) )
Article = Entry( Article = Entry(
id='article', id="article",
icon='fas fa-file-alt', icon="fas fa-file-alt",
plural='articles', plural="articles",
slug=True, slug=True,
) )
Photo = Entry( Photo = Entry(
id='photo', id="photo",
icon='fas fa-camera', icon="fas fa-camera",
plural='photos', plural="photos",
) )
Reply = Entry( Reply = Entry(
id='reply', id="reply",
icon='fas fa-comment', icon="fas fa-comment",
plural='replies', plural="replies",
on_home=False, on_home=False,
) )
Like = Entry( Like = Entry(
id='like', id="like",
icon='fas fa-heart', icon="fas fa-heart",
plural='likes', plural="likes",
on_home=False, on_home=False,
) )
Repost = Entry( Repost = Entry(
id='repost', id="repost",
icon='fas fa-retweet', icon="fas fa-retweet",
plural='reposts', plural="reposts",
) )
all = (Note, Article, Photo) all = (Note, Article, Photo)
@ -79,7 +79,7 @@ from_plural = {k.plural: k for k in all}
class EntryKindConverter: class EntryKindConverter:
regex = '|'.join(k.plural for k in all) regex = "|".join(k.plural for k in all)
def to_python(self, plural): def to_python(self, plural):
return from_plural[plural] return from_plural[plural]

View file

@ -8,7 +8,6 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
@ -17,20 +16,41 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Entry', name="Entry",
fields=[ 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)), "id",
('name', models.CharField(blank=True, max_length=100)), models.AutoField(
('summary', models.TextField(blank=True)), auto_created=True,
('content', models.TextField()), primary_key=True,
('published', models.DateTimeField()), serialize=False,
('updated', models.DateTimeField()), verbose_name="ID",
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ),
),
(
"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={ options={
'verbose_name_plural': 'entries', "verbose_name_plural": "entries",
'ordering': ['-published'], "ordering": ["-published"],
}, },
), ),
] ]

View file

@ -7,23 +7,42 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0005_auto_20171023_0158'), ("users", "0005_auto_20171023_0158"),
('entries', '0001_initial'), ("entries", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Syndication', name="Syndication",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('url', models.CharField(max_length=255)), "id",
('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='syndications', to='entries.Entry')), models.AutoField(
('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Profile')), 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={ options={
'ordering': ['profile'], "ordering": ["profile"],
}, },
), ),
] ]

View file

@ -6,14 +6,13 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('entries', '0002_syndication'), ("entries", "0002_syndication"),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(
model_name='entry', model_name="entry",
name='summary', name="summary",
), ),
] ]

View file

@ -8,20 +8,28 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('entries', '0003_remove_entry_summary'), ("entries", "0003_remove_entry_summary"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='entry', model_name="entry",
name='author', name="author",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to=settings.AUTH_USER_MODEL), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="entries",
to=settings.AUTH_USER_MODEL,
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='entry', model_name="entry",
name='kind', name="kind",
field=models.CharField(choices=[('note', 'note'), ('article', 'article')], db_index=True, default='note', max_length=30), field=models.CharField(
choices=[("note", "note"), ("article", "article")],
db_index=True,
default="note",
max_length=30,
),
), ),
] ]

View file

@ -6,20 +6,24 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('entries', '0004_auto_20171027_0846'), ("entries", "0004_auto_20171027_0846"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='entry', model_name="entry",
name='photo', name="photo",
field=models.ImageField(blank=True, upload_to=''), field=models.ImageField(blank=True, upload_to=""),
), ),
migrations.AlterField( migrations.AlterField(
model_name='entry', model_name="entry",
name='kind', name="kind",
field=models.CharField(choices=[('note', 'note'), ('article', 'article'), ('photo', 'photo')], db_index=True, default='note', max_length=30), field=models.CharField(
choices=[("note", "note"), ("article", "article"), ("photo", "photo")],
db_index=True,
default="note",
max_length=30,
),
), ),
] ]

View file

@ -8,34 +8,41 @@ import model_utils.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('entries', '0005_auto_20171027_1557'), ("entries", "0005_auto_20171027_1557"),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='entry', name="entry",
options={'ordering': ['-created'], 'verbose_name_plural': 'entries'}, options={"ordering": ["-created"], "verbose_name_plural": "entries"},
), ),
migrations.RenameField( migrations.RenameField(
model_name='entry', model_name="entry",
old_name='published', old_name="published",
new_name='created', new_name="created",
), ),
migrations.RenameField( migrations.RenameField(
model_name='entry', model_name="entry",
old_name='updated', old_name="updated",
new_name='modified', new_name="modified",
), ),
migrations.AlterField( migrations.AlterField(
model_name='entry', model_name="entry",
name='created', name="created",
field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), field=model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="created",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='entry', model_name="entry",
name='modified', name="modified",
field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), field=model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="modified",
),
), ),
] ]

View file

@ -6,20 +6,31 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('entries', '0006_auto_20171102_1200'), ("entries", "0006_auto_20171102_1200"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='entry', model_name="entry",
name='cite', name="cite",
field=models.CharField(blank=True, max_length=255), field=models.CharField(blank=True, max_length=255),
), ),
migrations.AlterField( migrations.AlterField(
model_name='entry', model_name="entry",
name='kind', 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), field=models.CharField(
choices=[
("note", "note"),
("article", "article"),
("photo", "photo"),
("reply", "reply"),
("like", "like"),
("repost", "repost"),
],
db_index=True,
default="note",
max_length=30,
),
), ),
] ]

View file

@ -6,25 +6,24 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('entries', '0007_auto_20171113_0841'), ("entries", "0007_auto_20171113_0841"),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(
model_name='entry', model_name="entry",
old_name='cite', old_name="cite",
new_name='in_reply_to', new_name="in_reply_to",
), ),
migrations.AddField( migrations.AddField(
model_name='entry', model_name="entry",
name='like_of', name="like_of",
field=models.CharField(blank=True, max_length=255), field=models.CharField(blank=True, max_length=255),
), ),
migrations.AddField( migrations.AddField(
model_name='entry', model_name="entry",
name='repost_of', name="repost_of",
field=models.CharField(blank=True, max_length=255), field=models.CharField(blank=True, max_length=255),
), ),
] ]

View file

@ -6,21 +6,28 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('entries', '0008_auto_20171116_2116'), ("entries", "0008_auto_20171116_2116"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Tag', name="Tag",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=255, unique=True)), "id",
('slug', models.CharField(max_length=255, unique=True)), 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={ options={
'ordering': ('name',), "ordering": ("name",),
}, },
), ),
] ]

View file

@ -6,15 +6,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('entries', '0009_tag'), ("entries", "0009_tag"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='entry', model_name="entry",
name='tags', name="tags",
field=models.ManyToManyField(related_name='entries', to='entries.Tag'), field=models.ManyToManyField(related_name="entries", to="entries.Tag"),
), ),
] ]

View file

@ -9,17 +9,17 @@ class Migration(migrations.Migration):
atomic = False atomic = False
dependencies = [ dependencies = [
('entries', '0010_entry_tags'), ("entries", "0010_entry_tags"),
] ]
operations = [ operations = [
migrations.RenameModel( migrations.RenameModel(
old_name='Tag', old_name="Tag",
new_name='Cat', new_name="Cat",
), ),
migrations.RenameField( migrations.RenameField(
model_name='entry', model_name="entry",
old_name='tags', old_name="tags",
new_name='cats', new_name="cats",
), ),
] ]

View file

@ -5,25 +5,25 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('entries', '0011_auto_20171120_1108'), ("entries", "0011_auto_20171120_1108"),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='syndication', name="syndication",
options={'ordering': ['domain']}, options={"ordering": ["domain"]},
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='syndication', model_name="syndication",
name='profile', name="profile",
), ),
migrations.AddField( migrations.AddField(
model_name='syndication', model_name="syndication",
name='domain', name="domain",
field=computed_property.fields.ComputedCharField( 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, preserve_default=False,
), ),
] ]

View file

@ -4,24 +4,19 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('entries', '0012_auto_20180628_2044'), ("entries", "0012_auto_20180628_2044"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='entry', model_name="entry",
name='kind', name="kind",
field=models.CharField( field=models.CharField(
choices=[ choices=[("note", "note"), ("article", "article"), ("photo", "photo")],
('note', 'note'),
('article', 'article'),
('photo', 'photo')
],
db_index=True, db_index=True,
default='note', default="note",
max_length=30 max_length=30,
), ),
), ),
] ]

View file

@ -17,6 +17,7 @@ from users.models import Site
from . import kinds from . import kinds
from lemoncurry import requests, utils from lemoncurry import requests, utils
ENTRY_KINDS = [(k.id, k.id) for k in kinds.all] 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) slug = models.CharField(max_length=255, unique=True)
def __str__(self): def __str__(self):
return '#' + self.name return "#" + self.name
@property @property
def url(self): def url(self):
return reverse('entries:cat', args=(self.slug,)) return reverse("entries:cat", args=(self.slug,))
class Meta: class Meta:
ordering = ('name',) ordering = ("name",)
class EntryManager(models.Manager): class EntryManager(models.Manager):
def get_queryset(self): def get_queryset(self):
qs = super(EntryManager, self).get_queryset() qs = super(EntryManager, self).get_queryset()
return (qs return qs.select_related("author").prefetch_related("cats", "syndications")
.select_related('author')
.prefetch_related('cats', 'syndications'))
class Entry(ModelMeta, TimeStampedModel): class Entry(ModelMeta, TimeStampedModel):
objects = EntryManager() objects = EntryManager()
kind = models.CharField( kind = models.CharField(
max_length=30, max_length=30, choices=ENTRY_KINDS, db_index=True, default=ENTRY_KINDS[0][0]
choices=ENTRY_KINDS,
db_index=True,
default=ENTRY_KINDS[0][0]
) )
name = models.CharField(max_length=100, blank=True) name = models.CharField(max_length=100, blank=True)
photo = models.ImageField(blank=True) photo = models.ImageField(blank=True)
content = models.TextField() 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) in_reply_to = models.CharField(max_length=255, blank=True)
like_of = 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( author = models.ForeignKey(
get_user_model(), get_user_model(),
related_name='entries', related_name="entries",
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
@ -79,10 +75,7 @@ class Entry(ModelMeta, TimeStampedModel):
def reply_context(self): def reply_context(self):
if not self.in_reply_to: if not self.in_reply_to:
return None return None
return interpret( return interpret(requests.mf2(self.in_reply_to).to_dict(), self.in_reply_to)
requests.mf2(self.in_reply_to).to_dict(),
self.in_reply_to
)
@property @property
def published(self): def published(self):
@ -93,35 +86,29 @@ class Entry(ModelMeta, TimeStampedModel):
return self.modified return self.modified
_metadata = { _metadata = {
'description': 'excerpt', "description": "excerpt",
'image': 'image_url', "image": "image_url",
'twitter_creator': 'twitter_creator', "twitter_creator": "twitter_creator",
'og_profile_id': 'og_profile_id', "og_profile_id": "og_profile_id",
} }
@property @property
def title(self): def title(self):
if self.name: if self.name:
return self.name return self.name
return shorten( return shorten(utils.to_plain(self.paragraphs[0]), width=100, placeholder="")
utils.to_plain(self.paragraphs[0]),
width=100,
placeholder=''
)
@property @property
def excerpt(self): def excerpt(self):
try: try:
return utils.to_plain(self.paragraphs[0 if self.name else 1]) return utils.to_plain(self.paragraphs[0 if self.name else 1])
except IndexError: except IndexError:
return ' ' return " "
@property @property
def paragraphs(self): def paragraphs(self):
lines = self.content.splitlines() lines = self.content.splitlines()
return [ return ["\n".join(para) for k, para in groupby(lines, key=bool) if k]
"\n".join(para) for k, para in groupby(lines, key=bool) if k
]
@property @property
def twitter_creator(self): def twitter_creator(self):
@ -136,31 +123,31 @@ class Entry(ModelMeta, TimeStampedModel):
return self.photo.url if self.photo else self.author.avatar_url return self.photo.url if self.photo else self.author.avatar_url
def __str__(self): 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): def get_absolute_url(self):
return self.absolute_url return self.absolute_url
@property @property
def absolute_url(self): def absolute_url(self):
base = 'https://' + DjangoSite.objects.get_current().domain base = "https://" + DjangoSite.objects.get_current().domain
return urljoin(base, self.url) return urljoin(base, self.url)
@property @property
def affected_urls(self): def affected_urls(self):
base = 'https://' + DjangoSite.objects.get_current().domain base = "https://" + DjangoSite.objects.get_current().domain
kind = kinds.from_id[self.kind] kind = kinds.from_id[self.kind]
urls = { urls = {
self.url, self.url,
reverse('entries:index', kwargs={'kind': kind}), reverse("entries:index", kwargs={"kind": kind}),
reverse('entries:atom_by_kind', kwargs={'kind': kind}), reverse("entries:atom_by_kind", kwargs={"kind": kind}),
reverse('entries:rss_by_kind', kwargs={'kind': kind}), reverse("entries:rss_by_kind", kwargs={"kind": kind}),
} | {cat.url for cat in self.cats.all()} } | {cat.url for cat in self.cats.all()}
if kind.on_home: if kind.on_home:
urls |= { urls |= {
reverse('home:index'), reverse("home:index"),
reverse('entries:atom'), reverse("entries:atom"),
reverse('entries:rss') reverse("entries:rss"),
} }
return {urljoin(base, u) for u in urls} return {urljoin(base, u) for u in urls}
@ -170,7 +157,7 @@ class Entry(ModelMeta, TimeStampedModel):
args = [kind, self.id] args = [kind, self.id]
if kind.slug: if kind.slug:
args.append(self.slug) args.append(self.slug)
return reverse('entries:entry', args=args) return reverse("entries:entry", args=args)
@property @property
def short_url(self): def short_url(self):
@ -182,49 +169,48 @@ class Entry(ModelMeta, TimeStampedModel):
@property @property
def json_ld(self): def json_ld(self):
base = 'https://' + DjangoSite.objects.get_current().domain base = "https://" + DjangoSite.objects.get_current().domain
url = urljoin(base, self.url) url = urljoin(base, self.url)
posting = { posting = {
'@context': 'http://schema.org', "@context": "http://schema.org",
'@type': 'BlogPosting', "@type": "BlogPosting",
'@id': url, "@id": url,
'url': url, "url": url,
'mainEntityOfPage': url, "mainEntityOfPage": url,
'author': { "author": {
'@type': 'Person', "@type": "Person",
'url': urljoin(base, self.author.url), "url": urljoin(base, self.author.url),
'name': self.author.name, "name": self.author.name,
}, },
'headline': self.title, "headline": self.title,
'description': self.excerpt, "description": self.excerpt,
'datePublished': self.created.isoformat(), "datePublished": self.created.isoformat(),
'dateModified': self.modified.isoformat(), "dateModified": self.modified.isoformat(),
} }
if self.photo: if self.photo:
posting['image'] = (urljoin(base, self.photo.url), ) posting["image"] = (urljoin(base, self.photo.url),)
return posting return posting
class Meta: class Meta:
verbose_name_plural = 'entries' verbose_name_plural = "entries"
ordering = ['-created'] ordering = ["-created"]
class Syndication(models.Model): class Syndication(models.Model):
entry = models.ForeignKey( entry = models.ForeignKey(
Entry, Entry, related_name="syndications", on_delete=models.CASCADE
related_name='syndications',
on_delete=models.CASCADE
) )
url = models.CharField(max_length=255) url = models.CharField(max_length=255)
domain = ComputedCharField( domain = ComputedCharField(
compute_from='calc_domain', max_length=255, compute_from="calc_domain",
max_length=255,
) )
def calc_domain(self): def calc_domain(self):
domain = urlparse(self.url).netloc domain = urlparse(self.url).netloc
if domain.startswith('www.'): if domain.startswith("www."):
domain = domain[4:] domain = domain[4:]
return domain return domain
@ -234,7 +220,7 @@ class Syndication(models.Model):
try: try:
return Site.objects.get(domain=d) return Site.objects.get(domain=d)
except Site.DoesNotExist: 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: class Meta:
ordering = ['domain'] ordering = ["domain"]

View file

@ -3,27 +3,27 @@ import pytest
@pytest.mark.django_db @pytest.mark.django_db
def test_atom(client): def test_atom(client):
res = client.get('/atom') res = client.get("/atom")
assert res.status_code == 200 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 @pytest.mark.django_db
def test_rss(client): def test_rss(client):
res = client.get('/rss') res = client.get("/rss")
assert res.status_code == 200 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 @pytest.mark.django_db
def test_atom_by_kind(client): def test_atom_by_kind(client):
res = client.get('/notes/atom') res = client.get("/notes/atom")
assert res.status_code == 200 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 @pytest.mark.django_db
def test_rss_by_kind(client): def test_rss_by_kind(client):
res = client.get('/notes/rss') res = client.get("/notes/rss")
assert res.status_code == 200 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"

View file

@ -3,47 +3,46 @@ from . import kinds
from .views import feeds, lists, perma from .views import feeds, lists, perma
from lemoncurry import breadcrumbs as crumbs from lemoncurry import breadcrumbs as crumbs
register_converter(kinds.EntryKindConverter, 'kind') register_converter(kinds.EntryKindConverter, "kind")
def to_pat(*args): def to_pat(*args):
return '^{0}$'.format(''.join(args)) return "^{0}$".format("".join(args))
def prefix(route): def prefix(route):
return app_name + ':' + route return app_name + ":" + route
id = r'/(?P<id>\d+)' id = r"/(?P<id>\d+)"
kind = r'(?P<kind>{0})'.format('|'.join(k.plural for k in kinds.all)) kind = r"(?P<kind>{0})".format("|".join(k.plural for k in kinds.all))
page = r'(?:/page/(?P<page>\d+))?' page = r"(?:/page/(?P<page>\d+))?"
slug = r'/(?P<slug>[^/]+)' slug = r"/(?P<slug>[^/]+)"
slug_opt = '(?:' + slug + ')?' slug_opt = "(?:" + slug + ")?"
app_name = 'entries' app_name = "entries"
urlpatterns = ( urlpatterns = (
path('atom', feeds.AtomHomeEntries(), name='atom'), path("atom", feeds.AtomHomeEntries(), name="atom"),
path('rss', feeds.RssHomeEntries(), name='rss'), path("rss", feeds.RssHomeEntries(), name="rss"),
path('cats/<slug:slug>', lists.by_cat, name='cat'), path("cats/<slug:slug>", lists.by_cat, name="cat"),
path('cats/<slug:slug>/page/<int:page>', lists.by_cat, name='cat'), path("cats/<slug:slug>/page/<int:page>", lists.by_cat, name="cat"),
path('<kind:kind>', lists.by_kind, name='index'), path("<kind:kind>", lists.by_kind, name="index"),
path('<kind:kind>/page/<int:page>', lists.by_kind, name='index'), path("<kind:kind>/page/<int:page>", lists.by_kind, name="index"),
path('<kind:kind>/atom', feeds.AtomByKind(), name='atom_by_kind'), path("<kind:kind>/atom", feeds.AtomByKind(), name="atom_by_kind"),
path('<kind:kind>/rss', feeds.RssByKind(), name='rss_by_kind'), path("<kind:kind>/rss", feeds.RssByKind(), name="rss_by_kind"),
path("<kind:kind>/<int:id>", perma.entry, name="entry"),
path('<kind:kind>/<int:id>', perma.entry, name='entry'), path("<kind:kind>/<int:id>/<slug:slug>", perma.entry, name="entry"),
path('<kind:kind>/<int:id>/<slug:slug>', perma.entry, name='entry'),
) )
class IndexCrumb(crumbs.Crumb): class IndexCrumb(crumbs.Crumb):
def __init__(self): def __init__(self):
super().__init__(prefix('index'), parent='home:index') super().__init__(prefix("index"), parent="home:index")
@property @property
def kind(self): def kind(self):
return self.match.kwargs['kind'] return self.match.kwargs["kind"]
@property @property
def label(self): def label(self):
@ -51,9 +50,9 @@ class IndexCrumb(crumbs.Crumb):
@property @property
def url(self): 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(IndexCrumb())
crumbs.add(prefix('entry'), parent=prefix('index')) crumbs.add(prefix("entry"), parent=prefix("index"))

View file

@ -11,8 +11,8 @@ from ..models import Entry
class Atom1FeedWithHub(Atom1Feed): class Atom1FeedWithHub(Atom1Feed):
def add_root_elements(self, handler): def add_root_elements(self, handler):
super().add_root_elements(handler) super().add_root_elements(handler)
handler.startElement('link', {'rel': 'hub', 'href': settings.PUSH_HUB}) handler.startElement("link", {"rel": "hub", "href": settings.PUSH_HUB})
handler.endElement('link') handler.endElement("link")
class EntriesFeed(Feed): class EntriesFeed(Feed):
@ -79,7 +79,7 @@ class RssHomeEntries(EntriesFeed):
return Site.objects.get_current().name return Site.objects.get_current().name
def link(self): def link(self):
return reverse('home:index') return reverse("home:index")
def description(self): def description(self):
return "content from {0}".format( return "content from {0}".format(

View file

@ -5,32 +5,32 @@ from ..models import Entry, Cat
from ..pagination import paginate from ..pagination import paginate
@render_to('entries/index.html') @render_to("entries/index.html")
def by_kind(request, kind, page=None): def by_kind(request, kind, page=None):
entries = Entry.objects.filter(kind=kind.id) entries = Entry.objects.filter(kind=kind.id)
entries = paginate(queryset=entries, reverse=kind.index_page, page=page) entries = paginate(queryset=entries, reverse=kind.index_page, page=page)
return { return {
'entries': entries, "entries": entries,
'atom': kind.atom, "atom": kind.atom,
'rss': kind.rss, "rss": kind.rss,
'title': kind.plural, "title": kind.plural,
} }
@render_to('entries/index.html') @render_to("entries/index.html")
def by_cat(request, slug, page=None): def by_cat(request, slug, page=None):
def url(page): def url(page):
kwargs = {'slug': slug} kwargs = {"slug": slug}
if page > 1: if page > 1:
kwargs['page'] = page kwargs["page"] = page
return reverse('entries:cat', kwargs=kwargs) return reverse("entries:cat", kwargs=kwargs)
cat = get_object_or_404(Cat, slug=slug) cat = get_object_or_404(Cat, slug=slug)
entries = cat.entries.all() entries = cat.entries.all()
entries = paginate(queryset=entries, reverse=url, page=page) entries = paginate(queryset=entries, reverse=url, page=page)
return { return {
'entries': entries, "entries": entries,
'title': '#' + cat.name, "title": "#" + cat.name,
} }

View file

@ -3,12 +3,12 @@ from django.shortcuts import redirect, get_object_or_404
from ..models import Entry from ..models import Entry
@render_to('entries/entry.html') @render_to("entries/entry.html")
def entry(request, kind, id, slug=None): def entry(request, kind, id, slug=None):
entry = get_object_or_404(Entry, pk=id) entry = get_object_or_404(Entry, pk=id)
if request.path != entry.url: if request.path != entry.url:
return redirect(entry.url, permanent=True) return redirect(entry.url, permanent=True)
return { return {
'entry': entry, "entry": entry,
'title': entry.title, "title": entry.title,
} }

View file

@ -1,5 +1,5 @@
import multiprocessing import multiprocessing
proc_name = 'lemoncurry' proc_name = "lemoncurry"
worker_class = 'gevent' worker_class = "gevent"
workers = multiprocessing.cpu_count() * 2 + 1 workers = multiprocessing.cpu_count() * 2 + 1

View file

@ -3,10 +3,10 @@ from django.urls import reverse
class HomeSitemap(sitemaps.Sitemap): class HomeSitemap(sitemaps.Sitemap):
changefreq = 'daily' changefreq = "daily"
def items(self): def items(self):
return ('home:index',) return ("home:index",)
def location(self, item): def location(self, item):
return reverse(item) return reverse(item)

View file

@ -2,9 +2,9 @@ from django.urls import path
from . import views from . import views
app_name = 'home' app_name = "home"
urlpatterns = [ urlpatterns = [
path('', views.index, name='index'), path("", views.index, name="index"),
path('page/<int:page>', views.index, name='index'), path("page/<int:page>", views.index, name="index"),
path('robots.txt', views.robots, name='robots.txt'), path("robots.txt", views.robots, name="robots.txt"),
] ]

View file

@ -8,34 +8,31 @@ from urllib.parse import urljoin
from entries import kinds, pagination from entries import kinds, pagination
from lemoncurry import breadcrumbs, utils 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 index(request, page=None):
def url(page): def url(page):
kwargs = {'page': page} if page != 1 else {} kwargs = {"page": page} if page != 1 else {}
return reverse('home:index', kwargs=kwargs) return reverse("home:index", kwargs=kwargs)
user = request.user user = request.user
if not hasattr(user, 'entries'): if not hasattr(user, "entries"):
user = get_object_or_404(User, pk=1) user = get_object_or_404(User, pk=1)
entries = user.entries.filter(kind__in=kinds.on_home) entries = user.entries.filter(kind__in=kinds.on_home)
entries = pagination.paginate(queryset=entries, reverse=url, page=page) entries = pagination.paginate(queryset=entries, reverse=url, page=page)
return { return {
'user': user, "user": user,
'entries': entries, "entries": entries,
'atom': reverse('entries:atom'), "atom": reverse("entries:atom"),
'rss': reverse('entries:rss'), "rss": reverse("entries:rss"),
} }
def robots(request): def robots(request):
base = utils.origin(request) base = utils.origin(request)
lines = ( lines = ("User-agent: *", "Sitemap: {0}".format(urljoin(base, reverse("sitemap"))))
'User-agent: *', return HttpResponse("\n".join(lines) + "\n", content_type="text/plain")
'Sitemap: {0}'.format(urljoin(base, reverse('sitemap')))
)
return HttpResponse("\n".join(lines) + "\n", content_type='text/plain')

View file

@ -7,25 +7,36 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [] # type: List[Tuple[str, str]]
] # type: List[Tuple[str, str]]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='IndieAuthCode', name="IndieAuthCode",
fields=[ fields=[
('id', models.AutoField(auto_created=True, (
primary_key=True, serialize=False, verbose_name='ID')), "id",
('code', models.CharField(max_length=64, unique=True)), models.AutoField(
('me', models.CharField(max_length=255)), auto_created=True,
('client_id', models.CharField(max_length=255)), primary_key=True,
('redirect_uri', models.CharField(max_length=255)), serialize=False,
('response_type', models.CharField(choices=[ verbose_name="ID",
('id', 'id'), ('code', 'code')], default='id', max_length=4)), ),
('scope', models.CharField(blank=True, max_length=200)), ),
("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)),
], ],
), ),
] ]

View file

@ -6,13 +6,12 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('lemonauth', '0001_initial'), ("lemonauth", "0001_initial"),
] ]
operations = [ operations = [
migrations.DeleteModel( migrations.DeleteModel(
name='IndieAuthCode', name="IndieAuthCode",
), ),
] ]

View file

@ -9,43 +9,112 @@ import randomslugfield.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('lemonauth', '0002_delete_indieauthcode'), ("lemonauth", "0002_delete_indieauthcode"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='IndieAuthCode', name="IndieAuthCode",
fields=[ 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')), "created",
('id', randomslugfield.fields.RandomSlugField(blank=True, editable=False, length=30, max_length=30, primary_key=True, serialize=False, unique=True)), model_utils.fields.AutoCreatedField(
('client_id', models.URLField()), default=django.utils.timezone.now,
('scope', models.TextField(blank=True)), editable=False,
('redirect_uri', models.URLField()), verbose_name="created",
('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)), ),
(
"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={ options={
'abstract': False, "abstract": False,
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Token', name="Token",
fields=[ 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')), "created",
('id', randomslugfield.fields.RandomSlugField(blank=True, editable=False, length=30, max_length=30, primary_key=True, serialize=False, unique=True)), model_utils.fields.AutoCreatedField(
('client_id', models.URLField()), default=django.utils.timezone.now,
('scope', models.TextField(blank=True)), editable=False,
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 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={ options={
'abstract': False, "abstract": False,
}, },
), ),
] ]

View file

@ -17,6 +17,7 @@ class AuthSecret(TimeStampedModel):
authorisation codes and tokens in IndieAuth - the two contain many authorisation codes and tokens in IndieAuth - the two contain many
identical fields, but just a few differences. identical fields, but just a few differences.
""" """
id = RandomSlugField(primary_key=True, length=30) id = RandomSlugField(primary_key=True, length=30)
client_id = models.URLField() client_id = models.URLField()
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
@ -27,7 +28,7 @@ class AuthSecret(TimeStampedModel):
return self.user.full_url return self.user.full_url
def __contains__(self, scope): def __contains__(self, scope):
return scope in self.scope.split(' ') return scope in self.scope.split(" ")
class Meta: class Meta:
abstract = True abstract = True
@ -41,10 +42,11 @@ class IndieAuthCode(AuthSecret):
Codes are single-use, and if unused will be expired automatically after Codes are single-use, and if unused will be expired automatically after
thirty seconds. thirty seconds.
""" """
redirect_uri = models.URLField() redirect_uri = models.URLField()
RESPONSE_TYPE = Choices('id', 'code') RESPONSE_TYPE = Choices("id", "code")
response_type = StatusField(choices_name='RESPONSE_TYPE') response_type = StatusField(choices_name="RESPONSE_TYPE")
@property @property
def expired(self): def expired(self):
@ -56,4 +58,5 @@ class Token(AuthSecret):
A Token grants a client long-term authorisation - it will not expire unless A Token grants a client long-term authorisation - it will not expire unless
explicitly revoked by the user. explicitly revoked by the user.
""" """
pass pass

View file

@ -3,17 +3,17 @@ from .models import IndieAuthCode, Token
def auth(request) -> Token: def auth(request) -> Token:
if 'HTTP_AUTHORIZATION' in request.META: if "HTTP_AUTHORIZATION" in request.META:
auth = request.META.get('HTTP_AUTHORIZATION').split(' ') auth = request.META.get("HTTP_AUTHORIZATION").split(" ")
if auth[0] != 'Bearer': if auth[0] != "Bearer":
raise error.bad_req('auth type {0} not supported'.format(auth[0])) raise error.bad_req("auth type {0} not supported".format(auth[0]))
if len(auth) != 2: if len(auth) != 2:
raise error.bad_req('invalid Bearer auth format, must be Bearer <token>') raise error.bad_req("invalid Bearer auth format, must be Bearer <token>")
token = auth[1] token = auth[1]
elif 'access_token' in request.POST: elif "access_token" in request.POST:
token = request.POST.get('access_token') token = request.POST.get("access_token")
elif 'access_token' in request.GET: elif "access_token" in request.GET:
token = request.GET.get('access_token') token = request.GET.get("access_token")
else: else:
raise error.unauthorized() raise error.unauthorized()
@ -28,11 +28,11 @@ def auth(request) -> Token:
def gen_auth_code(req): def gen_auth_code(req):
code = IndieAuthCode() code = IndieAuthCode()
code.user = req.user code.user = req.user
code.client_id = req.POST['client_id'] code.client_id = req.POST["client_id"]
code.redirect_uri = req.POST['redirect_uri'] code.redirect_uri = req.POST["redirect_uri"]
code.response_type = req.POST.get('response_type', 'id') code.response_type = req.POST.get("response_type", "id")
if 'scope' in req.POST: if "scope" in req.POST:
code.scope = ' '.join(req.POST.getlist('scope')) code.scope = " ".join(req.POST.getlist("scope"))
code.save() code.save()
return code.id return code.id

View file

@ -1,13 +1,17 @@
from django.urls import path from django.urls import path
from . import views from . import views
app_name = 'lemonauth' app_name = "lemonauth"
urlpatterns = [ urlpatterns = [
path('login', views.login, name='login'), path("login", views.login, name="login"),
path('logout', views.logout, name='logout'), path("logout", views.logout, name="logout"),
path('indie', views.IndieView.as_view(), name='indie'), path("indie", views.IndieView.as_view(), name="indie"),
path('indie/approve', views.indie_approve, name='indie_approve'), path("indie/approve", views.indie_approve, name="indie_approve"),
path('token', views.TokenView.as_view(), name='token'), path("token", views.TokenView.as_view(), name="token"),
path('tokens', views.TokensListView.as_view(), name='tokens'), path("tokens", views.TokensListView.as_view(), name="tokens"),
path('tokens/<path:client_id>', views.TokensRevokeView.as_view(), name='tokens_revoke'), path(
"tokens/<path:client_id>",
views.TokensRevokeView.as_view(),
name="tokens_revoke",
),
] ]

View file

@ -12,120 +12,114 @@ from urllib.parse import urlencode, urljoin, urlunparse, urlparse
from .. import tokens from .. import tokens
from ..models import IndieAuthCode from ..models import IndieAuthCode
breadcrumbs.add('lemonauth:indie', parent='home:index') breadcrumbs.add("lemonauth:indie", parent="home:index")
def canonical(url): def canonical(url):
if '//' not in url: if "//" not in url:
url = '//' + url url = "//" + url
(scheme, netloc, path, params, query, fragment) = urlparse(url) (scheme, netloc, path, params, query, fragment) = urlparse(url)
if not scheme or scheme == 'http': if not scheme or scheme == "http":
scheme = 'https' scheme = "https"
if not path: if not path:
path = '/' path = "/"
return urlunparse((scheme, netloc, path, params, query, fragment)) return urlunparse((scheme, netloc, path, params, query, fragment))
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name="dispatch")
class IndieView(TemplateView): class IndieView(TemplateView):
template_name = 'lemonauth/indie.html' template_name = "lemonauth/indie.html"
required_params = ('client_id', 'redirect_uri') required_params = ("client_id", "redirect_uri")
@method_decorator(login_required) @method_decorator(login_required)
@method_decorator(render_to(template_name)) @method_decorator(render_to(template_name))
def get(self, request): def get(self, request):
params = request.GET.dict() params = request.GET.dict()
params.setdefault('response_type', 'id') params.setdefault("response_type", "id")
for param in self.required_params: for param in self.required_params:
if param not in params: if param not in params:
return utils.bad_req( return utils.bad_req("parameter {0} is required".format(param))
'parameter {0} is required'.format(param)
)
me = request.user.full_url me = request.user.full_url
if 'me' in params: if "me" in params:
param_me = canonical(params['me']) param_me = canonical(params["me"])
if me != param_me: if me != param_me:
return utils.forbid( 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'] type = params["response_type"]
if type not in ('id', 'code'): if type not in ("id", "code"):
return utils.bad_req( return utils.bad_req("unknown response_type: {0}".format(type))
'unknown response_type: {0}'.format(type)
)
scopes = () scopes = ()
if type == 'code': if type == "code":
if 'scope' not in params: if "scope" not in params:
return utils.bad_req( return utils.bad_req("scopes required for code type")
'scopes required for code type' scopes = params["scope"].split(" ")
)
scopes = params['scope'].split(' ')
client = requests.mf2(params['client_id']) client = requests.mf2(params["client_id"])
rels = (client.to_dict()['rel-urls'] rels = client.to_dict()["rel-urls"].get(redirect_uri, {}).get("rels", ())
.get(redirect_uri, {}) verified = "redirect_uri" in rels
.get('rels', ()))
verified = 'redirect_uri' in rels
try: 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: except IndexError:
app = None app = None
return { return {
'app': app, "app": app,
'me': me, "me": me,
'redirect_uri': redirect_uri, "redirect_uri": redirect_uri,
'verified': verified, "verified": verified,
'params': params, "params": params,
'scopes': scopes, "scopes": scopes,
'title': 'indieauth from {client_id}'.format(**params), "title": "indieauth from {client_id}".format(**params),
} }
def post(self, request): def post(self, request):
post = request.POST.dict() post = request.POST.dict()
try: try:
code = IndieAuthCode.objects.get(pk=post.get('code')) code = IndieAuthCode.objects.get(pk=post.get("code"))
except IndieAuthCode.DoesNotExist: except IndieAuthCode.DoesNotExist:
# if anything at all goes wrong when decoding the auth code, bail # if anything at all goes wrong when decoding the auth code, bail
# out immediately. # out immediately.
return utils.forbid('invalid auth code') return utils.forbid("invalid auth code")
code.delete() code.delete()
if code.expired: if code.expired:
return utils.forbid('invalid auth code') return utils.forbid("invalid auth code")
if code.response_type != 'id': if code.response_type != "id":
return utils.bad_req( return utils.bad_req("this endpoint only supports response_type=id")
'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.client_id != post.get('client_id'): if code.redirect_uri != post.get("redirect_uri"):
return utils.forbid('client id did not match') return utils.forbid("redirect uri 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! # If we got here, it's valid! Yay!
return utils.choose_type(request, {'me': code.me}, { return utils.choose_type(
'application/x-www-form-urlencoded': utils.form_encoded_response, request,
'application/json': JsonResponse, {"me": code.me},
}) {
"application/x-www-form-urlencoded": utils.form_encoded_response,
"application/json": JsonResponse,
},
)
@login_required @login_required
@require_POST @require_POST
def approve(request): def approve(request):
params = { params = {
'me': urljoin(utils.origin(request), request.user.url), "me": urljoin(utils.origin(request), request.user.url),
'code': tokens.gen_auth_code(request), "code": tokens.gen_auth_code(request),
} }
if 'state' in request.POST: if "state" in request.POST:
params['state'] = request.POST['state'] params["state"] = request.POST["state"]
uri = request.POST['redirect_uri'] uri = request.POST["redirect_uri"]
sep = '&' if '?' in uri else '?' sep = "&" if "?" in uri else "?"
return redirect(uri + sep + urlencode(params)) return redirect(uri + sep + urlencode(params))

View file

@ -2,11 +2,11 @@ import django.contrib.auth.views
from otp_agents.forms import OTPAuthenticationForm from otp_agents.forms import OTPAuthenticationForm
from lemoncurry import breadcrumbs 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( login = django.contrib.auth.views.LoginView.as_view(
authentication_form=OTPAuthenticationForm, authentication_form=OTPAuthenticationForm,
extra_context={'title': 'log in'}, extra_context={"title": "log in"},
template_name='lemonauth/login.html', template_name="lemonauth/login.html",
redirect_authenticated_user=True, redirect_authenticated_user=True,
) )

View file

@ -7,41 +7,42 @@ from ..models import IndieAuthCode
from lemoncurry import utils from lemoncurry import utils
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name="dispatch")
class TokenView(View): class TokenView(View):
def get(self, req): def get(self, req):
token = tokens.auth(req) token = tokens.auth(req)
res = { res = {
'me': token.me, "me": token.me,
'client_id': token.client_id, "client_id": token.client_id,
'scope': token.scope, "scope": token.scope,
} }
return utils.choose_type(req, res) return utils.choose_type(req, res)
def post(self, req): def post(self, req):
post = req.POST post = req.POST
try: try:
code = IndieAuthCode.objects.get(pk=post.get('code')) code = IndieAuthCode.objects.get(pk=post.get("code"))
except IndieAuthCode.DoesNotExist: except IndieAuthCode.DoesNotExist:
return utils.forbid('invalid auth code') return utils.forbid("invalid auth code")
code.delete() code.delete()
if code.expired: if code.expired:
return utils.forbid('invalid auth code') return utils.forbid("invalid auth code")
if code.response_type != 'code': if code.response_type != "code":
return utils.bad_req( return utils.bad_req("this endpoint only supports response_type=code")
'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")
return utils.choose_type(
req,
{
"access_token": tokens.gen_token(code),
"me": code.me,
"scope": code.scope,
},
) )
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')
return utils.choose_type(req, {
'access_token': tokens.gen_token(code),
'me': code.me,
'scope': code.scope,
})

View file

@ -20,15 +20,15 @@ class Client:
self.id = client_id self.id = client_id
self.count = 0 self.count = 0
self.scopes = set() 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: try:
self.app = apps[0]['properties'] self.app = apps[0]["properties"]
except IndexError: except IndexError:
self.app = None self.app = None
class TokensListView(LoginRequiredMixin, TemplateView): class TokensListView(LoginRequiredMixin, TemplateView):
template_name = 'lemonauth/tokens.html' template_name = "lemonauth/tokens.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -36,6 +36,6 @@ class TokensListView(LoginRequiredMixin, TemplateView):
for token in self.request.user.token_set.all(): for token in self.request.user.token_set.all():
client = clients[token.client_id] client = clients[token.client_id]
client.count += 1 client.count += 1
client.scopes |= set(token.scope.split(' ')) client.scopes |= set(token.scope.split(" "))
context.update({'clients': clients, 'title': 'tokens'}) context.update({"clients": clients, "title": "tokens"})
return context return context

View file

@ -14,7 +14,7 @@ class Crumb:
return self._label return self._label
def __eq__(self, other): def __eq__(self, other):
if hasattr(other, 'route'): if hasattr(other, "route"):
return self.route == other.route return self.route == other.route
return self.route == other return self.route == other

View file

@ -2,6 +2,6 @@ from debug_toolbar.middleware import show_toolbar as core_show_toolbar
def show_toolbar(request): def show_toolbar(request):
if request.path.endswith('/amp'): if request.path.endswith("/amp"):
return False return False
return core_show_toolbar(request) return core_show_toolbar(request)

View file

@ -22,18 +22,22 @@ def environment(**options):
lstrip_blocks=True, lstrip_blocks=True,
**options **options
) )
env.filters.update({ env.filters.update(
'ago': ago, {
'friendly_url': friendly_url, "ago": ago,
'markdown': markdown, "friendly_url": friendly_url,
}) "markdown": markdown,
env.globals.update({ }
'entry_kinds': entry_kinds, )
'favicons': favicons, env.globals.update(
'package': load_package_json(), {
'settings': settings, "entry_kinds": entry_kinds,
'static': staticfiles_storage.url, "favicons": favicons,
'theme_color': theme_color, "package": load_package_json(),
'url': reverse, "settings": settings,
}) "static": staticfiles_storage.url,
"theme_color": theme_color,
"url": reverse,
}
)
return env return env

View file

@ -6,4 +6,4 @@ def ago(dt: datetime) -> str:
# We have to convert the datetime we get to local time first, because ago # We have to convert the datetime we get to local time first, because ago
# just strips the timezone from a timezone-aware datetime. # just strips the timezone from a timezone-aware datetime.
dt = dt.astimezone() dt = dt.astimezone()
return human(dt, precision=1, past_tense='{}', abbreviate=True) return human(dt, precision=1, past_tense="{}", abbreviate=True)

View file

@ -3,13 +3,13 @@ from bleach.linkifier import LinkifyFilter
from jinja2 import pass_eval_context from jinja2 import pass_eval_context
from markupsafe import Markup 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) TAGS.extend(ALLOWED_TAGS)
ATTRIBUTES = { ATTRIBUTES = {
'a': ['href', 'title', 'class'], "a": ["href", "title", "class"],
'details': ['open'], "details": ["open"],
'img': ['alt', 'src', 'title'], "img": ["alt", "src", "title"],
'span': ['class'], "span": ["class"],
} }
cleaner = Cleaner(tags=TAGS, attributes=ATTRIBUTES, filters=(LinkifyFilter,)) cleaner = Cleaner(tags=TAGS, attributes=ATTRIBUTES, filters=(LinkifyFilter,))

View file

@ -3,12 +3,14 @@ from markdown import Markdown
from .bleach import bleach from .bleach import bleach
md = Markdown(extensions=( md = Markdown(
'extra', extensions=(
'sane_lists', "extra",
'smarty', "sane_lists",
'toc', "smarty",
)) "toc",
)
)
@pass_eval_context @pass_eval_context

View file

@ -8,7 +8,9 @@ class ResponseException(Exception):
class ResponseExceptionMiddleware(MiddlewareMixin): 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): if isinstance(exception, ResponseException):
return exception.response return exception.response
raise exception raise exception

View file

@ -11,7 +11,7 @@ from mf2py import Parser
class DjangoCache(BaseCache): class DjangoCache(BaseCache):
@classmethod @classmethod
def key(cls, url): def key(cls, url):
return 'req:' + sha256(url.encode('utf-8')).hexdigest() return "req:" + sha256(url.encode("utf-8")).hexdigest()
def get(self, url): def get(self, url):
key = self.key(url) key = self.key(url)
@ -45,4 +45,4 @@ def get(url):
def mf2(url): def mf2(url):
r = get(url) r = get(url)
return Parser(doc=r.text, url=url, html_parser='html5lib') return Parser(doc=r.text, url=url, html_parser="html5lib")

View file

@ -16,7 +16,7 @@ from typing import List
APPEND_SLASH = False APPEND_SLASH = False
ADMINS = [ ADMINS = [
('dani', 'dani@00dani.me'), ("dani", "dani@00dani.me"),
] ]
BASE_DIR = path.dirname(path.dirname(path.dirname(path.abspath(__file__)))) 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/ # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS: List[str] = [] 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, # Settings to tighten up security - these can safely be on in dev mode too,
# since I dev using a local HTTPS server. # since I dev using a local HTTPS server.
@ -50,7 +50,7 @@ CSRF_COOKIE_SECURE = True
# Miscellanous headers to protect against attacks. # Miscellanous headers to protect against attacks.
SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = 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 # This technically isn't needed, since nginx doesn't let the app be accessed
# over insecure HTTP anyway. Just for completeness! # 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 # We run behind nginx, so we need nginx to tell us whether we're using HTTPS or
# not. # not.
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'lemoncurry', "lemoncurry",
'pyup_django', "pyup_django",
"django.contrib.admin",
'django.contrib.admin', "django.contrib.admindocs",
'django.contrib.admindocs', "django.contrib.auth",
'django.contrib.auth', "django.contrib.contenttypes",
'django.contrib.contenttypes', "django.contrib.humanize",
'django.contrib.humanize', "django.contrib.sessions",
'django.contrib.sessions', "django.contrib.sites",
'django.contrib.sites', "django.contrib.sitemaps",
'django.contrib.sitemaps', "django.contrib.messages",
'django.contrib.messages', "django.contrib.staticfiles",
'django.contrib.staticfiles', "analytical",
"annoying",
'analytical', "compressor",
'annoying', "computed_property",
'compressor', "corsheaders",
'computed_property', "debug_toolbar",
'corsheaders', "django_activeurl",
'debug_toolbar', "django_agent_trust",
'django_activeurl', "django_extensions",
'django_agent_trust', "django_otp",
'django_extensions', "django_otp.plugins.otp_static",
'django_otp', "django_otp.plugins.otp_totp",
'django_otp.plugins.otp_static', "django_rq",
'django_otp.plugins.otp_totp', "meta",
'django_rq', "entries",
'meta', "home",
"lemonauth",
'entries', "lemonshort",
'home', "micropub",
'lemonauth', "users",
'lemonshort', "webmention",
'micropub', "wellknowns",
'users',
'webmention',
'wellknowns',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware', "debug_toolbar.middleware.DebugToolbarMiddleware",
'django.middleware.http.ConditionalGetMiddleware', "django.middleware.http.ConditionalGetMiddleware",
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.admindocs.middleware.XViewMiddleware', "django.contrib.admindocs.middleware.XViewMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'corsheaders.middleware.CorsMiddleware', "corsheaders.middleware.CorsMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django_otp.middleware.OTPMiddleware', "django_otp.middleware.OTPMiddleware",
'django_agent_trust.middleware.AgentMiddleware', "django_agent_trust.middleware.AgentMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.contrib.sites.middleware.CurrentSiteMiddleware', "django.contrib.sites.middleware.CurrentSiteMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
'lemoncurry.middleware.ResponseExceptionMiddleware', "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 = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.jinja2.Jinja2', "BACKEND": "django.template.backends.jinja2.Jinja2",
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'environment': 'lemoncurry.jinja2.environment', "environment": "lemoncurry.jinja2.environment",
}, },
}, },
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
], ],
}, },
}, },
] ]
WSGI_APPLICATION = 'lemoncurry.wsgi.application' WSGI_APPLICATION = "lemoncurry.wsgi.application"
# Cache # Cache
# https://docs.djangoproject.com/en/1.11/ref/settings/#std:setting-CACHES # https://docs.djangoproject.com/en/1.11/ref/settings/#std:setting-CACHES
CACHES = { CACHES = {
'default': { "default": {
'BACKEND': 'django_redis.cache.RedisCache', "BACKEND": "django_redis.cache.RedisCache",
'LOCATION': 'redis://127.0.0.1:6380/0', "LOCATION": "redis://127.0.0.1:6380/0",
'KEY_PREFIX': 'lemoncurry', "KEY_PREFIX": "lemoncurry",
'OPTIONS': { "OPTIONS": {
'PARSER_CLASS': 'redis.connection.HiredisParser', "PARSER_CLASS": "redis.connection.HiredisParser",
'SERIALIZER': 'lemoncurry.msgpack.MSGPackModernSerializer', "SERIALIZER": "lemoncurry.msgpack.MSGPackModernSerializer",
}, },
'VERSION': 2, "VERSION": 2,
} }
} }
@ -168,51 +165,51 @@ CACHES = {
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.postgresql', "ENGINE": "django.db.backends.postgresql",
'NAME': environ.get('POSTGRES_DB', 'lemoncurry'), "NAME": environ.get("POSTGRES_DB", "lemoncurry"),
'USER': environ.get('POSTGRES_USER'), "USER": environ.get("POSTGRES_USER"),
'PASSWORD': environ.get('POSTGRES_PASSWORD'), "PASSWORD": environ.get("POSTGRES_PASSWORD"),
'HOST': environ.get('POSTGRES_HOST', 'localhost'), "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 # Password hashers
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
PASSWORD_HASHERS = [ PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher', "django.contrib.auth.hashers.Argon2PasswordHasher",
'django.contrib.auth.hashers.PBKDF2PasswordHasher', "django.contrib.auth.hashers.PBKDF2PasswordHasher",
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
'django.contrib.auth.hashers.BCryptPasswordHasher', "django.contrib.auth.hashers.BCryptPasswordHasher",
] ]
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators # 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 = [ AUTH_PASSWORD_VALIDATORS = [
{'NAME': PW_VALIDATOR_MODULE + '.UserAttributeSimilarityValidator'}, {"NAME": PW_VALIDATOR_MODULE + ".UserAttributeSimilarityValidator"},
{'NAME': PW_VALIDATOR_MODULE + '.MinimumLengthValidator'}, {"NAME": PW_VALIDATOR_MODULE + ".MinimumLengthValidator"},
{'NAME': PW_VALIDATOR_MODULE + '.CommonPasswordValidator'}, {"NAME": PW_VALIDATOR_MODULE + ".CommonPasswordValidator"},
{'NAME': PW_VALIDATOR_MODULE + '.NumericPasswordValidator'}, {"NAME": PW_VALIDATOR_MODULE + ".NumericPasswordValidator"},
] ]
LOGIN_URL = 'lemonauth:login' LOGIN_URL = "lemonauth:login"
LOGIN_REDIRECT_URL = 'home:index' LOGIN_REDIRECT_URL = "home:index"
LOGOUT_REDIRECT_URL = LOGIN_REDIRECT_URL LOGOUT_REDIRECT_URL = LOGIN_REDIRECT_URL
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/ # 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 USE_I18N = True
@ -224,23 +221,21 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/ # https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = "/static/"
STATIC_ROOT = path.join(BASE_DIR, 'static') STATIC_ROOT = path.join(BASE_DIR, "static")
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', "django.contrib.staticfiles.finders.FileSystemFinder",
'django.contrib.staticfiles.finders.AppDirectoriesFinder', "django.contrib.staticfiles.finders.AppDirectoriesFinder",
'compressor.finders.CompressorFinder', "compressor.finders.CompressorFinder",
)
STATICFILES_STORAGE = (
'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
) )
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
COMPRESS_PRECOMPILERS = ( 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_URL = STATIC_URL + "media/"
MEDIA_ROOT = path.join(STATIC_ROOT, 'media') MEDIA_ROOT = path.join(STATIC_ROOT, "media")
# django-contrib-sites # django-contrib-sites
# https://docs.djangoproject.com/en/dev/ref/contrib/sites/ # https://docs.djangoproject.com/en/dev/ref/contrib/sites/
@ -252,25 +247,25 @@ AGENT_COOKIE_SECURE = True
# django-cors-headers # django-cors-headers
CORS_ORIGIN_ALLOW_ALL = True CORS_ORIGIN_ALLOW_ALL = True
CORS_URLS_REGEX = r'^/(?!admin|auth/(?:login|logout|indie)).*$' CORS_URLS_REGEX = r"^/(?!admin|auth/(?:login|logout|indie)).*$"
# lemonshort # lemonshort
SHORT_BASE_URL = '/s/' SHORT_BASE_URL = "/s/"
SHORTEN_MODELS = { SHORTEN_MODELS = {
'e': 'entries.entry', "e": "entries.entry",
} }
# django-meta # django-meta
# https://django-meta.readthedocs.io/en/latest/settings.html # https://django-meta.readthedocs.io/en/latest/settings.html
META_SITE_PROTOCOL = 'https' META_SITE_PROTOCOL = "https"
META_USE_SITES = True META_USE_SITES = True
META_USE_OG_PROPERTIES = True META_USE_OG_PROPERTIES = True
META_USE_TWITTER_PROPERTIES = True META_USE_TWITTER_PROPERTIES = True
# django-push # django-push
# https://django-push.readthedocs.io/en/latest/publisher.html # https://django-push.readthedocs.io/en/latest/publisher.html
PUSH_HUB = 'https://00dani.superfeedr.com/' PUSH_HUB = "https://00dani.superfeedr.com/"
# django-rq # django-rq
# https://github.com/ui/django-rq # https://github.com/ui/django-rq
RQ_QUEUES = {'default': {'USE_REDIS_CACHE': 'default'}} RQ_QUEUES = {"default": {"USE_REDIS_CACHE": "default"}}

View file

@ -1,7 +1,7 @@
from .base import * from .base import *
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ["*"]
META_SITE_DOMAIN = '00dani.lo' META_SITE_DOMAIN = "00dani.lo"
META_FB_APPID = '142105433189339' META_FB_APPID = "142105433189339"
STATIC_URL = 'https://static.00dani.lo/' STATIC_URL = "https://static.00dani.lo/"
MEDIA_URL = 'https://media.00dani.lo/' MEDIA_URL = "https://media.00dani.lo/"

View file

@ -4,19 +4,19 @@ from os.path import join
from .base import * from .base import *
from .base import BASE_DIR, DATABASES from .base import BASE_DIR, DATABASES
ALLOWED_HOSTS = ['00dani.me'] ALLOWED_HOSTS = ["00dani.me"]
DEBUG = False DEBUG = False
SECRET_KEY = environ['DJANGO_SECRET_KEY'] SECRET_KEY = environ["DJANGO_SECRET_KEY"]
SERVER_EMAIL = 'lemoncurry@00dani.me' SERVER_EMAIL = "lemoncurry@00dani.me"
# Authenticate as an app-specific Postgres user in production. # 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') STATIC_ROOT = join(BASE_DIR, "..", "static")
MEDIA_ROOT = join(BASE_DIR, '..', 'media') MEDIA_ROOT = join(BASE_DIR, "..", "media")
STATIC_URL = 'https://cdn.00dani.me/' STATIC_URL = "https://cdn.00dani.me/"
MEDIA_URL = STATIC_URL + 'm/' MEDIA_URL = STATIC_URL + "m/"
META_SITE_DOMAIN = '00dani.me' META_SITE_DOMAIN = "00dani.me"
META_FB_APPID = '145311792869199' META_FB_APPID = "145311792869199"

View file

@ -1,8 +1,8 @@
from .base import * from .base import *
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ["*"]
SECURE_SSL_REDIRECT = False SECURE_SSL_REDIRECT = False
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
MEDIA_URL = '/media/' MEDIA_URL = "/media/"
STATIC_ROOT = path.join(BASE_DIR, 'media') STATIC_ROOT = path.join(BASE_DIR, "media")

View file

@ -8,5 +8,5 @@ register = template.Library()
@register.simple_tag @register.simple_tag
@register.filter(is_safe=True) @register.filter(is_safe=True)
def absolute_url(url): def absolute_url(url):
base = 'https://' + Site.objects.get_current().domain base = "https://" + Site.objects.get_current().domain
return urljoin(base, url) return urljoin(base, url)

View file

@ -5,13 +5,13 @@ from django.utils.safestring import mark_safe
from bleach.sanitizer import Cleaner, ALLOWED_TAGS from bleach.sanitizer import Cleaner, ALLOWED_TAGS
from bleach.linkifier import LinkifyFilter 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) tags.extend(ALLOWED_TAGS)
attributes = { attributes = {
'a': ['href', 'title', 'class'], "a": ["href", "title", "class"],
'details': ['open'], "details": ["open"],
'img': ['alt', 'src', 'title'], "img": ["alt", "src", "title"],
'span': ['class'], "span": ["class"],
} }
register = template.Library() register = template.Library()

View file

@ -11,5 +11,5 @@ register = template.Library()
@register.filter @register.filter
def jsonify(value): def jsonify(value):
if isinstance(value, QuerySet): if isinstance(value, QuerySet):
return mark_safe(serialize('json', value)) return mark_safe(serialize("json", value))
return mark_safe(json.dumps(value, cls=DjangoJSONEncoder)) return mark_safe(json.dumps(value, cls=DjangoJSONEncoder))

View file

@ -1,4 +1,3 @@
from django import template from django import template
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
@ -40,67 +39,71 @@ def site_name():
return Site.objects.get_current().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): def nav_left(request):
items = (MenuItem( items = (
label=k.plural, MenuItem(label=k.plural, icon=k.icon, url=("entries:index", (k,)))
icon=k.icon, for k in kinds.all
url=('entries:index', (k,)) )
) for k in kinds.all) return {"items": items, "request": request}
return {'items': items, 'request': request}
@register.inclusion_tag('lemoncurry/tags/nav.html') @register.inclusion_tag("lemoncurry/tags/nav.html")
def nav_right(request): def nav_right(request):
if request.user.is_authenticated: if request.user.is_authenticated:
items = ( items = (
MenuItem(label='admin', icon='fas fa-cog', url='admin:index'), MenuItem(label="admin", icon="fas fa-cog", url="admin:index"),
MenuItem(label='log out', icon='fas fa-sign-out-alt', MenuItem(
url='lemonauth:logout'), label="log out", icon="fas fa-sign-out-alt", url="lemonauth:logout"
),
) )
else: else:
items = ( items = (
MenuItem(label='log in', icon='fas fa-sign-in-alt', MenuItem(label="log in", icon="fas fa-sign-in-alt", url="lemonauth:login"),
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): def nav_crumbs(context, route):
crumbs = breadcrumbs.find(route) crumbs = breadcrumbs.find(route)
current = crumbs.pop() current = crumbs.pop()
item_list_element = [{ item_list_element = [
'@type': 'ListItem', {
'position': i + 1, "@type": "ListItem",
'item': { "position": i + 1,
'@id': context['origin'] + crumb.url, "item": {
'@type': 'WebPage', "@id": context["origin"] + crumb.url,
'name': crumb.label "@type": "WebPage",
"name": crumb.label,
},
} }
} for i, crumb in enumerate(crumbs)] for i, crumb in enumerate(crumbs)
item_list_element.append({ ]
'@type': 'ListItem', item_list_element.append(
'position': len(item_list_element) + 1, {
'item': { "@type": "ListItem",
'id': context['uri'], "position": len(item_list_element) + 1,
'@type': 'WebPage', "item": {
'name': current.label or context.get('title'), "id": context["uri"],
"@type": "WebPage",
"name": current.label or context.get("title"),
},
} }
}) )
breadcrumb_list = { breadcrumb_list = {
'@context': 'http://schema.org', "@context": "http://schema.org",
'@type': 'BreadcrumbList', "@type": "BreadcrumbList",
'itemListElement': item_list_element "itemListElement": item_list_element,
} }
return { return {
'breadcrumb_list': breadcrumb_list, "breadcrumb_list": breadcrumb_list,
'crumbs': crumbs, "crumbs": crumbs,
'current': current, "current": current,
'title': context.get('title'), "title": context.get("title"),
} }

View file

@ -3,12 +3,14 @@ from django import template
from markdown import Markdown from markdown import Markdown
from .bleach import bleach from .bleach import bleach
md = Markdown(extensions=( md = Markdown(
'extra', extensions=(
'sane_lists', "extra",
'smarty', "sane_lists",
'toc', "smarty",
)) "toc",
)
)
register = template.Library() register = template.Library()

View file

@ -6,43 +6,43 @@ from .. import breadcrumbs as b
@pytest.fixture @pytest.fixture
def nested_crumbs(): def nested_crumbs():
x = b.Crumb('nc.x', label='x') x = b.Crumb("nc.x", label="x")
y = b.Crumb('nc.y', label='y', parent='nc.x') y = b.Crumb("nc.y", label="y", parent="nc.x")
z = b.Crumb('nc.z', label='z', parent='nc.y') z = b.Crumb("nc.z", label="z", parent="nc.y")
crumbs = (x, y, z) crumbs = (x, y, z)
for crumb in crumbs: for crumb in crumbs:
b.breadcrumbs[crumb.route] = crumb b.breadcrumbs[crumb.route] = crumb
yield namedtuple('NestedCrumbs', 'x y z')(*crumbs) yield namedtuple("NestedCrumbs", "x y z")(*crumbs)
for crumb in crumbs: for crumb in crumbs:
del b.breadcrumbs[crumb.route] del b.breadcrumbs[crumb.route]
@pytest.fixture @pytest.fixture
def crumb_match(nested_crumbs): 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: class TestAdd:
def test_inserts_a_breadcrumb_without_parent(self): def test_inserts_a_breadcrumb_without_parent(self):
route = 'tests.add.insert' route = "tests.add.insert"
assert route not in b.breadcrumbs assert route not in b.breadcrumbs
b.add(route, 'some label') b.add(route, "some label")
assert route in b.breadcrumbs assert route in b.breadcrumbs
assert b.breadcrumbs[route] == route assert b.breadcrumbs[route] == route
route = b.breadcrumbs[route] route = b.breadcrumbs[route]
assert route.label == 'some label' assert route.label == "some label"
assert route.parent is None assert route.parent is None
def test_inserts_a_breadcrumb_with_parent(self): def test_inserts_a_breadcrumb_with_parent(self):
route = 'tests.add.with_parent' route = "tests.add.with_parent"
parent = 'tests.add.insert' parent = "tests.add.insert"
assert route not in b.breadcrumbs assert route not in b.breadcrumbs
b.add(route, 'child label', parent) b.add(route, "child label", parent)
assert route in b.breadcrumbs assert route in b.breadcrumbs
assert b.breadcrumbs[route] == route assert b.breadcrumbs[route] == route
route = b.breadcrumbs[route] route = b.breadcrumbs[route]
assert route.label == 'child label' assert route.label == "child label"
assert route.parent == parent assert route.parent == parent

View file

@ -5,22 +5,22 @@ from .. import utils
class TestOrigin: class TestOrigin:
def test_simple_http(self): def test_simple_http(self):
"""should return the correct origin for a vanilla HTTP site""" """should return the correct origin for a vanilla HTTP site"""
req = Mock(scheme='http', site=Mock(domain='lemoncurry.test')) req = Mock(scheme="http", site=Mock(domain="lemoncurry.test"))
assert utils.origin(req) == 'http://lemoncurry.test' assert utils.origin(req) == "http://lemoncurry.test"
def test_simple_https(self): def test_simple_https(self):
"""should return the correct origin for a vanilla HTTPS site""" """should return the correct origin for a vanilla HTTPS site"""
req = Mock(scheme='https', site=Mock(domain='secure.lemoncurry.test')) req = Mock(scheme="https", site=Mock(domain="secure.lemoncurry.test"))
assert utils.origin(req) == 'https://secure.lemoncurry.test' assert utils.origin(req) == "https://secure.lemoncurry.test"
class TestUri: class TestUri:
def test_siteroot(self): def test_siteroot(self):
"""should return correct full URI for requests to the site root""" """should return correct full URI for requests to the site root"""
req = Mock(scheme='https', path='/', site=Mock(domain='l.test')) req = Mock(scheme="https", path="/", site=Mock(domain="l.test"))
assert utils.uri(req) == 'https://l.test/' assert utils.uri(req) == "https://l.test/"
def test_path(self): def test_path(self):
"""should return correct full URI for requests with a path""" """should return correct full URI for requests with a path"""
req = Mock(scheme='https', path='/notes/23', site=Mock(domain='l.tst')) req = Mock(scheme="https", path="/notes/23", site=Mock(domain="l.tst"))
assert utils.uri(req) == 'https://l.tst/notes/23' assert utils.uri(req) == "https://l.tst/notes/23"

View file

@ -4,12 +4,14 @@ from yaml import safe_load
path = join( path = join(
settings.BASE_DIR, settings.BASE_DIR,
'lemoncurry', 'static', "lemoncurry",
'base16-materialtheme-scheme', 'material-darker.yaml', "static",
"base16-materialtheme-scheme",
"material-darker.yaml",
) )
with open(path, 'r') as f: with open(path, "r") as f:
theme = safe_load(f) theme = safe_load(f)
def color(i): def color(i):
return '#' + theme['base0' + format(i, '1X')] return "#" + theme["base0" + format(i, "1X")]

View file

@ -27,33 +27,37 @@ from entries.sitemaps import EntriesSitemap
from home.sitemaps import HomeSitemap from home.sitemaps import HomeSitemap
sections = { sections = {
'entries': EntriesSitemap, "entries": EntriesSitemap,
'home': HomeSitemap, "home": HomeSitemap,
} }
maps = {'sitemaps': sections} maps = {"sitemaps": sections}
urlpatterns = ( urlpatterns = (
path('', include('home.urls')), path("", include("home.urls")),
path('', include('entries.urls')), path("", include("entries.urls")),
path('', include('users.urls')), path("", include("users.urls")),
path('.well-known/', include('wellknowns.urls')), path(".well-known/", include("wellknowns.urls")),
path('admin/doc/', include('django.contrib.admindocs.urls')), path("admin/doc/", include("django.contrib.admindocs.urls")),
path('admin/', admin.site.urls), path("admin/", admin.site.urls),
path('auth/', include('lemonauth.urls')), path("auth/", include("lemonauth.urls")),
path('favicon.ico', RedirectView.as_view( path(
url=settings.MEDIA_URL + 'favicon/favicon.ico')), "favicon.ico",
path('micropub', include('micropub.urls')), RedirectView.as_view(url=settings.MEDIA_URL + "favicon/favicon.ico"),
path('s/', include('lemonshort.urls')), ),
path('webmention', include('webmention.urls')), path("micropub", include("micropub.urls")),
path("s/", include("lemonshort.urls")),
path('django-rq/', include('django_rq.urls')), path("webmention", include("webmention.urls")),
path('sitemap.xml', sitemap.index, maps, name='sitemap'), path("django-rq/", include("django_rq.urls")),
path('sitemaps/<section>.xml', sitemap.sitemap, maps, path("sitemap.xml", sitemap.index, maps, name="sitemap"),
name='django.contrib.sitemaps.views.sitemap'), path(
"sitemaps/<section>.xml",
sitemap.sitemap,
maps,
name="django.contrib.sitemaps.views.sitemap",
),
) # type: Tuple[URLPattern, ...] ) # type: Tuple[URLPattern, ...]
if settings.DEBUG: if settings.DEBUG:
import debug_toolbar import debug_toolbar
urlpatterns += (
path('__debug__/', include(debug_toolbar.urls)), urlpatterns += (path("__debug__/", include(debug_toolbar.urls)),)
)

View file

@ -20,7 +20,7 @@ class PackageJson:
def load(self) -> Dict[str, Any]: def load(self) -> Dict[str, Any]:
if self.data is None: 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) self.data = json.load(f)
assert self.data is not None assert self.data is not None
return self.data return self.data
@ -30,10 +30,10 @@ PACKAGE = PackageJson()
def friendly_url(url): def friendly_url(url):
if '//' not in url: if "//" not in url:
url = '//' + url url = "//" + url
(scheme, netloc, path, params, q, fragment) = urlparse(url) (scheme, netloc, path, params, q, fragment) = urlparse(url)
if path == '/': if path == "/":
return netloc return netloc
return "{}\u200B{}".format(netloc, path) return "{}\u200B{}".format(netloc, path)
@ -43,7 +43,7 @@ def load_package_json() -> Dict[str, Any]:
def origin(request): 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): def absolute_url(request, url):
@ -56,19 +56,18 @@ def uri(request):
def form_encoded_response(content): def form_encoded_response(content):
return HttpResponse( return HttpResponse(
urlencode(content), urlencode(content), content_type="application/x-www-form-urlencoded"
content_type='application/x-www-form-urlencoded'
) )
REPS = { REPS = {
'application/x-www-form-urlencoded': form_encoded_response, "application/x-www-form-urlencoded": form_encoded_response,
'application/json': JsonResponse, "application/json": JsonResponse,
} }
def choose_type(request, content, reps=REPS): 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()) type = get_best_match(accept, reps.keys())
if type: if type:
return reps[type](content) return reps[type](content)
@ -76,11 +75,11 @@ def choose_type(request, content, reps=REPS):
def bad_req(message): def bad_req(message):
return HttpResponseBadRequest(message, content_type='text/plain') return HttpResponseBadRequest(message, content_type="text/plain")
def forbid(message): def forbid(message):
return HttpResponseForbidden(message, content_type='text/plain') return HttpResponseForbidden(message, content_type="text/plain")
def to_plain(md): def to_plain(md):

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class LemonshortConfig(AppConfig): class LemonshortConfig(AppConfig):
name = 'lemonshort' name = "lemonshort"

View file

@ -6,8 +6,9 @@ from string import ascii_lowercase, ascii_uppercase
chars = ascii_uppercase + ascii_lowercase chars = ascii_uppercase + ascii_lowercase
conv = BaseConverter(chars) conv = BaseConverter(chars)
class AbcIdConverter: class AbcIdConverter:
regex = '[a-zA-Z]+' regex = "[a-zA-Z]+"
def to_python(self, value: str) -> int: def to_python(self, value: str) -> int:
return int(conv.decode(value)) return int(conv.decode(value))

View file

@ -11,7 +11,7 @@ def short_url(entity):
if not prefixes: if not prefixes:
for k, m in settings.SHORTEN_MODELS.items(): for k, m in settings.SHORTEN_MODELS.items():
prefixes[apps.get_model(m)] = k prefixes[apps.get_model(m)] = k
base = '/' base = "/"
if hasattr(settings, 'SHORT_BASE_URL'): if hasattr(settings, "SHORT_BASE_URL"):
base = settings.SHORT_BASE_URL base = settings.SHORT_BASE_URL
return base + prefixes[type(entity)] + AbcIdConverter().to_url(entity.id) return base + prefixes[type(entity)] + AbcIdConverter().to_url(entity.id)

View file

@ -3,14 +3,14 @@ from .. import convert
def test_to_python(): def test_to_python():
samples = { samples = {
'A': 0, "A": 0,
'B': 1, "B": 1,
'Y': 24, "Y": 24,
'a': 26, "a": 26,
'b': 27, "b": 27,
'y': 50, "y": 50,
'BA': 52, "BA": 52,
'BAB': 2705, "BAB": 2705,
} }
converter = convert.AbcIdConverter() converter = convert.AbcIdConverter()
for abc, id in samples.items(): for abc, id in samples.items():
@ -19,13 +19,13 @@ def test_to_python():
def test_id_to_abc(): def test_id_to_abc():
samples = { samples = {
1: 'B', 1: "B",
24: 'Y', 24: "Y",
26: 'a', 26: "a",
52: 'BA', 52: "BA",
78: 'Ba', 78: "Ba",
104: 'CA', 104: "CA",
130: 'Ca', 130: "Ca",
} }
converter = convert.AbcIdConverter() converter = convert.AbcIdConverter()
for id, abc in samples.items(): for id, abc in samples.items():

View file

@ -4,10 +4,10 @@ from django.urls import path, register_converter
from .convert import AbcIdConverter from .convert import AbcIdConverter
from .views import unshort from .views import unshort
register_converter(AbcIdConverter, 'abc_id') register_converter(AbcIdConverter, "abc_id")
app_name = 'lemonshort' app_name = "lemonshort"
urlpatterns = tuple( urlpatterns = tuple(
path('{0!s}<abc_id:tiny>'.format(k), unshort, name=m, kwargs={'model': m}) path("{0!s}<abc_id:tiny>".format(k), unshort, name=m, kwargs={"model": m})
for k, m in settings.SHORTEN_MODELS.items() for k, m in settings.SHORTEN_MODELS.items()
) )

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class MicropubConfig(AppConfig): class MicropubConfig(AppConfig):
name = 'micropub' name = "micropub"

View file

@ -4,33 +4,35 @@ from typing import Optional
def forbidden() -> ResponseException: def forbidden() -> ResponseException:
return res('forbidden', 403) return res("forbidden", 403)
def unauthorized() -> ResponseException: def unauthorized() -> ResponseException:
return res('unauthorized', 401) return res("unauthorized", 401)
def bad_req(msg: str) -> ResponseException: def bad_req(msg: str) -> ResponseException:
return res('invalid_request', msg=msg) return res("invalid_request", msg=msg)
def bad_type(type: str) -> ResponseException: def bad_type(type: str) -> ResponseException:
msg = 'unsupported request type {0}'.format(type) msg = "unsupported request type {0}".format(type)
return res('invalid_request', 415, msg) return res("invalid_request", 415, msg)
def bad_scope(scope: str) -> ResponseException: def bad_scope(scope: str) -> ResponseException:
return res('insufficient_scope', 401, scope=scope) return res("insufficient_scope", 401, scope=scope)
def res(error: str, def res(
error: str,
status: Optional[int] = 400, status: Optional[int] = 400,
msg: Optional[str] = None, msg: Optional[str] = None,
scope: Optional[str]=None): scope: Optional[str] = None,
content = {'error': error} ):
content = {"error": error}
if msg is not None: if msg is not None:
content['error_description'] = msg content["error_description"] = msg
if scope: if scope:
content['scope'] = scope content["scope"] = scope
return ResponseException(JsonResponse(content, status=status)) return ResponseException(JsonResponse(content, status=status))

View file

@ -2,8 +2,8 @@ from django.urls import path
from .views import micropub from .views import micropub
from .views.media import media from .views.media import media
app_name = 'micropub' app_name = "micropub"
urlpatterns = ( urlpatterns = (
path('', micropub, name='micropub'), path("", micropub, name="micropub"),
path('/media', media, name='media'), path("/media", media, name="media"),
) )

View file

@ -10,22 +10,22 @@ from .delete import delete
from .query import query from .query import query
actions = { actions = {
'create': create, "create": create,
'delete': delete, "delete": delete,
} }
@csrf_exempt @csrf_exempt
@require_http_methods(['GET', 'HEAD', 'POST']) @require_http_methods(["GET", "HEAD", "POST"])
def micropub(request): def micropub(request):
request.token = tokens.auth(request) request.token = tokens.auth(request)
if request.method in ('GET', 'HEAD'): if request.method in ("GET", "HEAD"):
return query(request) return query(request)
action = request.POST.get('action', 'create') action = request.POST.get("action", "create")
if request.content_type == 'application/json': if request.content_type == "application/json":
request.json = json.load(request) request.json = json.load(request)
action = request.json.get('action', 'create') action = request.json.get("action", "create")
if action not in actions: 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) return actions[action](request)

View file

@ -14,63 +14,62 @@ def form_to_mf2(request):
properties = {} properties = {}
post = request.POST post = request.POST
for key in post.keys(): for key in post.keys():
if key.endswith('[]'): if key.endswith("[]"):
key = key[:-2] key = key[:-2]
if key == 'access_token': if key == "access_token":
continue continue
properties[key] = post.getlist(key) + post.getlist(key + '[]') properties[key] = post.getlist(key) + post.getlist(key + "[]")
type = [] type = []
if 'h' in properties: if "h" in properties:
type = ['h-' + p for p in properties['h']] type = ["h-" + p for p in properties["h"]]
del properties['h'] del properties["h"]
return {'type': type, 'properties': properties} return {"type": type, "properties": properties}
def create(request): def create(request):
normalise = { normalise = {
'application/json': lambda r: r.json, "application/json": lambda r: r.json,
'application/x-www-form-urlencoded': form_to_mf2, "application/x-www-form-urlencoded": form_to_mf2,
} }
if 'create' not in request.token: if "create" not in request.token:
raise error.bad_scope('create') raise error.bad_scope("create")
if request.content_type not in normalise: if request.content_type not in normalise:
raise error.unsupported_type(request.content_type) raise error.unsupported_type(request.content_type)
body = normalise[request.content_type](request) body = normalise[request.content_type](request)
if 'type' not in body: if "type" not in body:
raise error.bad_req('mf2 object type required') raise error.bad_req("mf2 object type required")
if body['type'] != ['h-entry']: if body["type"] != ["h-entry"]:
raise error.bad_req('only h-entry supported') raise error.bad_req("only h-entry supported")
entry = Entry(author=request.token.user) entry = Entry(author=request.token.user)
props = body.get('properties', {}) props = body.get("properties", {})
kind = Note kind = Note
if 'name' in props: if "name" in props:
entry.name = '\n'.join(props['name']) entry.name = "\n".join(props["name"])
kind = Article kind = Article
if 'content' in props: if "content" in props:
entry.content = '\n'.join( entry.content = "\n".join(
c if isinstance(c, str) else c['html'] c if isinstance(c, str) else c["html"] for c in props["content"]
for c in props['content']
) )
if 'in-reply-to' in props: if "in-reply-to" in props:
entry.in_reply_to = props['in-reply-to'] entry.in_reply_to = props["in-reply-to"]
kind = Reply kind = Reply
if 'like-of' in props: if "like-of" in props:
entry.like_of = props['like-of'] entry.like_of = props["like-of"]
kind = Like kind = Like
if 'repost-of' in props: if "repost-of" in props:
entry.repost_of = props['repost-of'] entry.repost_of = props["repost-of"]
kind = Repost 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.kind = kind.id
entry.save() entry.save()
entry.cats.set(cats) entry.cats.set(cats)
entry.save() entry.save()
for url in props.get('syndication', []): for url in props.get("syndication", []):
entry.syndications.create(url=url) entry.syndications.create(url=url)
base = utils.origin(request) base = utils.origin(request)
@ -80,6 +79,6 @@ def create(request):
send_mentions.delay(perma) send_mentions.delay(perma)
res = HttpResponse(status=201) res = HttpResponse(status=201)
res['Location'] = perma res["Location"] = perma
res['Link'] = '<{}>; rel="shortlink"'.format(short) res["Link"] = '<{}>; rel="shortlink"'.format(short)
return res return res

View file

@ -6,24 +6,25 @@ from entries.jobs import ping_hub, send_mentions
from .. import error from .. import error
def delete(request): def delete(request):
normalise = { normalise = {
'application/json': lambda r: r.json.get('url'), "application/json": lambda r: r.json.get("url"),
'application/x-www-form-urlencoded': lambda r: r.POST.get('url'), "application/x-www-form-urlencoded": lambda r: r.POST.get("url"),
} }
if 'delete' not in request.token: if "delete" not in request.token:
raise error.bad_scope('delete') raise error.bad_scope("delete")
if request.content_type not in normalise: if request.content_type not in normalise:
raise error.unsupported_type(request.content_type) raise error.unsupported_type(request.content_type)
url = normalise[request.content_type](request) url = normalise[request.content_type](request)
entry = from_url(url) entry = from_url(url)
if entry.author != request.token.user: 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 perma = entry.absolute_url
pings = entry.affected_urls pings = entry.affected_urls
mentions = webmention.findMentions(perma)['refs'] mentions = webmention.findMentions(perma)["refs"]
entry.delete() entry.delete()

View file

@ -11,9 +11,9 @@ from lemoncurry.utils import absolute_url
from .. import error from .. import error
ACCEPTED_MEDIA_TYPES = ( ACCEPTED_MEDIA_TYPES = (
'image/gif', "image/gif",
'image/jpeg', "image/jpeg",
'image/png', "image/png",
) )
@ -21,15 +21,13 @@ ACCEPTED_MEDIA_TYPES = (
@require_POST @require_POST
def media(request): def media(request):
token = tokens.auth(request) token = tokens.auth(request)
if 'file' not in request.FILES: if "file" not in request.FILES:
raise error.bad_req( raise error.bad_req(
"a file named 'file' must be provided to the media endpoint" "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: if file.content_type not in ACCEPTED_MEDIA_TYPES:
raise error.bad_req( raise error.bad_req("unacceptable file type {0}".format(file.content_type))
'unacceptable file type {0}'.format(file.content_type)
)
mime = None mime = None
sha = hashlib.sha256() sha = hashlib.sha256()
@ -40,14 +38,15 @@ def media(request):
if mime != file.content_type: if mime != file.content_type:
raise error.bad_req( raise error.bad_req(
'detected file type {0} did not match specified file type {1}' "detected file type {0} did not match specified file type {1}".format(
.format(mime, file.content_type) 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) path = store.save(path, file)
url = absolute_url(request, store.url(path)) url = absolute_url(request, store.url(path))
res = HttpResponse(status=201) res = HttpResponse(status=201)
res['Location'] = url res["Location"] = url
return res return res

View file

@ -7,48 +7,47 @@ from lemoncurry.utils import absolute_url
from .. import error from .. import error
def config(request): def config(request):
config = syndicate_to(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 return config
def source(request): def source(request):
if 'url' not in request.GET: if "url" not in request.GET:
raise error.bad_req('must specify url parameter for source query') raise error.bad_req("must specify url parameter for source query")
entry = from_url(request.GET['url']) entry = from_url(request.GET["url"])
props = {} props = {}
keys = set(request.GET.getlist('properties') + request.GET.getlist('properties[]')) keys = set(request.GET.getlist("properties") + request.GET.getlist("properties[]"))
if not keys or 'content' in keys: if not keys or "content" in keys:
props['content'] = [entry.content] props["content"] = [entry.content]
if (not keys or 'category' in keys) and entry.cats.exists(): if (not keys or "category" in keys) and entry.cats.exists():
props['category'] = [cat.name for cat in entry.cats.all()] props["category"] = [cat.name for cat in entry.cats.all()]
if (not keys or 'name' in keys) and entry.name: if (not keys or "name" in keys) and entry.name:
props['name'] = [entry.name] props["name"] = [entry.name]
if (not keys or 'syndication' in keys) and entry.syndications.exists(): if (not keys or "syndication" in keys) and entry.syndications.exists():
props['syndication'] = [synd.url for synd in entry.syndications.all()] 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): def syndicate_to(request):
return {'syndicate-to': []} return {"syndicate-to": []}
queries = { queries = {
'config': config, "config": config,
'source': source, "source": source,
'syndicate-to': syndicate_to, "syndicate-to": syndicate_to,
} }
def query(request): def query(request):
if 'q' not in request.GET: if "q" not in request.GET:
raise error.bad_req('must specify q parameter') raise error.bad_req("must specify q parameter")
q = request.GET['q'] q = request.GET["q"]
if q not in queries: 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) res = queries[q](request)
return JsonResponse(res) return JsonResponse(res)

View file

@ -4,7 +4,7 @@ from .models import PgpKey, Profile, Site, User
class SiteAdmin(admin.ModelAdmin): class SiteAdmin(admin.ModelAdmin):
list_display = ('name', 'icon', 'domain', 'url_template') list_display = ("name", "icon", "domain", "url_template")
class PgpKeyInline(admin.TabularInline): class PgpKeyInline(admin.TabularInline):
@ -19,7 +19,7 @@ class ProfileInline(admin.TabularInline):
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
fieldsets = BaseUserAdmin.fieldsets + ( fieldsets = BaseUserAdmin.fieldsets + (
('Profile', {'fields': ('avatar', 'xmpp', 'note')}), ("Profile", {"fields": ("avatar", "xmpp", "note")}),
) )
inlines = ( inlines = (
PgpKeyInline, PgpKeyInline,

View file

@ -2,5 +2,5 @@ from django.apps import AppConfig
class UsersConfig(AppConfig): class UsersConfig(AppConfig):
name = 'users' name = "users"
verbose_name = 'Users and Profiles' verbose_name = "Users and Profiles"

View file

@ -9,40 +9,127 @@ import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('auth', '0008_alter_user_username_max_length'), ("auth", "0008_alter_user_username_max_length"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='User', name="User",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('password', models.CharField(max_length=128, verbose_name='password')), "id",
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), models.AutoField(
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), auto_created=True,
('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')), primary_key=True,
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), serialize=False,
('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), verbose_name="ID",
('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')), ("password", models.CharField(max_length=128, verbose_name="password")),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), (
('avatar', models.ImageField(upload_to='')), "last_login",
('note', models.TextField(blank=True)), models.DateTimeField(
('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')), blank=True, null=True, verbose_name="last login"
('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')), ),
),
(
"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={ options={
'verbose_name': 'user', "verbose_name": "user",
'verbose_name_plural': 'users', "verbose_name_plural": "users",
'abstract': False, "abstract": False,
}, },
managers=[ managers=[
('objects', django.contrib.auth.models.UserManager()), ("objects", django.contrib.auth.models.UserManager()),
], ],
), ),
] ]

View file

@ -7,15 +7,14 @@ import users.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0001_initial'), ("users", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='user', model_name="user",
name='avatar', name="avatar",
field=models.ImageField(upload_to=users.models.avatar_path), field=models.ImageField(upload_to=users.models.avatar_path),
), ),
] ]

View file

@ -8,19 +8,33 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0002_auto_20171023_0109'), ("users", "0002_auto_20171023_0109"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Key', name="Key",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('fingerprint', models.CharField(max_length=40)), "id",
('file', models.FileField(upload_to='keys')), models.AutoField(
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='keys', to=settings.AUTH_USER_MODEL)), 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,
),
),
], ],
), ),
] ]

View file

@ -8,48 +8,67 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0003_key'), ("users", "0003_key"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Profile', name="Profile",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('username', models.CharField(max_length=100)), "id",
('display_name', models.CharField(blank=True, max_length=100)), 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={ options={
'ordering': ('site', 'username'), "ordering": ("site", "username"),
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Site', name="Site",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=100, unique=True)), "id",
('icon', models.CharField(max_length=100)), models.AutoField(
('url', models.CharField(max_length=100)), 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={ options={
'ordering': ('name',), "ordering": ("name",),
}, },
), ),
migrations.AddField( migrations.AddField(
model_name='profile', model_name="profile",
name='site', name="site",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Site'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="users.Site"
),
), ),
migrations.AddField( migrations.AddField(
model_name='profile', model_name="profile",
name='user', name="user",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
), ),
migrations.AddField( migrations.AddField(
model_name='user', model_name="user",
name='profiles', name="profiles",
field=models.ManyToManyField(through='users.Profile', to='users.Site'), field=models.ManyToManyField(through="users.Profile", to="users.Site"),
), ),
] ]

View file

@ -8,19 +8,22 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0004_auto_20171023_0143'), ("users", "0004_auto_20171023_0143"),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(
model_name='user', model_name="user",
name='profiles', name="profiles",
), ),
migrations.AlterField( migrations.AlterField(
model_name='profile', model_name="profile",
name='user', name="user",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profiles', to=settings.AUTH_USER_MODEL), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="profiles",
to=settings.AUTH_USER_MODEL,
),
), ),
] ]

View file

@ -6,21 +6,20 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0005_auto_20171023_0158'), ("users", "0005_auto_20171023_0158"),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(
model_name='site', model_name="site",
old_name='url', old_name="url",
new_name='url_template', new_name="url_template",
), ),
migrations.AddField( migrations.AddField(
model_name='site', model_name="site",
name='domain', name="domain",
field=models.CharField(default='', max_length=100), field=models.CharField(default="", max_length=100),
preserve_default=False, preserve_default=False,
), ),
] ]

View file

@ -6,15 +6,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0006_auto_20171031_1336'), ("users", "0006_auto_20171031_1336"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='site', model_name="site",
name='domain', name="domain",
field=models.CharField(blank=True, max_length=100), field=models.CharField(blank=True, max_length=100),
), ),
] ]

View file

@ -6,14 +6,13 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0007_auto_20171031_1347'), ("users", "0007_auto_20171031_1347"),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='site', name="site",
options={'ordering': ('domain',)}, options={"ordering": ("domain",)},
), ),
] ]

View file

@ -6,15 +6,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0008_auto_20171031_1357'), ("users", "0008_auto_20171031_1357"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='user', model_name="user",
name='xmpp', name="xmpp",
field=models.EmailField(blank=True, max_length=254), field=models.EmailField(blank=True, max_length=254),
), ),
] ]

View file

@ -6,14 +6,13 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0009_user_xmpp'), ("users", "0009_user_xmpp"),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='site', name="site",
options={'ordering': ('name',)}, options={"ordering": ("name",)},
), ),
] ]

View file

@ -6,15 +6,13 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0010_auto_20171206_2211'), ("users", "0010_auto_20171206_2211"),
] ]
operations = [ operations = [
migrations.AlterModelManagers( migrations.AlterModelManagers(
name='user', name="user",
managers=[ managers=[],
],
), ),
] ]

View file

@ -7,16 +7,15 @@ import users.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0011_auto_20180124_1311'), ("users", "0011_auto_20180124_1311"),
] ]
operations = [ operations = [
migrations.AlterModelManagers( migrations.AlterModelManagers(
name='user', name="user",
managers=[ managers=[
('objects', users.models.UserManager()), ("objects", users.models.UserManager()),
], ],
), ),
] ]

View file

@ -5,20 +5,31 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0012_auto_20180129_1614'), ("users", "0012_auto_20180129_1614"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='user', model_name="user",
name='email_md5', name="email_md5",
field=computed_property.fields.ComputedCharField(compute_from='calc_email_md5', default='', editable=False, max_length=32, unique=True), field=computed_property.fields.ComputedCharField(
compute_from="calc_email_md5",
default="",
editable=False,
max_length=32,
unique=True,
),
), ),
migrations.AddField( migrations.AddField(
model_name='user', model_name="user",
name='email_sha256', name="email_sha256",
field=computed_property.fields.ComputedCharField(compute_from='calc_email_sha256', default='', editable=False, max_length=64, unique=True), field=computed_property.fields.ComputedCharField(
compute_from="calc_email_sha256",
default="",
editable=False,
max_length=64,
unique=True,
),
), ),
] ]

View file

@ -6,58 +6,79 @@ import users.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0013_auto_20180323_1200'), ("users", "0013_auto_20180323_1200"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='profile', model_name="profile",
name='display_name', name="display_name",
field=models.CharField( 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( migrations.AlterField(
model_name='profile', model_name="profile",
name='username', name="username",
field=models.CharField( 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( migrations.AlterField(
model_name='user', model_name="user",
name='avatar', name="avatar",
field=models.ImageField( 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( migrations.AlterField(
model_name='user', model_name="user",
name='email_md5', name="email_md5",
field=computed_property.fields.ComputedCharField( 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( migrations.AlterField(
model_name='user', model_name="user",
name='email_sha256', name="email_sha256",
field=computed_property.fields.ComputedCharField( 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( migrations.AlterField(
model_name='user', model_name="user",
name='last_name', name="last_name",
field=models.CharField( field=models.CharField(
blank=True, max_length=150, verbose_name='last name'), blank=True, max_length=150, verbose_name="last name"
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='user', model_name="user",
name='note', name="note",
field=models.TextField( 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( migrations.AlterField(
model_name='user', model_name="user",
name='xmpp', name="xmpp",
field=models.EmailField( 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,
),
), ),
] ]

View file

@ -5,17 +5,22 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0014_auto_20180711_1248'), ("users", "0014_auto_20180711_1248"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='user', model_name="user",
name='openid_sha256', name="openid_sha256",
field=computed_property.fields.ComputedCharField(compute_from='calc_openid_sha256', default='', editable=False, field=computed_property.fields.ComputedCharField(
help_text="SHA-256 hash of the user's OpenID URL, used for Libravatar", max_length=64, unique=True), 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, preserve_default=False,
), ),
] ]

View file

@ -4,19 +4,16 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0015_user_openid_sha256'), ("users", "0015_user_openid_sha256"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='user', model_name="user",
name='first_name', name="first_name",
field=models.CharField( field=models.CharField(
blank=True, blank=True, max_length=150, verbose_name="first name"
max_length=150,
verbose_name='first name'
), ),
), ),
] ]

View file

@ -4,14 +4,13 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0016_alter_user_first_name'), ("users", "0016_alter_user_first_name"),
] ]
operations = [ operations = [
migrations.RenameModel( migrations.RenameModel(
old_name='Key', old_name="Key",
new_name='PgpKey', new_name="PgpKey",
), ),
] ]

View file

@ -10,7 +10,7 @@ from lemoncurry import utils
def avatar_path(instance, name): 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): class Site(models.Model):
@ -19,7 +19,7 @@ class Site(models.Model):
domain = models.CharField(max_length=100, blank=True) domain = models.CharField(max_length=100, blank=True)
url_template = models.CharField(max_length=100) 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) return self.url_template.format(domain=self.domain, username=username)
@property @property
@ -30,12 +30,14 @@ class Site(models.Model):
return self.name return self.name
class Meta: class Meta:
ordering = ('name',) ordering = ("name",)
class UserManager(DjangoUserManager): class UserManager(DjangoUserManager):
def get_queryset(self): 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): class User(ModelMeta, AbstractUser):
@ -44,52 +46,56 @@ class User(ModelMeta, AbstractUser):
generated based on all their associated information and may author as many generated based on all their associated information and may author as many
h-entries (:model:`entries.Entry`) as they wish. h-entries (:model:`entries.Entry`) as they wish.
""" """
objects = UserManager() objects = UserManager()
avatar = models.ImageField( avatar = models.ImageField(
upload_to=avatar_path, upload_to=avatar_path, help_text="an avatar or photo that represents this user"
help_text='an avatar or photo that represents this user'
) )
note = models.TextField( note = models.TextField(
blank=True, blank=True, help_text="a bio or short description provided by the user"
help_text='a bio or short description provided by the user'
) )
xmpp = models.EmailField( xmpp = models.EmailField(
blank=True, blank=True, help_text="an XMPP address through which the user may be reached"
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 ;) # This is gonna need to change if I ever decide to add multiple-user support ;)
url = '/' url = "/"
email_md5 = ComputedCharField( email_md5 = ComputedCharField(
compute_from='calc_email_md5', max_length=32, unique=True, compute_from="calc_email_md5",
help_text="MD5 hash of the user's email, used for Libravatar" max_length=32,
unique=True,
help_text="MD5 hash of the user's email, used for Libravatar",
) )
email_sha256 = ComputedCharField( email_sha256 = ComputedCharField(
compute_from='calc_email_sha256', max_length=64, unique=True, compute_from="calc_email_sha256",
help_text="SHA-256 hash of the user's email, used for Libravatar" max_length=64,
unique=True,
help_text="SHA-256 hash of the user's email, used for Libravatar",
) )
openid_sha256 = ComputedCharField( openid_sha256 = ComputedCharField(
compute_from='calc_openid_sha256', max_length=64, unique=True, compute_from="calc_openid_sha256",
help_text="SHA-256 hash of the user's OpenID URL, used for Libravatar" max_length=64,
unique=True,
help_text="SHA-256 hash of the user's OpenID URL, used for Libravatar",
) )
@property @property
def calc_email_md5(self): def calc_email_md5(self):
return md5(self.email.lower().encode('utf-8')).hexdigest() return md5(self.email.lower().encode("utf-8")).hexdigest()
@property @property
def calc_email_sha256(self): def calc_email_sha256(self):
return sha256(self.email.lower().encode('utf-8')).hexdigest() return sha256(self.email.lower().encode("utf-8")).hexdigest()
@property @property
def calc_openid_sha256(self): def calc_openid_sha256(self):
return sha256(self.full_url.encode('utf-8')).hexdigest() return sha256(self.full_url.encode("utf-8")).hexdigest()
@property @property
def name(self): 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): def get_absolute_url(self):
return self.absolute_url return self.absolute_url
@ -100,7 +106,7 @@ class User(ModelMeta, AbstractUser):
@property @property
def full_url(self): def full_url(self):
base = 'https://' + DjangoSite.objects.get_current().domain base = "https://" + DjangoSite.objects.get_current().domain
return urljoin(base, self.url) return urljoin(base, self.url)
@property @property
@ -114,45 +120,45 @@ class User(ModelMeta, AbstractUser):
@cached_property @cached_property
def facebook_id(self): def facebook_id(self):
for p in self.profiles.all(): for p in self.profiles.all():
if p.site.name == 'Facebook': if p.site.name == "Facebook":
return p.username return p.username
return None return None
@cached_property @cached_property
def twitter_username(self): def twitter_username(self):
for p in self.profiles.all(): for p in self.profiles.all():
if p.site.name == 'Twitter': if p.site.name == "Twitter":
return '@' + p.username return "@" + p.username
return None return None
@property @property
def json_ld(self): def json_ld(self):
base = 'https://' + DjangoSite.objects.get_current().domain base = "https://" + DjangoSite.objects.get_current().domain
return { return {
'@context': 'http://schema.org', "@context": "http://schema.org",
'@type': 'Person', "@type": "Person",
'@id': self.full_url, "@id": self.full_url,
'url': self.full_url, "url": self.full_url,
'name': self.name, "name": self.name,
'email': self.email, "email": self.email,
'image': urljoin(base, self.avatar.url), "image": urljoin(base, self.avatar.url),
'givenName': self.first_name, "givenName": self.first_name,
'familyName': self.last_name, "familyName": self.last_name,
'sameAs': [profile.url for profile in self.profiles.all()] "sameAs": [profile.url for profile in self.profiles.all()],
} }
_metadata = { _metadata = {
'image': 'avatar_url', "image": "avatar_url",
'description': 'description', "description": "description",
'og_type': 'profile', "og_type": "profile",
'og_profile_id': 'facebook_id', "og_profile_id": "facebook_id",
'twitter_creator': 'twitter_username', "twitter_creator": "twitter_username",
} }
class ProfileManager(models.Manager): class ProfileManager(models.Manager):
def get_queryset(self): 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): class Profile(models.Model):
@ -163,26 +169,22 @@ class Profile(models.Model):
representative h-card. Additionally, :model:`entries.Syndication` is representative h-card. Additionally, :model:`entries.Syndication` is
tracked by linking each syndication to a particular profile. tracked by linking each syndication to a particular profile.
""" """
objects = ProfileManager() objects = ProfileManager()
user = models.ForeignKey( user = models.ForeignKey(User, related_name="profiles", on_delete=models.CASCADE)
User,
related_name='profiles',
on_delete=models.CASCADE
)
site = models.ForeignKey(Site, on_delete=models.CASCADE) site = models.ForeignKey(Site, on_delete=models.CASCADE)
username = models.CharField( username = models.CharField(
max_length=100, max_length=100, help_text="the user's actual handle or ID on the remote site"
help_text="the user's actual handle or ID on the remote site"
) )
display_name = models.CharField( display_name = models.CharField(
max_length=100, max_length=100,
blank=True, 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): def __str__(self):
if self.site.domain: if self.site.domain:
return self.name + '@' + self.site.domain return self.name + "@" + self.site.domain
return self.name return self.name
@property @property
@ -194,7 +196,7 @@ class Profile(models.Model):
return self.site.format(username=self.username) return self.site.format(username=self.username)
class Meta: class Meta:
ordering = ('site', 'username') ordering = ("site", "username")
class PgpKey(models.Model): 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 key will be added to the user's h-card with rel="pgpkey", a format
compatible with IndieAuth.com. compatible with IndieAuth.com.
""" """
user = models.ForeignKey(
User, user = models.ForeignKey(User, related_name="keys", on_delete=models.CASCADE)
related_name='keys',
on_delete=models.CASCADE
)
fingerprint = models.CharField(max_length=40) fingerprint = models.CharField(max_length=40)
file = models.FileField(upload_to='keys') file = models.FileField(upload_to="keys")
@property @property
def key_id(self): def key_id(self):

View file

@ -2,7 +2,5 @@ from django.urls import re_path
from .views import libravatar from .views import libravatar
app_name = 'users' app_name = "users"
urlpatterns = ( urlpatterns = (re_path("^avatar/(?P<hash>[a-z0-9]+)$", libravatar, name="libravatar"),)
re_path('^avatar/(?P<hash>[a-z0-9]+)$', libravatar, name='libravatar'),
)

View file

@ -8,16 +8,16 @@ from .models import User
def try_libravatar_org(hash, get): def try_libravatar_org(hash, get):
url = 'https://seccdn.libravatar.org/avatar/' + hash url = "https://seccdn.libravatar.org/avatar/" + hash
if get: if get:
url += '?' + get.urlencode() url += "?" + get.urlencode()
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@cache_page(60 * 15) @cache_page(60 * 15)
def libravatar(request, hash): def libravatar(request, hash):
g = request.GET g = request.GET
size = g.get('s', g.get('size', 80)) size = g.get("s", g.get("size", 80))
try: try:
size = int(size) size = int(size)
except ValueError: except ValueError:
@ -30,7 +30,7 @@ def libravatar(request, hash):
elif len(hash) == 64: elif len(hash) == 64:
where = Q(email_sha256=hash) | Q(openid_sha256=hash) where = Q(email_sha256=hash) | Q(openid_sha256=hash)
else: 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 # 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 # 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.crop((0, 0, natural_size, natural_size))
im = im.resize((size, size), resample=Image.HAMMING) 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) im.save(response, image_type)
return response return response

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class WebmentionConfig(AppConfig): class WebmentionConfig(AppConfig):
name = 'webmention' name = "webmention"

View file

@ -9,31 +9,71 @@ import model_utils.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('entries', '0011_auto_20171120_1108'), ("entries", "0011_auto_20171120_1108"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Webmention', name="Webmention",
fields=[ 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')), "id",
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), models.AutoField(
('source', models.CharField(max_length=255)), auto_created=True,
('target', models.CharField(max_length=255)), primary_key=True,
('state', models.CharField(choices=[('p', 'pending'), ('v', 'valid'), ('i', 'invalid'), ('d', 'deleted')], default='p', max_length=1)), serialize=False,
('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mentions', to='entries.Entry')), 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={ options={
'default_related_name': 'mentions', "default_related_name": "mentions",
}, },
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='webmention', name="webmention",
unique_together=set([('source', 'target')]), unique_together=set([("source", "target")]),
), ),
] ]

View file

@ -4,15 +4,15 @@ from model_utils.models import TimeStampedModel
class State: class State:
PENDING = 'p' PENDING = "p"
VALID = 'v' VALID = "v"
INVALID = 'i' INVALID = "i"
DELETED = 'd' DELETED = "d"
CHOICES = ( CHOICES = (
(PENDING, 'pending'), (PENDING, "pending"),
(VALID, 'valid'), (VALID, "valid"),
(INVALID, 'invalid'), (INVALID, "invalid"),
(DELETED, 'deleted'), (DELETED, "deleted"),
) )
@ -20,12 +20,8 @@ class Webmention(TimeStampedModel):
entry = models.ForeignKey(Entry, on_delete=models.CASCADE) entry = models.ForeignKey(Entry, on_delete=models.CASCADE)
source = models.CharField(max_length=255) source = models.CharField(max_length=255)
target = models.CharField(max_length=255) target = models.CharField(max_length=255)
state = models.CharField( state = models.CharField(choices=State.CHOICES, default=State.PENDING, max_length=1)
choices=State.CHOICES,
default=State.PENDING,
max_length=1
)
class Meta: class Meta:
default_related_name = 'mentions' default_related_name = "mentions"
unique_together = ('source', 'target') unique_together = ("source", "target")

View file

@ -1,8 +1,8 @@
from django.urls import path from django.urls import path
from . import views from . import views
app_name = 'webmention' app_name = "webmention"
urlpatterns = ( urlpatterns = (
path('s', views.accept, name='accept'), path("s", views.accept, name="accept"),
path('s/<int:mention_id>', views.status, name='status') path("s/<int:mention_id>", views.status, name="status"),
) )

View file

@ -13,36 +13,36 @@ from .models import State, Webmention
@csrf_exempt @csrf_exempt
@require_POST @require_POST
def accept(request): def accept(request):
if 'source' not in request.POST: if "source" not in request.POST:
return bad_req('missing source url') return bad_req("missing source url")
source_url = request.POST['source'] source_url = request.POST["source"]
if 'target' not in request.POST: if "target" not in request.POST:
return bad_req('missing target url') return bad_req("missing target url")
target_url = request.POST['target'] target_url = request.POST["target"]
source = urlparse(source_url) source = urlparse(source_url)
target = urlparse(target_url) target = urlparse(target_url)
if source.scheme not in ('http', 'https'): if source.scheme not in ("http", "https"):
return bad_req('unsupported source scheme') return bad_req("unsupported source scheme")
if target.scheme not in ('http', 'https'): if target.scheme not in ("http", "https"):
return bad_req('unsupported target scheme') return bad_req("unsupported target scheme")
if target.netloc != request.site.domain: if target.netloc != request.site.domain:
return bad_req('target not on this site') return bad_req("target not on this site")
origin = 'https://' + target.netloc origin = "https://" + target.netloc
try: try:
match = resolve(target.path) match = resolve(target.path)
except Resolver404: except Resolver404:
return bad_req('target not found') return bad_req("target not found")
if match.view_name != 'entries:entry': if match.view_name != "entries:entry":
return bad_req('target does not accept webmentions') return bad_req("target does not accept webmentions")
try: try:
entry = Entry.objects.get(pk=match.kwargs['id']) entry = Entry.objects.get(pk=match.kwargs["id"])
except Entry.DoesNotExist: except Entry.DoesNotExist:
return bad_req('target not found') return bad_req("target not found")
try: try:
mention = Webmention.objects.get(source=source_url, target=target_url) mention = Webmention.objects.get(source=source_url, target=target_url)
@ -54,10 +54,10 @@ def accept(request):
mention.entry = entry mention.entry = entry
mention.state = State.PENDING mention.state = State.PENDING
mention.save() 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 = HttpResponse(status=201)
res['Location'] = urljoin(origin, status_url) res["Location"] = urljoin(origin, status_url)
return res return res

Some files were not shown because too many files have changed in this diff Show more