diff --git a/.claude/skills/docs/SKILL.md b/.claude/skills/docs/SKILL.md index efc4c7c0..da9fd28b 100644 --- a/.claude/skills/docs/SKILL.md +++ b/.claude/skills/docs/SKILL.md @@ -2,7 +2,7 @@ name: docs description: Update, review, or build the Sphinx documentation for Democrasite disable-model-invocation: true -argument-hint: "[what to update or check, e.g. 'review for accuracy' or 'add activitypub section']" +argument-hint: "[what to update or check, e.g. 'review for accuracy' or 'add social section']" --- ## Task @@ -30,7 +30,7 @@ docs/ ├── democrasite.webiscite.views.rst ├── democrasite.webiscite.api.rst ├── democrasite.users.rst - ├── democrasite.activitypub.rst + ├── democrasite.social.rst └── … (one file per module) ``` diff --git a/CLAUDE.md b/CLAUDE.md index 479e78bf..af7a187a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ just coverage # Run tests with coverage + open HTML report just shell # Bash shell in django container just pyshell # Django shell_plus (IPython) just run # Execute arbitrary command in django container -just loaddata # Load fixtures (democrasite + activitypub) +just loaddata # Load fixtures (democrasite + social) ``` To run a single test file or test: @@ -49,7 +49,7 @@ Pytest is configured with `--ds=config.settings.test --reuse-db` in `pyproject.t - **`democrasite/webiscite/`** — Core app. Models: `PullRequest`, `Bill`, `Vote`. Handles GitHub webhooks, voting logic, constitution enforcement, and Celery tasks for auto-merging. - **`democrasite/users/`** — Custom `User` model (extends `AbstractUser` with single `name` field instead of first/last). OAuth integration. -- **`democrasite/activitypub/`** — ActivityPub federation. Models: `Person` (linked to User with keypair), `Follow`. +- **`democrasite/social/`** — Social network for short notes. Models: `Person` (linked to User with keypair), `Follow`. ### Configuration diff --git a/README.rst b/README.rst index dcb3861a..f5b49a8d 100644 --- a/README.rst +++ b/README.rst @@ -94,7 +94,7 @@ Loading initial data To load some initial sample data into the database, run:: - $ python manage.py loaddata democrasite activitypub + $ python manage.py loaddata democrasite social Setting up your users ^^^^^^^^^^^^^^^^^^^^^ diff --git a/config/settings/base.py b/config/settings/base.py index 56f32bee..199b5d7e 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -102,7 +102,7 @@ # Custom apps "democrasite.users", "democrasite.webiscite", - "democrasite.activitypub", + "democrasite.social", ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS diff --git a/config/urls.py b/config/urls.py index 6bed9ffc..1d43b969 100644 --- a/config/urls.py +++ b/config/urls.py @@ -23,10 +23,8 @@ # User management path("users/", include("democrasite.users.urls", namespace="users")), path("accounts/", include("allauth.urls")), - # ActivityPub - path( - "activitypub/", include("democrasite.activitypub.urls", namespace="activitypub") - ), + # Social + path("social/", include("democrasite.social.urls", namespace="social")), # webiscite path("", include("democrasite.webiscite.urls", namespace="webiscite")), # Media files diff --git a/democrasite/activitypub/migrations/0001_initial.py b/democrasite/activitypub/migrations/0001_initial.py deleted file mode 100644 index 93ee266d..00000000 --- a/democrasite/activitypub/migrations/0001_initial.py +++ /dev/null @@ -1,117 +0,0 @@ -# Generated by Django 5.1.6 on 2025-07-25 00:03 - -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields -import mptt.fields -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Note', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('content', models.TextField()), - ('lft', models.PositiveIntegerField(editable=False)), - ('rght', models.PositiveIntegerField(editable=False)), - ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), - ('level', models.PositiveIntegerField(editable=False)), - ('in_reply_to', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='replies', to='activitypub.note')), - ], - options={ - 'ordering': ['-created'], - }, - ), - migrations.CreateModel( - name='Like', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('note', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='activitypub.note')), - ], - options={ - 'ordering': ['-created'], - }, - ), - migrations.CreateModel( - name='Person', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('private_key', models.TextField()), - ('public_key', models.TextField()), - ('bio', models.TextField(blank=True, help_text='A short biography or description of the person')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name_plural': 'People', - }, - ), - migrations.AddField( - model_name='note', - name='author', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='notes', to='activitypub.person'), - ), - migrations.AddField( - model_name='note', - name='likes', - field=models.ManyToManyField(blank=True, related_name='likes', through='activitypub.Like', to='activitypub.person'), - ), - migrations.AddField( - model_name='like', - name='person', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='activitypub.person'), - ), - migrations.CreateModel( - name='Repost', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('note', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='activitypub.note')), - ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='activitypub.person')), - ], - options={ - 'ordering': ['-created'], - 'unique_together': {('person', 'note')}, - }, - ), - migrations.AddField( - model_name='note', - name='reposts', - field=models.ManyToManyField(blank=True, related_name='reposts', through='activitypub.Repost', to='activitypub.person'), - ), - migrations.AlterUniqueTogether( - name='like', - unique_together={('person', 'note')}, - ), - migrations.CreateModel( - name='Follow', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('follower', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following_set', to='activitypub.person')), - ('following', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follower_set', to='activitypub.person')), - ], - options={ - 'constraints': [models.UniqueConstraint(fields=('following', 'follower'), name='unique_follow', violation_error_code="Can't follow a person more than once")], - }, - ), - migrations.AddField( - model_name='person', - name='following', - field=models.ManyToManyField(blank=True, help_text='People this person is following', related_name='followers', through='activitypub.Follow', to='activitypub.person'), - ), - ] diff --git a/democrasite/activitypub/migrations/0003_alter_person_following.py b/democrasite/activitypub/migrations/0003_alter_person_following.py deleted file mode 100644 index 028c119a..00000000 --- a/democrasite/activitypub/migrations/0003_alter_person_following.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.10 on 2026-02-14 22:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('activitypub', '0002_historicalnote_historicallike_historicalperson_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='person', - name='following', - field=models.ManyToManyField(blank=True, help_text='People this person is following', related_name='followers', through='activitypub.Follow', through_fields=('follower', 'following'), to='activitypub.person'), - ), - ] diff --git a/democrasite/activitypub/tests/test_urls.py b/democrasite/activitypub/tests/test_urls.py deleted file mode 100644 index 3dc4056a..00000000 --- a/democrasite/activitypub/tests/test_urls.py +++ /dev/null @@ -1,102 +0,0 @@ -from django.urls import resolve -from django.urls import reverse - -from democrasite.activitypub.models import Note -from democrasite.activitypub.models import Person - - -def test_list(): - assert reverse("activitypub:note-list") == "/activitypub/notes/" - assert resolve("/activitypub/notes/").view_name == "activitypub:note-list" - - -def test_create(): - assert reverse("activitypub:note-create") == "/activitypub/notes/create/" - assert resolve("/activitypub/notes/create/").view_name == "activitypub:note-create" - - -def test_note_detail(note: Note): - assert ( - reverse("activitypub:note-detail", kwargs={"pk": note.id}) - == f"/activitypub/notes/{note.id}/" - ) - assert ( - resolve(f"/activitypub/notes/{note.id}/").view_name == "activitypub:note-detail" - ) - - -def test_note_reply(note: Note): - assert ( - reverse("activitypub:note-reply", kwargs={"pk": note.id}) - == f"/activitypub/notes/{note.id}/reply/" - ) - assert ( - resolve(f"/activitypub/notes/{note.id}/reply/").view_name - == "activitypub:note-reply" - ) - - -def test_note_like(note: Note): - assert ( - reverse("activitypub:note-like", kwargs={"pk": note.id}) - == f"/activitypub/notes/{note.id}/like/" - ) - assert ( - resolve(f"/activitypub/notes/{note.id}/like/").view_name - == "activitypub:note-like" - ) - - -def test_note_repost(note: Note): - assert ( - reverse("activitypub:note-repost", kwargs={"pk": note.id}) - == f"/activitypub/notes/{note.id}/repost/" - ) - assert ( - resolve(f"/activitypub/notes/{note.id}/repost/").view_name - == "activitypub:note-repost" - ) - - -def test_person_create(): - assert reverse("activitypub:person-create") == "/activitypub/person/create/" - assert ( - resolve("/activitypub/person/create/").view_name == "activitypub:person-create" - ) - - -def test_person_update(): - assert reverse("activitypub:person-update") == "/activitypub/person/update/" - assert ( - resolve("/activitypub/person/update/").view_name == "activitypub:person-update" - ) - - -def test_following(): - assert reverse("activitypub:following-notes") == "/activitypub/person/following/" - assert ( - resolve("/activitypub/person/following/").view_name - == "activitypub:following-notes" - ) - - -def test_person_detail(person: Person): - assert ( - reverse("activitypub:person-detail", kwargs={"username": person.display_name}) - == f"/activitypub/person/{person.display_name}/" - ) - assert ( - resolve(f"/activitypub/person/{person.display_name}/").view_name - == "activitypub:person-detail" - ) - - -def test_follow(person: Person): - assert ( - reverse("activitypub:person-follow", kwargs={"username": person.display_name}) - == f"/activitypub/person/{person.display_name}/follow/" - ) - assert ( - resolve(f"/activitypub/person/{person.display_name}/follow/").view_name - == "activitypub:person-follow" - ) diff --git a/democrasite/activitypub/__init__.py b/democrasite/social/__init__.py similarity index 100% rename from democrasite/activitypub/__init__.py rename to democrasite/social/__init__.py diff --git a/democrasite/activitypub/admin.py b/democrasite/social/admin.py similarity index 73% rename from democrasite/activitypub/admin.py rename to democrasite/social/admin.py index d54d9c19..c4e2ccc0 100644 --- a/democrasite/activitypub/admin.py +++ b/democrasite/social/admin.py @@ -2,8 +2,8 @@ from mptt.admin import MPTTModelAdmin from simple_history.admin import SimpleHistoryAdmin -from democrasite.activitypub.models import Note -from democrasite.activitypub.models import Person +from democrasite.social.models import Note +from democrasite.social.models import Person @admin.register(Note) diff --git a/democrasite/activitypub/apps.py b/democrasite/social/apps.py similarity index 52% rename from democrasite/activitypub/apps.py rename to democrasite/social/apps.py index ea163de9..f95e1817 100644 --- a/democrasite/activitypub/apps.py +++ b/democrasite/social/apps.py @@ -1,6 +1,7 @@ from django.apps import AppConfig -class ActivitypubConfig(AppConfig): +class SocialConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "democrasite.activitypub" + name = "democrasite.social" + label = "social" diff --git a/democrasite/activitypub/fixtures/activitypub.json b/democrasite/social/fixtures/social.json similarity index 89% rename from democrasite/activitypub/fixtures/activitypub.json rename to democrasite/social/fixtures/social.json index b1d42f52..7d2807f6 100644 --- a/democrasite/activitypub/fixtures/activitypub.json +++ b/democrasite/social/fixtures/social.json @@ -34,7 +34,7 @@ } }, { - "model": "activitypub.person", + "model": "social.person", "pk": 1, "fields": { "created": "2025-07-04T23:22:11.652Z", @@ -46,7 +46,7 @@ } }, { - "model": "activitypub.person", + "model": "social.person", "pk": 2, "fields": { "created": "2025-07-05T16:15:12.547Z", @@ -59,7 +59,7 @@ }, { - "model": "activitypub.historicalperson", + "model": "social.historicalperson", "pk": 1, "fields": { "id": 1, @@ -76,7 +76,7 @@ } }, { - "model": "activitypub.historicalperson", + "model": "social.historicalperson", "pk": 2, "fields": { "id": 2, @@ -93,42 +93,42 @@ } }, { - "model": "activitypub.like", + "model": "social.like", "pk": 1, "fields": { "person": 2, "note": 1, "created": "2025-07-05T16:15:47.380Z" } }, { - "model": "activitypub.like", + "model": "social.like", "pk": 2, "fields": { "person": 2, "note": 2, "created": "2025-07-05T16:15:49.455Z" } }, { - "model": "activitypub.like", + "model": "social.like", "pk": 3, "fields": { "person": 1, "note": 2, "created": "2025-07-05T16:23:07.500Z" } }, { - "model": "activitypub.repost", + "model": "social.repost", "pk": 1, "fields": { "person": 2, "note": 1, "created": "2025-07-05T16:15:30.859Z" } }, { - "model": "activitypub.repost", + "model": "social.repost", "pk": 2, "fields": { "person": 2, "note": 4, "created": "2025-07-05T16:17:12.726Z" } }, { - "model": "activitypub.repost", + "model": "social.repost", "pk": 3, "fields": { "person": 1, "note": 2, "created": "2025-07-05T16:23:04.907Z" } }, { - "model": "activitypub.repost", + "model": "social.repost", "pk": 4, "fields": { "person": 1, "note": 5, "created": "2025-07-05T16:23:10.741Z" } }, { - "model": "activitypub.note", + "model": "social.note", "pk": 1, "fields": { "created": "2025-07-04T23:22:33.271Z", @@ -143,7 +143,7 @@ } }, { - "model": "activitypub.note", + "model": "social.note", "pk": 2, "fields": { "created": "2025-07-04T23:22:43.126Z", @@ -158,7 +158,7 @@ } }, { - "model": "activitypub.note", + "model": "social.note", "pk": 3, "fields": { "created": "2025-07-04T23:23:17.547Z", @@ -173,7 +173,7 @@ } }, { - "model": "activitypub.note", + "model": "social.note", "pk": 4, "fields": { "created": "2025-07-05T16:15:43.177Z", @@ -188,7 +188,7 @@ } }, { - "model": "activitypub.note", + "model": "social.note", "pk": 5, "fields": { "created": "2025-07-05T16:16:39.332Z", @@ -203,7 +203,7 @@ } }, { - "model": "activitypub.historicalnote", + "model": "social.historicalnote", "pk": 1, "fields": { "id": 5, @@ -223,7 +223,7 @@ } }, { - "model": "activitypub.historicalnote", + "model": "social.historicalnote", "pk": 2, "fields": { "id": 4, @@ -243,7 +243,7 @@ } }, { - "model": "activitypub.historicalnote", + "model": "social.historicalnote", "pk": 3, "fields": { "id": 3, @@ -263,7 +263,7 @@ } }, { - "model": "activitypub.historicalnote", + "model": "social.historicalnote", "pk": 4, "fields": { "id": 2, @@ -283,7 +283,7 @@ } }, { - "model": "activitypub.historicalnote", + "model": "social.historicalnote", "pk": 5, "fields": { "id": 1, diff --git a/democrasite/activitypub/migrations/0002_historicalnote_historicallike_historicalperson_and_more.py b/democrasite/social/migrations/0001_initial.py similarity index 51% rename from democrasite/activitypub/migrations/0002_historicalnote_historicallike_historicalperson_and_more.py rename to democrasite/social/migrations/0001_initial.py index 7f860ddd..f0d0b860 100644 --- a/democrasite/activitypub/migrations/0002_historicalnote_historicallike_historicalperson_and_more.py +++ b/democrasite/social/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-09-07 22:09 +# Generated by Django 5.2.12 on 2026-03-23 20:20 import django.db.models.deletion import django.utils.timezone @@ -11,12 +11,72 @@ class Migration(migrations.Migration): + initial = True + dependencies = [ - ('activitypub', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ + migrations.CreateModel( + name='Follow', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='HistoricalPerson', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('private_key', models.TextField()), + ('public_key', models.TextField()), + ('bio', models.TextField(blank=True, help_text='A short biography or description of the person')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical person', + 'verbose_name_plural': 'historical People', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('content', models.TextField()), + ('lft', models.PositiveIntegerField(editable=False)), + ('rght', models.PositiveIntegerField(editable=False)), + ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), + ('level', models.PositiveIntegerField(editable=False)), + ('in_reply_to', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='replies', to='social.note')), + ], + options={ + 'ordering': ['-created'], + }, + ), + migrations.CreateModel( + name='Like', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('note', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='social.note')), + ], + options={ + 'ordering': ['-created'], + }, + ), migrations.CreateModel( name='HistoricalNote', fields=[ @@ -32,9 +92,8 @@ class Migration(migrations.Migration): ('history_date', models.DateTimeField(db_index=True)), ('history_change_reason', models.CharField(max_length=100, null=True)), ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), - ('author', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='activitypub.person')), ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), - ('in_reply_to', mptt.fields.TreeForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='activitypub.note')), + ('in_reply_to', mptt.fields.TreeForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='social.note')), ], options={ 'verbose_name': 'historical note', @@ -45,41 +104,68 @@ class Migration(migrations.Migration): bases=(simple_history.models.HistoricalChanges, models.Model), ), migrations.CreateModel( - name='HistoricalLike', + name='Person', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('private_key', models.TextField()), + ('public_key', models.TextField()), + ('bio', models.TextField(blank=True, help_text='A short biography or description of the person')), + ('following', models.ManyToManyField(blank=True, help_text='People this person is following', related_name='followers', through='social.Follow', through_fields=('follower', 'following'), to='social.person')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'People', + }, + ), + migrations.AddField( + model_name='note', + name='author', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='notes', to='social.person'), + ), + migrations.AddField( + model_name='note', + name='likes', + field=models.ManyToManyField(blank=True, related_name='likes', through='social.Like', to='social.person'), + ), + migrations.AddField( + model_name='like', + name='person', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='social.person'), + ), + migrations.CreateModel( + name='HistoricalRepost', fields=[ ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), ('created', models.DateTimeField(blank=True, editable=False)), ('m2m_history_id', models.AutoField(primary_key=True, serialize=False)), - ('note', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='activitypub.note')), - ('person', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='activitypub.person')), - ('history', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='activitypub.historicalnote')), + ('history', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='social.historicalnote')), + ('note', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='social.note')), + ('person', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='social.person')), ], options={ - 'verbose_name': 'HistoricalLike', + 'verbose_name': 'HistoricalRepost', }, bases=(simple_history.models.HistoricalChanges, models.Model), ), + migrations.AddField( + model_name='historicalnote', + name='author', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='social.person'), + ), migrations.CreateModel( - name='HistoricalPerson', + name='HistoricalLike', fields=[ ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('private_key', models.TextField()), - ('public_key', models.TextField()), - ('bio', models.TextField(blank=True, help_text='A short biography or description of the person')), - ('history_id', models.AutoField(primary_key=True, serialize=False)), - ('history_date', models.DateTimeField(db_index=True)), - ('history_change_reason', models.CharField(max_length=100, null=True)), - ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), - ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), - ('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ('created', models.DateTimeField(blank=True, editable=False)), + ('m2m_history_id', models.AutoField(primary_key=True, serialize=False)), + ('history', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='social.historicalnote')), + ('note', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='social.note')), + ('person', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='social.person')), ], options={ - 'verbose_name': 'historical person', - 'verbose_name_plural': 'historical People', - 'ordering': ('-history_date', '-history_id'), - 'get_latest_by': ('history_date', 'history_id'), + 'verbose_name': 'HistoricalLike', }, bases=(simple_history.models.HistoricalChanges, models.Model), ), @@ -89,28 +175,52 @@ class Migration(migrations.Migration): ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), ('created', models.DateTimeField(blank=True, editable=False)), ('m2m_history_id', models.AutoField(primary_key=True, serialize=False)), - ('follower', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='activitypub.person')), - ('following', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='activitypub.person')), - ('history', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='activitypub.historicalperson')), + ('history', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='social.historicalperson')), + ('follower', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='social.person')), + ('following', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='social.person')), ], options={ 'verbose_name': 'HistoricalFollow', }, bases=(simple_history.models.HistoricalChanges, models.Model), ), + migrations.AddField( + model_name='follow', + name='follower', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following_set', to='social.person'), + ), + migrations.AddField( + model_name='follow', + name='following', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follower_set', to='social.person'), + ), migrations.CreateModel( - name='HistoricalRepost', + name='Repost', fields=[ - ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), - ('created', models.DateTimeField(blank=True, editable=False)), - ('m2m_history_id', models.AutoField(primary_key=True, serialize=False)), - ('history', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='activitypub.historicalnote')), - ('note', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='activitypub.note')), - ('person', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='activitypub.person')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('note', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='social.note')), + ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='social.person')), ], options={ - 'verbose_name': 'HistoricalRepost', + 'ordering': ['-created'], }, - bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.AddField( + model_name='note', + name='reposts', + field=models.ManyToManyField(blank=True, related_name='reposts', through='social.Repost', to='social.person'), + ), + migrations.AlterUniqueTogether( + name='like', + unique_together={('person', 'note')}, + ), + migrations.AddConstraint( + model_name='follow', + constraint=models.UniqueConstraint(fields=('following', 'follower'), name='unique_follow', violation_error_code="Can't follow a person more than once"), + ), + migrations.AlterUniqueTogether( + name='repost', + unique_together={('person', 'note')}, ), ] diff --git a/democrasite/activitypub/migrations/__init__.py b/democrasite/social/migrations/__init__.py similarity index 100% rename from democrasite/activitypub/migrations/__init__.py rename to democrasite/social/migrations/__init__.py diff --git a/democrasite/activitypub/models.py b/democrasite/social/models.py similarity index 95% rename from democrasite/activitypub/models.py rename to democrasite/social/models.py index 9a8dc0b2..69f9a23b 100644 --- a/democrasite/activitypub/models.py +++ b/democrasite/social/models.py @@ -1,4 +1,4 @@ -"""Models for the activitypub app.""" +"""Models for the social app.""" from django.contrib.auth import get_user_model from django.db import models @@ -95,14 +95,10 @@ def get_absolute_url(self): Returns: str: URL for the person detail. """ - return reverse( - "activitypub:person-detail", kwargs={"username": self.display_name} - ) + return reverse("social:person-detail", kwargs={"username": self.display_name}) def get_follow_url(self): - return reverse( - "activitypub:person-follow", kwargs={"username": self.display_name} - ) + return reverse("social:person-follow", kwargs={"username": self.display_name}) def is_following(self, person: "Person") -> bool: """Check if a person is following this person. @@ -280,10 +276,10 @@ def get_absolute_url(self): Returns: str: URL for the note detail. """ - return reverse("activitypub:note-detail", kwargs={"pk": self.pk}) + return reverse("social:note-detail", kwargs={"pk": self.pk}) def get_like_url(self): - return reverse("activitypub:note-like", kwargs={"pk": self.id}) + return reverse("social:note-like", kwargs={"pk": self.id}) def liked_by(self, person: Person) -> bool: """Check if a person has liked the note. @@ -310,7 +306,7 @@ def like(self, person: Person) -> bool: return True def get_repost_url(self): - return reverse("activitypub:note-repost", kwargs={"pk": self.id}) + return reverse("social:note-repost", kwargs={"pk": self.id}) def reposted_by(self, person: Person) -> bool: """Check if a person has reposted the note. diff --git a/democrasite/activitypub/tests/__init__.py b/democrasite/social/tests/__init__.py similarity index 100% rename from democrasite/activitypub/tests/__init__.py rename to democrasite/social/tests/__init__.py diff --git a/democrasite/activitypub/tests/conftest.py b/democrasite/social/tests/conftest.py similarity index 75% rename from democrasite/activitypub/tests/conftest.py rename to democrasite/social/tests/conftest.py index aaa58989..a3b2aed5 100644 --- a/democrasite/activitypub/tests/conftest.py +++ b/democrasite/social/tests/conftest.py @@ -1,7 +1,7 @@ import pytest -from democrasite.activitypub.models import Note -from democrasite.activitypub.models import Person +from democrasite.social.models import Note +from democrasite.social.models import Person from .factories import NoteFactory from .factories import PersonFactory diff --git a/democrasite/activitypub/tests/factories.py b/democrasite/social/tests/factories.py similarity index 84% rename from democrasite/activitypub/tests/factories.py rename to democrasite/social/tests/factories.py index 989669c1..083655e8 100644 --- a/democrasite/activitypub/tests/factories.py +++ b/democrasite/social/tests/factories.py @@ -1,8 +1,8 @@ import factory from factory.django import DjangoModelFactory -from democrasite.activitypub.models import Note -from democrasite.activitypub.models import Person +from democrasite.social.models import Note +from democrasite.social.models import Person from democrasite.users.tests.factories import UserFactory diff --git a/democrasite/activitypub/tests/test_models.py b/democrasite/social/tests/test_models.py similarity index 90% rename from democrasite/activitypub/tests/test_models.py rename to democrasite/social/tests/test_models.py index 4ef98ecf..0c2dd7e8 100644 --- a/democrasite/activitypub/tests/test_models.py +++ b/democrasite/social/tests/test_models.py @@ -1,5 +1,5 @@ -from democrasite.activitypub.models import Note -from democrasite.activitypub.models import Person +from democrasite.social.models import Note +from democrasite.social.models import Person from democrasite.users.models import User from .factories import NoteFactory @@ -35,14 +35,11 @@ def test_display_name(self, person: Person): assert person.display_name == person.user.username def test_get_absolute_url(self, person: Person): - assert ( - person.get_absolute_url() == f"/activitypub/person/{person.user.username}/" - ) + assert person.get_absolute_url() == f"/social/person/{person.user.username}/" def test_get_follow_url(self, person: Person): assert ( - person.get_follow_url() - == f"/activitypub/person/{person.user.username}/follow/" + person.get_follow_url() == f"/social/person/{person.user.username}/follow/" ) def test_is_following(self, person: Person): @@ -135,10 +132,10 @@ def test_str_short_content(self): assert str(note) == f"{note.author.user.username}: short" def test_get_absolute_url(self, note: Note): - assert note.get_absolute_url() == f"/activitypub/notes/{note.pk}/" + assert note.get_absolute_url() == f"/social/notes/{note.pk}/" def test_get_like_url(self, note: Note): - assert note.get_like_url() == f"/activitypub/notes/{note.id}/like/" + assert note.get_like_url() == f"/social/notes/{note.id}/like/" def test_liked_by(self, note: Note, person: Person): assert not note.liked_by(person) @@ -157,7 +154,7 @@ def test_like(self, note: Note, person: Person): assert person not in note.likes.all() def test_get_repost_url(self, note: Note): - assert note.get_repost_url() == f"/activitypub/notes/{note.id}/repost/" + assert note.get_repost_url() == f"/social/notes/{note.id}/repost/" def test_reposted_by(self, note: Note, person: Person): assert not note.reposted_by(person) diff --git a/democrasite/activitypub/tests/test_templates.py b/democrasite/social/tests/test_templates.py similarity index 62% rename from democrasite/activitypub/tests/test_templates.py rename to democrasite/social/tests/test_templates.py index 6b61e6c9..c69ba546 100644 --- a/democrasite/activitypub/tests/test_templates.py +++ b/democrasite/social/tests/test_templates.py @@ -5,10 +5,10 @@ from django.test import RequestFactory from django.urls import reverse -from democrasite.activitypub.models import Note -from democrasite.activitypub.models import Person -from democrasite.activitypub.tests.factories import NoteFactory -from democrasite.activitypub.tests.factories import PersonFactory +from democrasite.social.models import Note +from democrasite.social.models import Person +from democrasite.social.tests.factories import NoteFactory +from democrasite.social.tests.factories import PersonFactory from democrasite.users.models import User @@ -17,7 +17,7 @@ def test_base_no_person(self, user: User, rf: RequestFactory): request = rf.get("/fake-url/") request.user = user - response = render(request, "activitypub/base.html") + response = render(request, "social/base.html") assert response.status_code == HTTPStatus.OK assert b"Create Profile" in response.content @@ -26,54 +26,52 @@ def test_base_with_person(self, person: Person, rf: RequestFactory): request = rf.get("/fake-url/") request.user = person.user - response = render(request, "activitypub/base.html") + response = render(request, "social/base.html") assert b"Profile" in response.content class TestNoteListTemplate: def test_no_notes(self, client: Client): - response = client.get(reverse("activitypub:note-list")) + response = client.get(reverse("social:note-list")) assert response.status_code == HTTPStatus.OK - assert response.templates[0].name == "activitypub/note_list.html" + assert response.templates[0].name == "social/note_list.html" assert b"No notes available" in response.content def test_with_notes(self, note: Note, client: Client): NoteFactory.create(in_reply_to=note) - content = client.get(reverse("activitypub:note-list")).content.decode() + content = client.get(reverse("social:note-list")).content.decode() assert note.content in content assert "No notes available" not in content assert "In reply to: " in content def test_reply_link(self, note: Note, user: User, person: Person, client: Client): - content = client.get(reverse("activitypub:note-list")).content.decode() + content = client.get(reverse("social:note-list")).content.decode() - assert reverse("activitypub:note-reply", kwargs={"pk": note.id}) not in content + assert reverse("social:note-reply", kwargs={"pk": note.id}) not in content client.force_login(user) - content = client.get(reverse("activitypub:note-list")).content.decode() + content = client.get(reverse("social:note-list")).content.decode() - assert ( - reverse("activitypub:note-reply", kwargs={"pk": note.id}) not in content - ), "Reply link should not be visible to users without a Person profile" + assert reverse("social:note-reply", kwargs={"pk": note.id}) not in content, ( + "Reply link should not be visible to users without a Person profile" + ) client.force_login(person.user) - content = client.get(reverse("activitypub:note-list")).content.decode() + content = client.get(reverse("social:note-list")).content.decode() - assert reverse("activitypub:note-reply", kwargs={"pk": note.id}) in content + assert reverse("social:note-reply", kwargs={"pk": note.id}) in content def test_reposted(self, note: Note, person: Person, client: Client): note.repost(person) content = client.get( - reverse( - "activitypub:person-detail", kwargs={"username": person.display_name} - ) + reverse("social:person-detail", kwargs={"username": person.display_name}) ).content.decode() assert "Reposted by:" in content @@ -81,12 +79,10 @@ def test_reposted(self, note: Note, person: Person, client: Client): class TestNoteDetailTemplate: def test_note_detail(self, note: Note, client: Client): - response = client.get( - reverse("activitypub:note-detail", kwargs={"pk": note.id}) - ) + response = client.get(reverse("social:note-detail", kwargs={"pk": note.id})) assert response.status_code == HTTPStatus.OK - assert response.templates[0].name == "activitypub/note_detail.html" + assert response.templates[0].name == "social/note_detail.html" assert note.content in response.content.decode() assert "No replies yet." in response.content.decode() @@ -94,17 +90,17 @@ def test_reply_link(self, note: Note, person: Person, client: Client): client.force_login(person.user) content = client.get( - reverse("activitypub:note-detail", kwargs={"pk": note.id}) + reverse("social:note-detail", kwargs={"pk": note.id}) ).content.decode() - assert reverse("activitypub:note-reply", kwargs={"pk": note.id}) in content + assert reverse("social:note-reply", kwargs={"pk": note.id}) in content def test_note_replies(self, note: Note, client: Client): reply1 = NoteFactory.create(in_reply_to=note) reply2 = NoteFactory.create(in_reply_to=note) content = client.get( - reverse("activitypub:note-detail", kwargs={"pk": note.id}) + reverse("social:note-detail", kwargs={"pk": note.id}) ).content.decode() assert reply1.content in content @@ -116,7 +112,7 @@ def test_note_ancestors(self, note: Note, client: Client): reply2 = NoteFactory.create(in_reply_to=reply) content = client.get( - reverse("activitypub:note-detail", kwargs={"pk": reply2.id}) + reverse("social:note-detail", kwargs={"pk": reply2.id}) ).content.decode() assert note.content in content @@ -125,7 +121,7 @@ def test_note_ancestors(self, note: Note, client: Client): def test_like(self, note: Note, person: Person, client: Client): content = client.get( - reverse("activitypub:note-detail", kwargs={"pk": note.id}) + reverse("social:note-detail", kwargs={"pk": note.id}) ).content.decode() assert "bi-heart" not in content @@ -133,7 +129,7 @@ def test_like(self, note: Note, person: Person, client: Client): client.force_login(person.user) content = client.get( - reverse("activitypub:note-detail", kwargs={"pk": note.id}) + reverse("social:note-detail", kwargs={"pk": note.id}) ).content.decode() assert "bi-heart" in content @@ -142,14 +138,14 @@ def test_like(self, note: Note, person: Person, client: Client): note.like(person) content = client.get( - reverse("activitypub:note-detail", kwargs={"pk": note.id}) + reverse("social:note-detail", kwargs={"pk": note.id}) ).content.decode() assert "bi-heart-fill" in content def test_repost(self, note: Note, person: Person, client: Client): content = client.get( - reverse("activitypub:note-detail", kwargs={"pk": note.id}) + reverse("social:note-detail", kwargs={"pk": note.id}) ).content.decode() assert "bi-repeat" not in content @@ -157,7 +153,7 @@ def test_repost(self, note: Note, person: Person, client: Client): client.force_login(person.user) content = client.get( - reverse("activitypub:note-detail", kwargs={"pk": note.id}) + reverse("social:note-detail", kwargs={"pk": note.id}) ).content.decode() assert "bi-repeat" in content @@ -166,7 +162,7 @@ def test_repost(self, note: Note, person: Person, client: Client): note.repost(person) content = client.get( - reverse("activitypub:note-detail", kwargs={"pk": note.id}) + reverse("social:note-detail", kwargs={"pk": note.id}) ).content.decode() assert "text-success" in content @@ -176,31 +172,29 @@ class TestNoteFormTemplate: def test_note_form(self, person: Person, client: Client): client.force_login(person.user) - response = client.get(reverse("activitypub:note-create")) + response = client.get(reverse("social:note-create")) assert response.status_code == HTTPStatus.OK - assert response.templates[0].name == "activitypub/note_form.html" + assert response.templates[0].name == "social/note_form.html" assert b"Content" in response.content def test_note_reply_form(self, note: Note, person: Person, client: Client): client.force_login(person.user) - response = client.get(reverse("activitypub:note-reply", kwargs={"pk": note.id})) + response = client.get(reverse("social:note-reply", kwargs={"pk": note.id})) assert response.status_code == HTTPStatus.OK - assert response.templates[0].name == "activitypub/note_form.html" + assert response.templates[0].name == "social/note_form.html" class TestPersonDetailTemplate: def test_person_detail(self, person: Person, client: Client): response = client.get( - reverse( - "activitypub:person-detail", kwargs={"username": person.display_name} - ) + reverse("social:person-detail", kwargs={"username": person.display_name}) ) assert response.status_code == HTTPStatus.OK - assert response.templates[0].name == "activitypub/person_detail.html" + assert response.templates[0].name == "social/person_detail.html" assert person.display_name in response.content.decode() assert person.bio in response.content.decode() assert "This user hasn't posted anything yet." in response.content.decode() @@ -215,9 +209,7 @@ def test_person_notes(self, person: Person, client: Client): bad_note = NoteFactory.create() content = client.get( - reverse( - "activitypub:person-detail", kwargs={"username": person.display_name} - ) + reverse("social:person-detail", kwargs={"username": person.display_name}) ).content.decode() assert note.content in content @@ -230,17 +222,13 @@ def test_follow_button(self, person: Person, client: Client): client.force_login(person2.user) content = client.get( - reverse( - "activitypub:person-detail", kwargs={"username": person.display_name} - ) + reverse("social:person-detail", kwargs={"username": person.display_name}) ).content.decode() assert "Follow" in content # this doesn't actually work as a check assert "person-unfollow" not in content assert ( - reverse( - "activitypub:person-follow", kwargs={"username": person.display_name} - ) + reverse("social:person-follow", kwargs={"username": person.display_name}) in content ) @@ -250,16 +238,12 @@ def test_unfollow_button(self, person: Person, client: Client): client.force_login(person2.user) content = client.get( - reverse( - "activitypub:person-detail", kwargs={"username": person.display_name} - ) + reverse("social:person-detail", kwargs={"username": person.display_name}) ).content.decode() assert "person-unfollow" in content assert ( - reverse( - "activitypub:person-follow", kwargs={"username": person.display_name} - ) + reverse("social:person-follow", kwargs={"username": person.display_name}) in content ) @@ -267,21 +251,19 @@ def test_edit_link(self, person: Person, client: Client): client.force_login(person.user) response = client.get( - reverse( - "activitypub:person-detail", kwargs={"username": person.display_name} - ) + reverse("social:person-detail", kwargs={"username": person.display_name}) ) assert response.status_code == HTTPStatus.OK - assert reverse("activitypub:person-update") in response.content.decode() + assert reverse("social:person-update") in response.content.decode() class TestPersonFormTemplate: def test_person_update_form(self, person: Person, client: Client): client.force_login(person.user) - response = client.get(reverse("activitypub:person-update")) + response = client.get(reverse("social:person-update")) assert response.status_code == HTTPStatus.OK - assert response.templates[0].name == "activitypub/person_form.html" + assert response.templates[0].name == "social/person_form.html" assert b"Bio" in response.content diff --git a/democrasite/social/tests/test_urls.py b/democrasite/social/tests/test_urls.py new file mode 100644 index 00000000..2611c47c --- /dev/null +++ b/democrasite/social/tests/test_urls.py @@ -0,0 +1,84 @@ +from django.urls import resolve +from django.urls import reverse + +from democrasite.social.models import Note +from democrasite.social.models import Person + + +def test_list(): + assert reverse("social:note-list") == "/social/notes/" + assert resolve("/social/notes/").view_name == "social:note-list" + + +def test_create(): + assert reverse("social:note-create") == "/social/notes/create/" + assert resolve("/social/notes/create/").view_name == "social:note-create" + + +def test_note_detail(note: Note): + assert ( + reverse("social:note-detail", kwargs={"pk": note.id}) + == f"/social/notes/{note.id}/" + ) + assert resolve(f"/social/notes/{note.id}/").view_name == "social:note-detail" + + +def test_note_reply(note: Note): + assert ( + reverse("social:note-reply", kwargs={"pk": note.id}) + == f"/social/notes/{note.id}/reply/" + ) + assert resolve(f"/social/notes/{note.id}/reply/").view_name == "social:note-reply" + + +def test_note_like(note: Note): + assert ( + reverse("social:note-like", kwargs={"pk": note.id}) + == f"/social/notes/{note.id}/like/" + ) + assert resolve(f"/social/notes/{note.id}/like/").view_name == "social:note-like" + + +def test_note_repost(note: Note): + assert ( + reverse("social:note-repost", kwargs={"pk": note.id}) + == f"/social/notes/{note.id}/repost/" + ) + assert resolve(f"/social/notes/{note.id}/repost/").view_name == "social:note-repost" + + +def test_person_create(): + assert reverse("social:person-create") == "/social/person/create/" + assert resolve("/social/person/create/").view_name == "social:person-create" + + +def test_person_update(): + assert reverse("social:person-update") == "/social/person/update/" + assert resolve("/social/person/update/").view_name == "social:person-update" + + +def test_following(): + assert reverse("social:following-notes") == "/social/person/following/" + assert resolve("/social/person/following/").view_name == "social:following-notes" + + +def test_person_detail(person: Person): + assert ( + reverse("social:person-detail", kwargs={"username": person.display_name}) + == f"/social/person/{person.display_name}/" + ) + assert ( + resolve(f"/social/person/{person.display_name}/").view_name + == "social:person-detail" + ) + + +def test_follow(person: Person): + assert ( + reverse("social:person-follow", kwargs={"username": person.display_name}) + == f"/social/person/{person.display_name}/follow/" + ) + assert ( + resolve(f"/social/person/{person.display_name}/follow/").view_name + == "social:person-follow" + ) diff --git a/democrasite/activitypub/tests/test_views.py b/democrasite/social/tests/test_views.py similarity index 94% rename from democrasite/activitypub/tests/test_views.py rename to democrasite/social/tests/test_views.py index 52e32b7c..bc0e26f0 100644 --- a/democrasite/activitypub/tests/test_views.py +++ b/democrasite/social/tests/test_views.py @@ -13,9 +13,9 @@ from django.test import RequestFactory from django.urls import reverse -from democrasite.activitypub import views -from democrasite.activitypub.models import Note -from democrasite.activitypub.models import Person +from democrasite.social import views +from democrasite.social.models import Note +from democrasite.social.models import Person from democrasite.users.models import User from .factories import NoteFactory @@ -27,7 +27,7 @@ def test_queryset(self, client: Client): batch_size = 3 NoteFactory.create_batch(batch_size) - response = client.get(reverse("activitypub:note-list")) + response = client.get(reverse("social:note-list")) notes = response.context["object_list"] assert len(notes) == batch_size for i in range(1, batch_size): @@ -54,7 +54,7 @@ def test_no_auth(self, rf: RequestFactory): assert isinstance(response, HttpResponseRedirect) assert response.status_code == HTTPStatus.FOUND - assert response.url == reverse("activitypub:note-list") + assert response.url == reverse("social:note-list") messages_sent = [m.message for m in get_messages(request)] assert messages_sent == [ @@ -75,7 +75,7 @@ def test_no_person(self, rf: RequestFactory, user: User): assert isinstance(response, HttpResponseRedirect) assert response.status_code == HTTPStatus.FOUND - assert response.url == reverse("activitypub:note-list") + assert response.url == reverse("social:note-list") messages_sent = [m.message for m in get_messages(request)] assert messages_sent == [ @@ -230,7 +230,7 @@ def test_unauthenticated(self, rf: RequestFactory): assert response.status_code == HTTPStatus.FOUND assert ( response.url - == f"{reverse(settings.LOGIN_URL)}?next={reverse('activitypub:note-list')}" + == f"{reverse(settings.LOGIN_URL)}?next={reverse('social:note-list')}" ) messages_sent = [m.message for m in get_messages(request)] @@ -248,7 +248,7 @@ def test_person_exists(self, rf: RequestFactory, person: Person): assert response.status_code == HTTPStatus.FOUND assert ( response.url - == f"{reverse('activitypub:person-detail', args=[person.display_name])}" + == f"{reverse('social:person-detail', args=[person.display_name])}" ) messages_sent = [m.message for m in get_messages(request)] @@ -259,7 +259,7 @@ def test_create_person(self, user: User, client: Client): client.force_login(user) - response = client.post(reverse("activitypub:person-create")) + response = client.post(reverse("social:person-create")) user.refresh_from_db() assert hasattr(user, "person") diff --git a/democrasite/activitypub/urls.py b/democrasite/social/urls.py similarity index 94% rename from democrasite/activitypub/urls.py rename to democrasite/social/urls.py index 7cf2234f..c7367113 100644 --- a/democrasite/activitypub/urls.py +++ b/democrasite/social/urls.py @@ -1,10 +1,10 @@ -"""URLs for ActivityPub views.""" +"""URLs for Social views.""" from django.urls import path from . import views -app_name = "activitypub" +app_name = "social" urlpatterns = [ path("notes/", views.note_list_view, name="note-list"), path("notes/create/", views.note_create_view, name="note-create"), diff --git a/democrasite/activitypub/views.py b/democrasite/social/views.py similarity index 97% rename from democrasite/activitypub/views.py rename to democrasite/social/views.py index 03a431d9..a4240cfa 100644 --- a/democrasite/activitypub/views.py +++ b/democrasite/social/views.py @@ -53,7 +53,7 @@ def handle_no_permission(self): messages.error( self.request, "You must have an ActivityPub profile to access this page." ) - return HttpResponseRedirect(reverse("activitypub:note-list")) + return HttpResponseRedirect(reverse("social:note-list")) def require_user_profile(view_func): @@ -173,12 +173,12 @@ def post(self, request): """Ensure the user does not already have a Person profile.""" if not self.request.user.is_authenticated: messages.info(request, "You must be logged in to do that.") - return redirect_to_login(reverse("activitypub:note-list")) + return redirect_to_login(reverse("social:note-list")) try: if self.request.user.person: messages.info(request, "You already have an ActivityPub Profile!") return redirect( - "activitypub:person-detail", + "social:person-detail", username=request.user.person.display_name, ) except Person.DoesNotExist: diff --git a/democrasite/static/css/activitypub.css b/democrasite/static/css/social.css similarity index 100% rename from democrasite/static/css/activitypub.css rename to democrasite/static/css/social.css diff --git a/democrasite/templates/base.html b/democrasite/templates/base.html index 07cf5678..32564236 100644 --- a/democrasite/templates/base.html +++ b/democrasite/templates/base.html @@ -114,7 +114,7 @@