Compare commits
7 commits
5ac46dad63
...
2e7d12b3e6
Author | SHA1 | Date | |
---|---|---|---|
2e7d12b3e6 | |||
cd990e4e2f | |||
fe187da491 | |||
636b470001 | |||
e5cf94d488 | |||
c5458c2d06 | |||
7af8636687 |
114 changed files with 2950 additions and 2769 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -18,5 +18,6 @@ media
|
||||||
/.env
|
/.env
|
||||||
/.mypy_cache
|
/.mypy_cache
|
||||||
/.pytest_cache
|
/.pytest_cache
|
||||||
|
/*.egg-info/
|
||||||
/static
|
/static
|
||||||
node_modules
|
node_modules
|
||||||
|
|
|
@ -1,30 +1,41 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v2.1.0
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-byte-order-marker
|
- id: check-ast
|
||||||
|
- id: check-builtin-literals
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
|
- id: check-docstring-first
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
- id: check-json
|
- id: check-json
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
|
- id: check-symlinks
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-vcs-permalinks
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
- id: destroyed-symlinks
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: flake8
|
- id: fix-byte-order-marker
|
||||||
- id: mixed-line-ending
|
- id: mixed-line-ending
|
||||||
args:
|
args:
|
||||||
- --fix=lf
|
- --fix=lf
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 23.7.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
language_version: python3.11
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: pytest
|
- id: pytest
|
||||||
name: Check pytest unit tests pass
|
name: Check pytest unit tests pass
|
||||||
entry: pipenv run pytest
|
entry: poetry run pytest
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: system
|
language: system
|
||||||
types: [python]
|
types: [python]
|
||||||
- id: mypy
|
- id: mypy
|
||||||
name: Check mypy static types match
|
name: Check mypy static types match
|
||||||
entry: pipenv run mypy . --ignore-missing-imports
|
entry: poetry run mypy . --ignore-missing-imports
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: system
|
language: system
|
||||||
types: [python]
|
types: [python]
|
||||||
|
|
1496
Pipfile.lock
generated
1496
Pipfile.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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)
|
||||||
|
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class EntriesConfig(AppConfig):
|
class EntriesConfig(AppConfig):
|
||||||
name = 'entries'
|
name = "entries"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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"],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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",),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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')
|
|
||||||
|
|
|
@ -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)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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 = [] # type: 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,110 +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",
|
||||||
'favicon',
|
"home",
|
||||||
'meta',
|
"lemonauth",
|
||||||
|
"lemonshort",
|
||||||
'entries',
|
"micropub",
|
||||||
'home',
|
"users",
|
||||||
'lemonauth',
|
"webmention",
|
||||||
'lemonshort',
|
"wellknowns",
|
||||||
'micropub',
|
|
||||||
'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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,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
|
||||||
|
|
||||||
|
@ -225,21 +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/
|
||||||
|
@ -251,28 +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"}}
|
||||||
|
|
||||||
# django-super-favicon
|
|
||||||
FAVICON_STORAGE = 'django.core.files.storage.DefaultStorage'
|
|
||||||
|
|
|
@ -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/"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
|
@ -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)),)
|
||||||
)
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class LemonshortConfig(AppConfig):
|
class LemonshortConfig(AppConfig):
|
||||||
name = 'lemonshort'
|
name = "lemonshort"
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class MicropubConfig(AppConfig):
|
class MicropubConfig(AppConfig):
|
||||||
name = 'micropub'
|
name = "micropub"
|
||||||
|
|
|
@ -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(
|
||||||
status: Optional[int]=400,
|
error: str,
|
||||||
msg: Optional[str]=None,
|
status: Optional[int] = 400,
|
||||||
scope: Optional[str]=None):
|
msg: Optional[str] = None,
|
||||||
content = {'error': error}
|
scope: Optional[str] = None,
|
||||||
|
):
|
||||||
|
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))
|
||||||
|
|
|
@ -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"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
1317
poetry.lock
generated
Normal file
1317
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,63 +1,66 @@
|
||||||
[[source]]
|
[tool.poetry]
|
||||||
url = "https://pypi.python.org/simple"
|
name = "lemoncurry"
|
||||||
verify_ssl = true
|
version = "1.11.0"
|
||||||
name = "pypi"
|
description = "Indieweb-compatible personal website"
|
||||||
|
authors = ["Danielle McLean <dani@00dani.me>"]
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
[requires]
|
[tool.poetry.dependencies]
|
||||||
python_version = '3.9'
|
python = "^3.9"
|
||||||
|
|
||||||
[packages]
|
|
||||||
django = ">=3,<4"
|
|
||||||
django-compressor = "*"
|
|
||||||
gunicorn = {extras = ["gevent"]}
|
|
||||||
"psycopg2-binary" = "*"
|
|
||||||
pillow = "*"
|
|
||||||
django-meta = "*"
|
|
||||||
django-activeurl = "*"
|
|
||||||
django-otp = "*"
|
|
||||||
qrcode = "*"
|
|
||||||
django-otp-agents = "*"
|
|
||||||
python-slugify = "*"
|
|
||||||
"mf2py" = "*"
|
|
||||||
markdown = "*"
|
|
||||||
bleach = "*"
|
|
||||||
django-debug-toolbar = "*"
|
|
||||||
xrd = "*"
|
|
||||||
django-push = "*"
|
|
||||||
pyyaml = "*"
|
|
||||||
django-annoying = "*"
|
|
||||||
accept-types = "*"
|
accept-types = "*"
|
||||||
django-analytical = "*"
|
|
||||||
django-model-utils = "*"
|
|
||||||
django-rq = "*"
|
|
||||||
ronkyuu = "*"
|
|
||||||
cachecontrol = "*"
|
|
||||||
hiredis = "*"
|
|
||||||
"mf2util" = "*"
|
|
||||||
django-cors-headers = "*"
|
|
||||||
"argon2-cffi" = "*"
|
|
||||||
python-baseconv = "*"
|
|
||||||
django-computed-property = "*"
|
|
||||||
docutils = "*"
|
|
||||||
django-super-favicon = "*"
|
|
||||||
django-redis = "*"
|
|
||||||
gevent = "*"
|
|
||||||
django-extensions = "*"
|
|
||||||
python-magic = "*"
|
|
||||||
pyup-django = "*"
|
|
||||||
jinja2 = "*"
|
|
||||||
msgpack = "*"
|
|
||||||
django-randomslugfield = "*"
|
|
||||||
ago = "*"
|
ago = "*"
|
||||||
|
argon2-cffi = "*"
|
||||||
|
bleach = "*"
|
||||||
|
cachecontrol = "*"
|
||||||
|
django = "<4,>=3"
|
||||||
|
django-activeurl = "*"
|
||||||
|
django-analytical = "*"
|
||||||
|
django-annoying = "*"
|
||||||
|
django-compressor = "*"
|
||||||
|
django-computed-property = "*"
|
||||||
|
django-cors-headers = "*"
|
||||||
|
django-debug-toolbar = "*"
|
||||||
|
django-extensions = "*"
|
||||||
|
django-meta = "*"
|
||||||
|
django-model-utils = "*"
|
||||||
|
django-otp = "*"
|
||||||
|
django-otp-agents = "*"
|
||||||
|
django-push = "*"
|
||||||
|
django-randomslugfield = "*"
|
||||||
|
django-redis = "*"
|
||||||
|
django-rq = "*"
|
||||||
|
docutils = "*"
|
||||||
|
gevent = "*"
|
||||||
|
gunicorn = {extras = ["gevent"], version = "*"}
|
||||||
|
hiredis = "*"
|
||||||
|
jinja2 = "*"
|
||||||
|
markdown = "*"
|
||||||
|
mf2py = "*"
|
||||||
|
mf2util = "*"
|
||||||
|
msgpack = "*"
|
||||||
|
pillow = "*"
|
||||||
|
psycopg2-binary = "*"
|
||||||
|
python-baseconv = "*"
|
||||||
|
python-magic = "*"
|
||||||
|
python-slugify = "*"
|
||||||
|
pyup-django = "*"
|
||||||
|
pyyaml = "*"
|
||||||
|
qrcode = "*"
|
||||||
|
ronkyuu = "*"
|
||||||
|
xrd = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[tool.poetry.dev-dependencies]
|
||||||
|
mypy = "*"
|
||||||
ptpython = "*"
|
ptpython = "*"
|
||||||
pytest-django = "*"
|
pytest-django = "*"
|
||||||
werkzeug = "*"
|
|
||||||
watchdog = "*"
|
|
||||||
mypy = "*"
|
|
||||||
types-pyyaml = "*"
|
|
||||||
types-requests = "*"
|
|
||||||
types-python-slugify = "*"
|
|
||||||
types-bleach = "*"
|
types-bleach = "*"
|
||||||
types-markdown = "*"
|
types-markdown = "*"
|
||||||
|
types-python-slugify = "*"
|
||||||
|
types-pyyaml = "*"
|
||||||
|
types-requests = "*"
|
||||||
|
watchdog = "*"
|
||||||
|
werkzeug = "*"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry>=0.12"]
|
||||||
|
build-backend = "poetry.masonry.api"
|
|
@ -1,14 +1,14 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
from .models import Key, Profile, Site, User
|
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 KeyInline(admin.TabularInline):
|
class PgpKeyInline(admin.TabularInline):
|
||||||
model = Key
|
model = PgpKey
|
||||||
extra = 1
|
extra = 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,10 +19,10 @@ 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 = (
|
||||||
KeyInline,
|
PgpKeyInline,
|
||||||
ProfileInline,
|
ProfileInline,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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",)},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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",)},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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=[],
|
||||||
],
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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'
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
16
users/migrations/0017_rename_key_pgpkey.py
Normal file
16
users/migrations/0017_rename_key_pgpkey.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 3.2.20 on 2023-08-10 06:37
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("users", "0016_alter_user_first_name"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name="Key",
|
||||||
|
new_name="PgpKey",
|
||||||
|
),
|
||||||
|
]
|
119
users/models.py
119
users/models.py
|
@ -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,22 +196,19 @@ 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 Key(models.Model):
|
class PgpKey(models.Model):
|
||||||
"""
|
"""
|
||||||
Represents a PGP key that belongs to a particular :model:`users.User`. Each
|
Represents a PGP key that belongs to a particular :model:`users.User`. Each
|
||||||
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):
|
||||||
|
@ -231,4 +230,4 @@ class Key(models.Model):
|
||||||
same way GnuPG does. This can make reading the fingerprint a little
|
same way GnuPG does. This can make reading the fingerprint a little
|
||||||
friendlier.
|
friendlier.
|
||||||
"""
|
"""
|
||||||
return " ".join(self.fingerprint[i:i+4] for i in range(0, 40, 4))
|
return " ".join(self.fingerprint[i : i + 4] for i in range(0, 40, 4))
|
||||||
|
|
|
@ -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'),
|
|
||||||
)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue