From 81de10b70c85c5222b17d8c4358a8aa8812f2559 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Fri, 18 Nov 2022 08:28:15 -0700 Subject: Migration reset, start of docs, env vars --- .gitignore | 1 + activities/migrations/0001_initial.py | 239 ++++++++++++++++++++- activities/migrations/0002_fan_out.py | 103 --------- .../migrations/0003_alter_post_object_uri.py | 18 -- ...ed_post_published_alter_fanout_type_and_more.py | 126 ----------- ...005_post_hashtags_alter_fanout_type_and_more.py | 48 ----- activities/migrations/0006_alter_post_hashtags.py | 18 -- activities/migrations/0007_post_edited.py | 18 -- activities/migrations/0008_postattachment.py | 69 ------ .../migrations/0009_alter_postattachment_file.py | 28 --- core/apps.py | 3 - core/migrations/0001_initial.py | 14 +- core/migrations/0002_alter_config_image.py | 28 --- core/models/config.py | 2 +- docs/Makefile | 20 ++ docs/conf.py | 26 +++ docs/index.rst | 13 ++ docs/installation.rst | 76 +++++++ docs/make.bat | 35 +++ requirements.txt | 1 + static/css/style.css | 4 + stator/management/commands/runstator.py | 3 + stator/migrations/0001_initial.py | 2 +- takahe/settings/base.py | 59 ++++- takahe/urls.py | 5 +- templates/admin/domain_create.html | 2 + templates/admin/domain_edit.html | 2 + templates/admin/domains.html | 3 + templates/auth/login.html | 1 + templates/auth/perform_reset_success.html | 7 +- templates/settings/profile.html | 4 +- users/migrations/0001_initial.py | 79 ++++++- users/migrations/0002_identity_public_key_id.py | 18 -- .../0003_user_last_seen_alter_identity_domain.py | 34 --- users/migrations/0004_passwordreset.py | 60 ------ users/migrations/0005_invite.py | 32 --- users/models/domain.py | 7 +- users/models/password_reset.py | 2 +- users/views/activitypub.py | 59 ++++- users/views/admin/domains.py | 52 +++-- users/views/auth.py | 5 + users/views/identity.py | 32 ++- users/views/settings.py | 4 +- 43 files changed, 681 insertions(+), 681 deletions(-) delete mode 100644 activities/migrations/0002_fan_out.py delete mode 100644 activities/migrations/0003_alter_post_object_uri.py delete mode 100644 activities/migrations/0004_rename_authored_post_published_alter_fanout_type_and_more.py delete mode 100644 activities/migrations/0005_post_hashtags_alter_fanout_type_and_more.py delete mode 100644 activities/migrations/0006_alter_post_hashtags.py delete mode 100644 activities/migrations/0007_post_edited.py delete mode 100644 activities/migrations/0008_postattachment.py delete mode 100644 activities/migrations/0009_alter_postattachment_file.py delete mode 100644 core/migrations/0002_alter_config_image.py create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/installation.rst create mode 100644 docs/make.bat delete mode 100644 users/migrations/0002_identity_public_key_id.py delete mode 100644 users/migrations/0003_user_last_seen_alter_identity_domain.py delete mode 100644 users/migrations/0004_passwordreset.py delete mode 100644 users/migrations/0005_invite.py diff --git a/.gitignore b/.gitignore index 5f0eef3..7266b2f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ *.sqlite3 .venv /*.env +/docs/_build /media/ notes.md diff --git a/activities/migrations/0001_initial.py b/activities/migrations/0001_initial.py index 0b350ef..19f3026 100644 --- a/activities/migrations/0001_initial.py +++ b/activities/migrations/0001_initial.py @@ -1,9 +1,16 @@ -# Generated by Django 4.1.3 on 2022-11-11 20:02 +# Generated by Django 4.1.3 on 2022-11-18 17:49 + +import functools import django.db.models.deletion +import django.utils.timezone from django.db import migrations, models +import activities.models.fan_out import activities.models.post +import activities.models.post_attachment +import activities.models.post_interaction +import core.uploads import stator.models @@ -42,7 +49,12 @@ class Migration(migrations.Migration): ), ), ("local", models.BooleanField()), - ("object_uri", models.CharField(blank=True, max_length=500, null=True)), + ( + "object_uri", + models.CharField( + blank=True, max_length=500, null=True, unique=True + ), + ), ( "visibility", models.IntegerField( @@ -63,26 +75,222 @@ class Migration(migrations.Migration): "in_reply_to", models.CharField(blank=True, max_length=500, null=True), ), + ("hashtags", models.JSONField(blank=True, null=True)), + ("published", models.DateTimeField(default=django.utils.timezone.now)), + ("edited", models.DateTimeField(blank=True, null=True)), ("created", models.DateTimeField(auto_now_add=True)), ("updated", models.DateTimeField(auto_now=True)), ( "author", models.ForeignKey( on_delete=django.db.models.deletion.PROTECT, - related_name="statuses", + related_name="posts", to="users.identity", ), ), ( "mentions", models.ManyToManyField( - related_name="posts_mentioning", to="users.identity" + blank=True, related_name="posts_mentioning", to="users.identity" ), ), ( "to", models.ManyToManyField( - related_name="posts_to", to="users.identity" + blank=True, related_name="posts_to", to="users.identity" + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="PostInteraction", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("state_ready", models.BooleanField(default=True)), + ("state_changed", models.DateTimeField(auto_now_add=True)), + ("state_attempted", models.DateTimeField(blank=True, null=True)), + ("state_locked_until", models.DateTimeField(blank=True, null=True)), + ( + "state", + stator.models.StateField( + choices=[ + ("new", "new"), + ("fanned_out", "fanned_out"), + ("undone", "undone"), + ("undone_fanned_out", "undone_fanned_out"), + ], + default="new", + graph=activities.models.post_interaction.PostInteractionStates, + max_length=100, + ), + ), + ( + "object_uri", + models.CharField( + blank=True, max_length=500, null=True, unique=True + ), + ), + ( + "type", + models.CharField( + choices=[("like", "Like"), ("boost", "Boost")], max_length=100 + ), + ), + ("published", models.DateTimeField(default=django.utils.timezone.now)), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ( + "identity", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="interactions", + to="users.identity", + ), + ), + ( + "post", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="interactions", + to="activities.post", + ), + ), + ], + options={ + "index_together": {("type", "identity", "post")}, + }, + ), + migrations.CreateModel( + name="PostAttachment", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("state_ready", models.BooleanField(default=True)), + ("state_changed", models.DateTimeField(auto_now_add=True)), + ("state_attempted", models.DateTimeField(blank=True, null=True)), + ("state_locked_until", models.DateTimeField(blank=True, null=True)), + ( + "state", + stator.models.StateField( + choices=[("new", "new"), ("fetched", "fetched")], + default="new", + graph=activities.models.post_attachment.PostAttachmentStates, + max_length=100, + ), + ), + ("mimetype", models.CharField(max_length=200)), + ( + "file", + models.FileField( + blank=True, + null=True, + upload_to=functools.partial( + core.uploads.upload_namer, *("attachments",), **{} + ), + ), + ), + ("remote_url", models.CharField(blank=True, max_length=500, null=True)), + ("name", models.TextField(blank=True, null=True)), + ("width", models.IntegerField(blank=True, null=True)), + ("height", models.IntegerField(blank=True, null=True)), + ("focal_x", models.IntegerField(blank=True, null=True)), + ("focal_y", models.IntegerField(blank=True, null=True)), + ("blurhash", models.TextField(blank=True, null=True)), + ( + "post", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="attachments", + to="activities.post", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="FanOut", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("state_ready", models.BooleanField(default=True)), + ("state_changed", models.DateTimeField(auto_now_add=True)), + ("state_attempted", models.DateTimeField(blank=True, null=True)), + ("state_locked_until", models.DateTimeField(blank=True, null=True)), + ( + "state", + stator.models.StateField( + choices=[("new", "new"), ("sent", "sent")], + default="new", + graph=activities.models.fan_out.FanOutStates, + max_length=100, + ), + ), + ( + "type", + models.CharField( + choices=[ + ("post", "Post"), + ("interaction", "Interaction"), + ("undo_interaction", "Undo Interaction"), + ], + max_length=100, + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ( + "identity", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="fan_outs", + to="users.identity", + ), + ), + ( + "subject_post", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="fan_outs", + to="activities.post", + ), + ), + ( + "subject_post_interaction", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="fan_outs", + to="activities.postinteraction", ), ), ], @@ -107,10 +315,11 @@ class Migration(migrations.Migration): models.CharField( choices=[ ("post", "Post"), - ("mention", "Mention"), - ("like", "Like"), - ("follow", "Follow"), ("boost", "Boost"), + ("mentioned", "Mentioned"), + ("liked", "Liked"), + ("followed", "Followed"), + ("boosted", "Boosted"), ], max_length=100, ), @@ -140,15 +349,25 @@ class Migration(migrations.Migration): blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, - related_name="timeline_events_about_us", + related_name="timeline_events", to="activities.post", ), ), + ( + "subject_post_interaction", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="timeline_events", + to="activities.postinteraction", + ), + ), ], options={ "index_together": { - ("identity", "type", "subject_post", "subject_identity"), ("identity", "type", "subject_identity"), + ("identity", "type", "subject_post", "subject_identity"), }, }, ), diff --git a/activities/migrations/0002_fan_out.py b/activities/migrations/0002_fan_out.py deleted file mode 100644 index f3b626e..0000000 --- a/activities/migrations/0002_fan_out.py +++ /dev/null @@ -1,103 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-12 05:36 - -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - -import activities.models.fan_out -import stator.models - - -class Migration(migrations.Migration): - - dependencies = [ - ("users", "0001_initial"), - ("activities", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="post", - name="authored", - field=models.DateTimeField(default=django.utils.timezone.now), - ), - migrations.AlterField( - model_name="post", - name="author", - field=models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="posts", - to="users.identity", - ), - ), - migrations.AlterField( - model_name="post", - name="mentions", - field=models.ManyToManyField( - blank=True, related_name="posts_mentioning", to="users.identity" - ), - ), - migrations.AlterField( - model_name="post", - name="to", - field=models.ManyToManyField( - blank=True, related_name="posts_to", to="users.identity" - ), - ), - migrations.CreateModel( - name="FanOut", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("state_ready", models.BooleanField(default=True)), - ("state_changed", models.DateTimeField(auto_now_add=True)), - ("state_attempted", models.DateTimeField(blank=True, null=True)), - ("state_locked_until", models.DateTimeField(blank=True, null=True)), - ( - "state", - stator.models.StateField( - choices=[("new", "new"), ("sent", "sent")], - default="new", - graph=activities.models.fan_out.FanOutStates, - max_length=100, - ), - ), - ( - "type", - models.CharField( - choices=[("post", "Post"), ("boost", "Boost")], max_length=100 - ), - ), - ("created", models.DateTimeField(auto_now_add=True)), - ("updated", models.DateTimeField(auto_now=True)), - ( - "identity", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="fan_outs", - to="users.identity", - ), - ), - ( - "subject_post", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="fan_outs", - to="activities.post", - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/activities/migrations/0003_alter_post_object_uri.py b/activities/migrations/0003_alter_post_object_uri.py deleted file mode 100644 index 4f98bc9..0000000 --- a/activities/migrations/0003_alter_post_object_uri.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-13 03:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("activities", "0002_fan_out"), - ] - - operations = [ - migrations.AlterField( - model_name="post", - name="object_uri", - field=models.CharField(blank=True, max_length=500, null=True, unique=True), - ), - ] diff --git a/activities/migrations/0004_rename_authored_post_published_alter_fanout_type_and_more.py b/activities/migrations/0004_rename_authored_post_published_alter_fanout_type_and_more.py deleted file mode 100644 index 7972f18..0000000 --- a/activities/migrations/0004_rename_authored_post_published_alter_fanout_type_and_more.py +++ /dev/null @@ -1,126 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-14 00:41 - -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - -import activities.models.post_interaction -import stator.models - - -class Migration(migrations.Migration): - - dependencies = [ - ("users", "0002_identity_public_key_id"), - ("activities", "0003_alter_post_object_uri"), - ] - - operations = [ - migrations.RenameField( - model_name="post", - old_name="authored", - new_name="published", - ), - migrations.AlterField( - model_name="fanout", - name="type", - field=models.CharField( - choices=[("post", "Post"), ("interaction", "Interaction")], - max_length=100, - ), - ), - migrations.AlterField( - model_name="timelineevent", - name="subject_post", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="timeline_events", - to="activities.post", - ), - ), - migrations.CreateModel( - name="PostInteraction", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("state_ready", models.BooleanField(default=True)), - ("state_changed", models.DateTimeField(auto_now_add=True)), - ("state_attempted", models.DateTimeField(blank=True, null=True)), - ("state_locked_until", models.DateTimeField(blank=True, null=True)), - ( - "state", - stator.models.StateField( - choices=[("new", "new"), ("fanned_out", "fanned_out")], - default="new", - graph=activities.models.post_interaction.PostInteractionStates, - max_length=100, - ), - ), - ( - "object_uri", - models.CharField( - blank=True, max_length=500, null=True, unique=True - ), - ), - ( - "type", - models.CharField( - choices=[("like", "Like"), ("boost", "Boost")], max_length=100 - ), - ), - ("published", models.DateTimeField(default=django.utils.timezone.now)), - ("created", models.DateTimeField(auto_now_add=True)), - ("updated", models.DateTimeField(auto_now=True)), - ( - "identity", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="interactions", - to="users.identity", - ), - ), - ( - "post", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="interactions", - to="activities.post", - ), - ), - ], - options={ - "index_together": {("type", "identity", "post")}, - }, - ), - migrations.AddField( - model_name="fanout", - name="subject_post_interaction", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="fan_outs", - to="activities.postinteraction", - ), - ), - migrations.AddField( - model_name="timelineevent", - name="subject_post_interaction", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="timeline_events", - to="activities.postinteraction", - ), - ), - ] diff --git a/activities/migrations/0005_post_hashtags_alter_fanout_type_and_more.py b/activities/migrations/0005_post_hashtags_alter_fanout_type_and_more.py deleted file mode 100644 index 07d5cca..0000000 --- a/activities/migrations/0005_post_hashtags_alter_fanout_type_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-16 20:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ( - "activities", - "0004_rename_authored_post_published_alter_fanout_type_and_more", - ), - ] - - operations = [ - migrations.AddField( - model_name="post", - name="hashtags", - field=models.JSONField(default=[]), - ), - migrations.AlterField( - model_name="fanout", - name="type", - field=models.CharField( - choices=[ - ("post", "Post"), - ("interaction", "Interaction"), - ("undo_interaction", "Undo Interaction"), - ], - max_length=100, - ), - ), - migrations.AlterField( - model_name="timelineevent", - name="type", - field=models.CharField( - choices=[ - ("post", "Post"), - ("boost", "Boost"), - ("mentioned", "Mentioned"), - ("liked", "Liked"), - ("followed", "Followed"), - ("boosted", "Boosted"), - ], - max_length=100, - ), - ), - ] diff --git a/activities/migrations/0006_alter_post_hashtags.py b/activities/migrations/0006_alter_post_hashtags.py deleted file mode 100644 index b6149ea..0000000 --- a/activities/migrations/0006_alter_post_hashtags.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-17 04:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("activities", "0005_post_hashtags_alter_fanout_type_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="post", - name="hashtags", - field=models.JSONField(blank=True, null=True), - ), - ] diff --git a/activities/migrations/0007_post_edited.py b/activities/migrations/0007_post_edited.py deleted file mode 100644 index d4a661f..0000000 --- a/activities/migrations/0007_post_edited.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-17 04:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("activities", "0006_alter_post_hashtags"), - ] - - operations = [ - migrations.AddField( - model_name="post", - name="edited", - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/activities/migrations/0008_postattachment.py b/activities/migrations/0008_postattachment.py deleted file mode 100644 index 168ed58..0000000 --- a/activities/migrations/0008_postattachment.py +++ /dev/null @@ -1,69 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-17 05:42 - -import django.db.models.deletion -from django.db import migrations, models - -import activities.models.post_attachment -import stator.models - - -class Migration(migrations.Migration): - - dependencies = [ - ("activities", "0007_post_edited"), - ] - - operations = [ - migrations.CreateModel( - name="PostAttachment", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("state_ready", models.BooleanField(default=True)), - ("state_changed", models.DateTimeField(auto_now_add=True)), - ("state_attempted", models.DateTimeField(blank=True, null=True)), - ("state_locked_until", models.DateTimeField(blank=True, null=True)), - ( - "state", - stator.models.StateField( - choices=[("new", "new"), ("fetched", "fetched")], - default="new", - graph=activities.models.post_attachment.PostAttachmentStates, - max_length=100, - ), - ), - ("mimetype", models.CharField(max_length=200)), - ( - "file", - models.FileField( - blank=True, null=True, upload_to="attachments/%Y/%m/%d/" - ), - ), - ("remote_url", models.CharField(blank=True, max_length=500, null=True)), - ("name", models.TextField(blank=True, null=True)), - ("width", models.IntegerField(blank=True, null=True)), - ("height", models.IntegerField(blank=True, null=True)), - ("focal_x", models.IntegerField(blank=True, null=True)), - ("focal_y", models.IntegerField(blank=True, null=True)), - ("blurhash", models.TextField(blank=True, null=True)), - ( - "post", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="attachments", - to="activities.post", - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/activities/migrations/0009_alter_postattachment_file.py b/activities/migrations/0009_alter_postattachment_file.py deleted file mode 100644 index 0a250c3..0000000 --- a/activities/migrations/0009_alter_postattachment_file.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-18 01:40 - -import functools - -from django.db import migrations, models - -import core.uploads - - -class Migration(migrations.Migration): - - dependencies = [ - ("activities", "0008_postattachment"), - ] - - operations = [ - migrations.AlterField( - model_name="postattachment", - name="file", - field=models.FileField( - blank=True, - null=True, - upload_to=functools.partial( - core.uploads.upload_namer, *("attachments",), **{} - ), - ), - ), - ] diff --git a/core/apps.py b/core/apps.py index 54693d5..6098f6b 100644 --- a/core/apps.py +++ b/core/apps.py @@ -9,7 +9,4 @@ class CoreConfig(AppConfig): name = "core" def ready(self) -> None: - from core.models import Config - - Config.system = Config.load_system() jsonld.set_document_loader(builtin_document_loader) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index 2c4731f..900260f 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -1,16 +1,20 @@ -# Generated by Django 4.1.3 on 2022-11-16 21:23 +# Generated by Django 4.1.3 on 2022-11-18 17:49 + +import functools import django.db.models.deletion from django.conf import settings from django.db import migrations, models +import core.uploads + class Migration(migrations.Migration): initial = True dependencies = [ - ("users", "0002_identity_public_key_id"), + ("users", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -32,7 +36,11 @@ class Migration(migrations.Migration): ( "image", models.ImageField( - blank=True, null=True, upload_to="config/%Y/%m/%d/" + blank=True, + null=True, + upload_to=functools.partial( + core.uploads.upload_namer, *("config",), **{} + ), ), ), ( diff --git a/core/migrations/0002_alter_config_image.py b/core/migrations/0002_alter_config_image.py deleted file mode 100644 index 86dcebb..0000000 --- a/core/migrations/0002_alter_config_image.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-18 01:40 - -import functools - -from django.db import migrations, models - -import core.uploads - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="config", - name="image", - field=models.ImageField( - blank=True, - null=True, - upload_to=functools.partial( - core.uploads.upload_namer, *("config",), **{} - ), - ), - ), - ] diff --git a/core/models/config.py b/core/models/config.py index 2a27d2e..57d9e55 100644 --- a/core/models/config.py +++ b/core/models/config.py @@ -160,7 +160,7 @@ class Config(models.Model): site_icon: UploadedImage = static("img/icon-128.png") site_banner: UploadedImage = static("img/fjords-banner-600.jpg") - signup_allowed: bool = False + signup_allowed: bool = True signup_invite_only: bool = False signup_text: str = "" diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..73e4692 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,26 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "Takahē" +copyright = "2022, Andrew Godwin" +author = "Andrew Godwin" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions: list = [] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "alabaster" +html_static_path = ["_static"] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..a50c369 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,13 @@ +Takahē +====== + + +Welcome to the Takahē documentation! Takahē is an ActivityPub server, designed +for low- to medium-size installations, and with the ability to serve multiple +domains at once. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + installation diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..9c39a9d --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,76 @@ +Installation +============ + +We recommend running using the Docker/OCI image; this contains all of the +necessary dependencies and static file handling preconfigured for you. + +All configuration is done via either environment variables, or online through +the web interface. + + +Prerequisites +------------- + +* SSL support (Takahē *requires* HTTPS) +* Something that can run Docker/OCI images ("serverless" platforms are fine!) +* A PostgreSQL 14 (or above) database +* One of these to store uploaded images and media: + * Amazon S3 + * Google Cloud Storage + * Writable local directory (must be accessible by all running copies!) + + +Environment Variables +--------------------- + +All of these variables are *required* for a working installation, and should +be provided from the first boot. + +* ``PGHOST``, ``PGPORT``, ``PGUSER``, ``PGDATABASE``, and ``PGPASSWORD`` are the + standard PostgreSQL environment variables for configuring your database. + +* ``TAKAHE_MEDIA_BACKEND`` must be one of ``local``, ``s3`` or ``gcs``. + + * If it is set to ``local``, you must also provide ``TAKAHE_MEDIA_ROOT``, + the path to the local media directory, and ``TAKAHE_MEDIA_URL``, a + fully-qualified URL prefix that serves that directory. + + * If it is set to ``gcs``, you must also provide ``TAKAHE_MEDIA_BUCKET``, + the name of the bucket to store files in. + + * If it is set to ``s3``, you must also provide ``TAKAHE_MEDIA_BUCKET``, + the name of the bucket to store files in. + +* ``TAKAHE_MAIN_DOMAIN`` should be the domain name (without ``https://``) that + will be used for default links (such as in emails). It does *not* need to be + the same as any domain you are hosting user accounts on. + +* ``TAKAHE_EMAIL_HOST`` and ``TAKAHE_EMAIL_PORT`` (along with + ``TAKAHE_EMAIL_USER`` and ``TAKAHE_EMAIL_PASSWORD``, if needed) should point + to an SMTP server Takahe can use for sending email. Email is *required*, to + allow account creation and password resets. + + * If you are using SendGrid, you can just set an API key in + ``TAKAHE_EMAIL_SENDGRID_KEY`` instead. + +* ``TAKAHE_EMAIL_FROM`` is the email address that emails from the system will + appear to come from. + +* ``TAKAHE_AUTO_ADMIN_EMAIL`` should be an email address that you would like to + be automatically promoted to administrator when it signs up. You only need + this for initial setup, and can unset it after that if you like. + + +Making An Admin Account +----------------------- + +Once the webserver is up and working, go to the "create account" flow and +create a new account using the email you specified in +``TAKAHE_AUTO_ADMIN_EMAIL``. + +Once you set your password using the link emailed to you, you will have an +admin account. + +If your email settings have a problem and you don't get the email, don't worry; +fix them and then follow the "reset my password" flow on the login screen, and +you'll get another password reset email that you can use. diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..954237b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/requirements.txt b/requirements.txt index ce82854..3b0cb1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ psycopg2~=2.9.5 bleach~=5.0.1 pydantic~=1.10.2 django-htmx~=1.13.0 +django-storages[google,boto3]~=1.13.1 diff --git a/static/css/style.css b/static/css/style.css index eba0e4d..5f35cc2 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -549,6 +549,10 @@ form .buttons { margin: -20px 0 15px 0; } +form p+.buttons { + margin-top: 0; +} + .right-column form .buttons { margin: 5px 10px 5px 0; } diff --git a/stator/management/commands/runstator.py b/stator/management/commands/runstator.py index a77192e..eaa2585 100644 --- a/stator/management/commands/runstator.py +++ b/stator/management/commands/runstator.py @@ -4,6 +4,7 @@ from asgiref.sync import async_to_sync from django.apps import apps from django.core.management.base import BaseCommand +from core.models import Config from stator.models import StatorModel from stator.runner import StatorRunner @@ -22,6 +23,8 @@ class Command(BaseCommand): parser.add_argument("model_labels", nargs="*", type=str) def handle(self, model_labels: List[str], concurrency: int, *args, **options): + # Cache system config + Config.system = Config.load_system() # Resolve the models list into names models = cast( List[Type[StatorModel]], diff --git a/stator/migrations/0001_initial.py b/stator/migrations/0001_initial.py index f7d652e..8dcfc07 100644 --- a/stator/migrations/0001_initial.py +++ b/stator/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.3 on 2022-11-10 05:56 +# Generated by Django 4.1.3 on 2022-11-18 17:49 from django.db import migrations, models diff --git a/takahe/settings/base.py b/takahe/settings/base.py index 614bfd2..d2e30c3 100644 --- a/takahe/settings/base.py +++ b/takahe/settings/base.py @@ -1,5 +1,7 @@ import os +import sys from pathlib import Path +from typing import Optional BASE_DIR = Path(__file__).resolve().parent.parent.parent @@ -56,11 +58,11 @@ WSGI_APPLICATION = "takahe.wsgi.application" DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql_psycopg2", - "HOST": os.environ.get("POSTGRES_HOST", "localhost"), - "PORT": os.environ.get("POSTGRES_PORT", 5432), - "NAME": os.environ.get("POSTGRES_DB", "takahe"), - "USER": os.environ.get("POSTGRES_USER", "postgres"), - "PASSWORD": os.environ.get("POSTGRES_PASSWORD"), + "HOST": os.environ.get("PGHOST", "localhost"), + "PORT": os.environ.get("PGPORT", 5432), + "NAME": os.environ.get("PGDATABASE", "takahe"), + "USER": os.environ.get("PGUSER", "postgres"), + "PASSWORD": os.environ.get("PGPASSWORD"), } } @@ -109,12 +111,47 @@ STATICFILES_DIRS = [ ALLOWED_HOSTS = ["*"] +### User-configurable options, pulled from the environment ### + MAIN_DOMAIN = os.environ["TAKAHE_MAIN_DOMAIN"] if "/" in MAIN_DOMAIN: print("TAKAHE_MAIN_DOMAIN should be just the domain name - no https:// or path") - -EMAIL_FROM = os.environ["TAKAHE_EMAIL_FROM"] - -# Note that this MUST be a fully qualified URL in production -MEDIA_URL = os.environ.get("TAKAHE_MEDIA_URL", "/media/") -MEDIA_ROOT = os.environ.get("TAKAHE_MEDIA_ROOT", BASE_DIR / "media") + sys.exit(1) + + +if os.environ.get("TAKAHE_EMAIL_CONSOLE_ONLY"): + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + EMAIL_FROM = "test@example.com" +else: + EMAIL_FROM = os.environ["TAKAHE_EMAIL_FROM"] + if "TAKAHE_EMAIL_SENDGRID_KEY" in os.environ: + EMAIL_HOST = "smtp.sendgrid.net" + EMAIL_PORT = 587 + EMAIL_HOST_USER: Optional[str] = "apikey" + EMAIL_HOST_PASSWORD: Optional[str] = os.environ["TAKAHE_EMAIL_SENDGRID_KEY"] + EMAIL_USE_TLS = True + else: + EMAIL_HOST = os.environ["TAKAHE_EMAIL_HOST"] + EMAIL_PORT = int(os.environ["TAKAHE_EMAIL_PORT"]) + EMAIL_HOST_USER = os.environ.get("TAKAHE_EMAIL_USER") + EMAIL_HOST_PASSWORD = os.environ.get("TAKAHE_EMAIL_PASSWORD") + EMAIL_USE_SSL = EMAIL_PORT == 465 + EMAIL_USE_TLS = EMAIL_PORT == 587 + +AUTO_ADMIN_EMAIL = os.environ.get("TAKAHE_AUTO_ADMIN_EMAIL") + +# Set up media storage +MEDIA_BACKEND = os.environ.get("TAKAHE_MEDIA_BACKEND", None) +if MEDIA_BACKEND == "local": + # Note that this MUST be a fully qualified URL in production + MEDIA_URL = os.environ.get("TAKAHE_MEDIA_URL", "/media/") + MEDIA_ROOT = os.environ.get("TAKAHE_MEDIA_ROOT", BASE_DIR / "media") +elif MEDIA_BACKEND == "gcs": + DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" + GS_BUCKET_NAME = os.environ["TAKAHE_MEDIA_BUCKET"] +elif MEDIA_BACKEND == "s3": + DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" + AWS_STORAGE_BUCKET_NAME = os.environ["TAKAHE_MEDIA_BUCKET"] +else: + print("Unknown TAKAHE_MEDIA_BACKEND value") + sys.exit(1) diff --git a/takahe/urls.py b/takahe/urls.py index 8c01d64..1f1b203 100644 --- a/takahe/urls.py +++ b/takahe/urls.py @@ -86,8 +86,7 @@ urlpatterns = [ ), # Identity views path("@/", identity.ViewIdentity.as_view()), - path("@/actor/", activitypub.Actor.as_view()), - path("@/actor/inbox/", activitypub.Inbox.as_view()), + path("@/inbox/", activitypub.Inbox.as_view()), path("@/action/", identity.ActionIdentity.as_view()), # Posts path("compose/", posts.Compose.as_view(), name="compose"), @@ -109,6 +108,8 @@ urlpatterns = [ # Well-known endpoints path(".well-known/webfinger", activitypub.Webfinger.as_view()), path(".well-known/host-meta", activitypub.HostMeta.as_view()), + path(".well-known/nodeinfo", activitypub.NodeInfo.as_view()), + path("nodeinfo/2.0/", activitypub.NodeInfo2.as_view()), # Task runner path(".stator/runner/", stator.RequestRunner.as_view()), # Django admin diff --git a/templates/admin/domain_create.html b/templates/admin/domain_create.html index dcc57fa..23c1ebf 100644 --- a/templates/admin/domain_create.html +++ b/templates/admin/domain_create.html @@ -33,8 +33,10 @@
Access Control {% include "forms/_field.html" with field=form.public %} + {% include "forms/_field.html" with field=form.default %}
+ Back
diff --git a/templates/admin/domain_edit.html b/templates/admin/domain_edit.html index 59bb8a2..3e7f70b 100644 --- a/templates/admin/domain_edit.html +++ b/templates/admin/domain_edit.html @@ -13,8 +13,10 @@
Access Control {% include "forms/_field.html" with field=form.public %} + {% include "forms/_field.html" with field=form.default %}
+ Back Delete
diff --git a/templates/admin/domains.html b/templates/admin/domains.html index bb7d8e4..8ef09fe 100644 --- a/templates/admin/domains.html +++ b/templates/admin/domains.html @@ -14,6 +14,9 @@ {% if domain.service_domain %}({{ domain.service_domain }}){% endif %} + {% if domain.default %} + Default + {% endif %} {% empty %}

You have no domains set up.

diff --git a/templates/auth/login.html b/templates/auth/login.html index b3b0a05..80b003b 100644 --- a/templates/auth/login.html +++ b/templates/auth/login.html @@ -12,6 +12,7 @@ {% endfor %}
+ Forgot Password
diff --git a/templates/auth/perform_reset_success.html b/templates/auth/perform_reset_success.html index 001e5d7..3f5125a 100644 --- a/templates/auth/perform_reset_success.html +++ b/templates/auth/perform_reset_success.html @@ -1,13 +1,14 @@ {% extends "base.html" %} -{% block title %}Password Reset{% endblock %} +{% block title %}Password Set{% endblock %} {% block content %}
- Password Reset + Password Set

- Your password for {{ email }} has been reset! + Your password for {{ email }} has been set. You can + now login.

diff --git a/templates/settings/profile.html b/templates/settings/profile.html index 5c00557..12ea206 100644 --- a/templates/settings/profile.html +++ b/templates/settings/profile.html @@ -12,8 +12,8 @@
Images - {% include "forms/_field.html" with field=form.icon preview=request.identity.icon.url %} - {% include "forms/_field.html" with field=form.image preview=request.identity.image.url %} + {% include "forms/_field.html" with field=form.icon %} + {% include "forms/_field.html" with field=form.image %}
View Profile diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index a51ef00..d8ab363 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.3 on 2022-11-11 20:02 +# Generated by Django 4.1.3 on 2022-11-18 17:49 import functools @@ -6,10 +6,12 @@ import django.db.models.deletion from django.conf import settings from django.db import migrations, models +import core.uploads import stator.models import users.models.follow import users.models.identity import users.models.inbox_message +import users.models.password_reset class Migration(migrations.Migration): @@ -45,6 +47,7 @@ class Migration(migrations.Migration): ("deleted", models.BooleanField(default=False)), ("created", models.DateTimeField(auto_now_add=True)), ("updated", models.DateTimeField(auto_now=True)), + ("last_seen", models.DateTimeField(auto_now_add=True)), ], options={ "abstract": False, @@ -70,6 +73,7 @@ class Migration(migrations.Migration): ("local", models.BooleanField()), ("blocked", models.BooleanField(default=False)), ("public", models.BooleanField(default=False)), + ("default", models.BooleanField(default=False)), ("created", models.DateTimeField(auto_now_add=True)), ("updated", models.DateTimeField(auto_now=True)), ( @@ -111,6 +115,25 @@ class Migration(migrations.Migration): "abstract": False, }, ), + migrations.CreateModel( + name="Invite", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("token", models.CharField(max_length=500, unique=True)), + ("email", models.EmailField(blank=True, max_length=254, null=True)), + ("note", models.TextField(blank=True, null=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ], + ), migrations.CreateModel( name="UserEvent", fields=[ @@ -146,6 +169,48 @@ class Migration(migrations.Migration): ), ], ), + migrations.CreateModel( + name="PasswordReset", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("state_ready", models.BooleanField(default=True)), + ("state_changed", models.DateTimeField(auto_now_add=True)), + ("state_attempted", models.DateTimeField(blank=True, null=True)), + ("state_locked_until", models.DateTimeField(blank=True, null=True)), + ( + "state", + stator.models.StateField( + choices=[("new", "new"), ("sent", "sent")], + default="new", + graph=users.models.password_reset.PasswordResetStates, + max_length=100, + ), + ), + ("token", models.CharField(max_length=500, unique=True)), + ("new_account", models.BooleanField()), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="password_resets", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), migrations.CreateModel( name="Identity", fields=[ @@ -194,9 +259,7 @@ class Migration(migrations.Migration): blank=True, null=True, upload_to=functools.partial( - users.models.identity.upload_namer, - *("profile_images",), - **{}, + core.uploads.upload_namer, *("profile_images",), **{} ), ), ), @@ -206,14 +269,13 @@ class Migration(migrations.Migration): blank=True, null=True, upload_to=functools.partial( - users.models.identity.upload_namer, - *("background_images",), - **{}, + core.uploads.upload_namer, *("background_images",), **{} ), ), ), ("private_key", models.TextField(blank=True, null=True)), ("public_key", models.TextField(blank=True, null=True)), + ("public_key_id", models.TextField(blank=True, null=True)), ("created", models.DateTimeField(auto_now_add=True)), ("updated", models.DateTimeField(auto_now=True)), ("fetched", models.DateTimeField(blank=True, null=True)), @@ -224,6 +286,7 @@ class Migration(migrations.Migration): blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, + related_name="identities", to="users.domain", ), ), @@ -302,7 +365,7 @@ class Migration(migrations.Migration): ("local_requested", "local_requested"), ("remote_requested", "remote_requested"), ("accepted", "accepted"), - ("undone_locally", "undone_locally"), + ("undone", "undone"), ("undone_remotely", "undone_remotely"), ], default="unrequested", diff --git a/users/migrations/0002_identity_public_key_id.py b/users/migrations/0002_identity_public_key_id.py deleted file mode 100644 index 3648c20..0000000 --- a/users/migrations/0002_identity_public_key_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-12 21:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("users", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="identity", - name="public_key_id", - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/users/migrations/0003_user_last_seen_alter_identity_domain.py b/users/migrations/0003_user_last_seen_alter_identity_domain.py deleted file mode 100644 index b6c49d1..0000000 --- a/users/migrations/0003_user_last_seen_alter_identity_domain.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-17 04:18 - -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("users", "0002_identity_public_key_id"), - ] - - operations = [ - migrations.AddField( - model_name="user", - name="last_seen", - field=models.DateTimeField( - auto_now_add=True, default=django.utils.timezone.now - ), - preserve_default=False, - ), - migrations.AlterField( - model_name="identity", - name="domain", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="identities", - to="users.domain", - ), - ), - ] diff --git a/users/migrations/0004_passwordreset.py b/users/migrations/0004_passwordreset.py deleted file mode 100644 index d996ff4..0000000 --- a/users/migrations/0004_passwordreset.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-18 01:40 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - -import stator.models -import users.models.password_reset - - -class Migration(migrations.Migration): - - dependencies = [ - ("users", "0003_user_last_seen_alter_identity_domain"), - ] - - operations = [ - migrations.CreateModel( - name="PasswordReset", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("state_ready", models.BooleanField(default=True)), - ("state_changed", models.DateTimeField(auto_now_add=True)), - ("state_attempted", models.DateTimeField(blank=True, null=True)), - ("state_locked_until", models.DateTimeField(blank=True, null=True)), - ( - "state", - stator.models.StateField( - choices=[("new", "new"), ("sent", "sent")], - default="new", - graph=users.models.password_reset.PasswordResetStates, - max_length=100, - ), - ), - ("token", models.CharField(max_length=500, unique=True)), - ("new_account", models.BooleanField()), - ("created", models.DateTimeField(auto_now_add=True)), - ("updated", models.DateTimeField(auto_now=True)), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="password_resets", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/users/migrations/0005_invite.py b/users/migrations/0005_invite.py deleted file mode 100644 index bb18841..0000000 --- a/users/migrations/0005_invite.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-18 06:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("users", "0004_passwordreset"), - ] - - operations = [ - migrations.CreateModel( - name="Invite", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("token", models.CharField(max_length=500, unique=True)), - ("email", models.EmailField(blank=True, max_length=254, null=True)), - ("note", models.TextField(blank=True, null=True)), - ("created", models.DateTimeField(auto_now_add=True)), - ("updated", models.DateTimeField(auto_now=True)), - ], - ), - ] diff --git a/users/models/domain.py b/users/models/domain.py index 4743503..c238025 100644 --- a/users/models/domain.py +++ b/users/models/domain.py @@ -41,6 +41,9 @@ class Domain(models.Model): # should) public = models.BooleanField(default=False) + # If this is the default domain (shown as the default entry for new users) + default = models.BooleanField(default=False) + # Domains can also be linked to one or more users for their private use # This should be display domains ONLY users = models.ManyToManyField("users.User", related_name="domains", blank=True) @@ -52,7 +55,7 @@ class Domain(models.Model): root = "/admin/domains/" create = "/admin/domains/create/" edit = "/admin/domains/{self.domain}/" - delete = "/admin/domains/{self.domain}/delete/" + delete = "{e