Skip to content

Commit df1c99e

Browse files
authored
Microblogging app (#215)
* minor improvements * Implemented basic microblog * Add profile page to microblog * improve templates * test fix * minor improvements * Added tests * Create note snippet * Added likes and reposts * Added follow button and following feed * More tests * Moved app navbar links right * Added fixture data * fix test * Improve coverage
1 parent 0b1f263 commit df1c99e

51 files changed

Lines changed: 2179 additions & 129 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

config/settings/base.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"allauth.account",
7070
"allauth.socialaccount",
7171
"django_celery_beat",
72+
"mptt",
7273
# django rest framework
7374
"rest_framework",
7475
"rest_framework.authtoken",
@@ -83,6 +84,7 @@
8384
# Custom apps
8485
"democrasite.users",
8586
"democrasite.webiscite",
87+
"democrasite.activitypub",
8688
]
8789
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
8890
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
@@ -232,12 +234,6 @@
232234
ADMINS = [("Admin", "admin@democrasite.tech")]
233235
# https://docs.djangoproject.com/en/dev/ref/settings/#managers
234236
MANAGERS = ADMINS
235-
# https://cookiecutter-django.readthedocs.io/en/latest/settings.html#other-environment-settings
236-
# Force the `admin` sign in process to go through the `django-allauth` workflow
237-
# Note that this would require a social account to be marked as an admin using
238-
# ``user.is_staff = True``, or require being logged in to a social account before
239-
# being able to log in to the admin site since there is no local account login page.
240-
DJANGO_ADMIN_FORCE_ALLAUTH = env.bool("DJANGO_ADMIN_FORCE_ALLAUTH", default=False)
241237

242238
# LOGGING
243239
# ------------------------------------------------------------------------------

config/settings/local.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@
2828
"localhost",
2929
"0.0.0.0", # noqa: S104
3030
"127.0.0.1",
31-
# For frontend
32-
"django",
3331
# For docker (healtcheck)
3432
"democrasite-local-django",
3533
]
@@ -73,7 +71,7 @@
7371
"SHOW_TEMPLATE_CONTEXT": True,
7472
}
7573
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips
76-
INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"]
74+
INTERNAL_IPS = ["127.0.0.1", "10.0.2.2", "192.168.65.1"]
7775
if env("USE_DOCKER", default="no") == "yes":
7876
import socket
7977

config/settings/test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@
3333
# ------------------------------------------------------------------------------
3434
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
3535
TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore[index]
36+
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips
37+
# Required to have "debug=True" in tempalte context
38+
INTERNAL_IPS = ["127.0.0.1"]

config/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
# User management
2424
path("users/", include("democrasite.users.urls", namespace="users")),
2525
path("accounts/", include("allauth.urls")),
26+
# ActivityPub
27+
path(
28+
"activitypub/", include("democrasite.activitypub.urls", namespace="activitypub")
29+
),
2630
# webiscite
2731
path("", include("democrasite.webiscite.urls", namespace="webiscite")),
2832
# Media files

democrasite/activitypub/__init__.py

Whitespace-only changes.

democrasite/activitypub/admin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.contrib import admin
2+
3+
from democrasite.activitypub.models import Note
4+
from democrasite.activitypub.models import Person
5+
6+
# Register your models here.
7+
admin.site.register(Person)
8+
admin.site.register(Note)

democrasite/activitypub/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class ActivitypubConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "democrasite.activitypub"
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
[
2+
{
3+
"model": "users.user",
4+
"pk": 1,
5+
"fields": {
6+
"password": "!QOegZX0EFLTPMUheUJaZHT2iBlDjTUVteO1ERFoL",
7+
"last_login": "2025-07-05T16:23:01.641Z",
8+
"is_superuser": false,
9+
"username": "test",
10+
"email": "",
11+
"is_staff": false,
12+
"is_active": true,
13+
"date_joined": "2025-07-04T23:22:05.325Z",
14+
"name": "",
15+
"groups": [],
16+
"user_permissions": []
17+
}
18+
},
19+
{
20+
"model": "users.user",
21+
"pk": 2,
22+
"fields": {
23+
"password": "!QOegZX0EFLTPMUheUJaZHT2iBlDjTUVteO1ERFoL",
24+
"last_login": "2025-07-05T00:26:58.691Z",
25+
"is_superuser": true,
26+
"username": "test2",
27+
"email": "",
28+
"is_staff": true,
29+
"is_active": true,
30+
"date_joined": "2025-07-05T00:26:54.979Z",
31+
"name": "",
32+
"groups": [],
33+
"user_permissions": []
34+
}
35+
},
36+
{
37+
"model": "activitypub.person",
38+
"pk": 1,
39+
"fields": {
40+
"created": "2025-07-04T23:22:11.652Z",
41+
"modified": "2025-07-04T23:22:11.652Z",
42+
"user": 1,
43+
"private_key": "private_key_placeholder",
44+
"public_key": "public_key_placeholder",
45+
"bio": "",
46+
"following": []
47+
}
48+
},
49+
{
50+
"model": "activitypub.person",
51+
"pk": 2,
52+
"fields": {
53+
"created": "2025-07-05T16:15:12.547Z",
54+
"modified": "2025-07-05T16:15:12.547Z",
55+
"user": 2,
56+
"private_key": "private_key_placeholder",
57+
"public_key": "public_key_placeholder",
58+
"bio": "",
59+
"following": []
60+
}
61+
},
62+
{
63+
"model": "activitypub.like",
64+
"pk": 1,
65+
"fields": { "person": 2, "note": 1, "created": "2025-07-05T16:15:47.380Z" }
66+
},
67+
{
68+
"model": "activitypub.like",
69+
"pk": 2,
70+
"fields": { "person": 2, "note": 2, "created": "2025-07-05T16:15:49.455Z" }
71+
},
72+
{
73+
"model": "activitypub.like",
74+
"pk": 3,
75+
"fields": { "person": 1, "note": 2, "created": "2025-07-05T16:23:07.500Z" }
76+
},
77+
{
78+
"model": "activitypub.repost",
79+
"pk": 1,
80+
"fields": { "person": 2, "note": 1, "created": "2025-07-05T16:15:30.859Z" }
81+
},
82+
{
83+
"model": "activitypub.repost",
84+
"pk": 2,
85+
"fields": { "person": 2, "note": 4, "created": "2025-07-05T16:17:12.726Z" }
86+
},
87+
{
88+
"model": "activitypub.repost",
89+
"pk": 3,
90+
"fields": { "person": 1, "note": 2, "created": "2025-07-05T16:23:04.907Z" }
91+
},
92+
{
93+
"model": "activitypub.repost",
94+
"pk": 4,
95+
"fields": { "person": 1, "note": 5, "created": "2025-07-05T16:23:10.741Z" }
96+
},
97+
{
98+
"model": "activitypub.note",
99+
"pk": 1,
100+
"fields": {
101+
"created": "2025-07-04T23:22:33.271Z",
102+
"modified": "2025-07-04T23:22:33.271Z",
103+
"author": 1,
104+
"content": "This is a note.",
105+
"in_reply_to": null,
106+
"lft": 1,
107+
"rght": 2,
108+
"tree_id": 1,
109+
"level": 0
110+
}
111+
},
112+
{
113+
"model": "activitypub.note",
114+
"pk": 2,
115+
"fields": {
116+
"created": "2025-07-04T23:22:43.126Z",
117+
"modified": "2025-07-04T23:22:43.126Z",
118+
"author": 1,
119+
"content": "This is another note.",
120+
"in_reply_to": null,
121+
"lft": 1,
122+
"rght": 6,
123+
"tree_id": 2,
124+
"level": 0
125+
}
126+
},
127+
{
128+
"model": "activitypub.note",
129+
"pk": 3,
130+
"fields": {
131+
"created": "2025-07-04T23:23:17.547Z",
132+
"modified": "2025-07-04T23:23:17.547Z",
133+
"author": 1,
134+
"content": "This is a reply to that other note.",
135+
"in_reply_to": 2,
136+
"lft": 2,
137+
"rght": 5,
138+
"tree_id": 2,
139+
"level": 1
140+
}
141+
},
142+
{
143+
"model": "activitypub.note",
144+
"pk": 4,
145+
"fields": {
146+
"created": "2025-07-05T16:15:43.177Z",
147+
"modified": "2025-07-05T16:15:43.177Z",
148+
"author": 2,
149+
"content": "This is a note by a different user",
150+
"in_reply_to": null,
151+
"lft": 1,
152+
"rght": 2,
153+
"tree_id": 3,
154+
"level": 0
155+
}
156+
},
157+
{
158+
"model": "activitypub.note",
159+
"pk": 5,
160+
"fields": {
161+
"created": "2025-07-05T16:16:39.332Z",
162+
"modified": "2025-07-05T16:16:39.332Z",
163+
"author": 2,
164+
"content": "This is a reply to a reply by a different user! (I need to improve the display of replies to replies in the note detail view)",
165+
"in_reply_to": 3,
166+
"lft": 3,
167+
"rght": 4,
168+
"tree_id": 2,
169+
"level": 2
170+
}
171+
}
172+
]

democrasite/activitypub/forms.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from django import forms
2+
3+
from .models import Note
4+
from .models import Person
5+
6+
7+
class NoteForm(forms.ModelForm):
8+
"""Form for creating or replying to a note."""
9+
10+
content = forms.CharField(
11+
max_length=500,
12+
widget=forms.Textarea(
13+
attrs={"rows": 3, "placeholder": "Share your thoughts..."}
14+
),
15+
help_text="Your note content (max 500 characters).",
16+
)
17+
18+
class Meta:
19+
model = Note
20+
fields = ["content"]
21+
22+
23+
class PersonForm(forms.ModelForm):
24+
"""Form for creating or updating a person's profile."""
25+
26+
bio = forms.CharField(
27+
max_length=500,
28+
required=False,
29+
widget=forms.Textarea(attrs={"rows": 3, "placeholder": "Describe yourself"}),
30+
help_text="Your bio content (max 500 characters).",
31+
)
32+
33+
class Meta:
34+
model = Person
35+
fields = ["bio"]
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Generated by Django 5.1.6 on 2025-06-27 21:07
2+
3+
import django.db.models.deletion
4+
import django.utils.timezone
5+
import model_utils.fields
6+
import mptt.fields
7+
from django.conf import settings
8+
from django.db import migrations, models
9+
10+
11+
class Migration(migrations.Migration):
12+
13+
initial = True
14+
15+
dependencies = [
16+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
17+
]
18+
19+
operations = [
20+
migrations.CreateModel(
21+
name='Note',
22+
fields=[
23+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
24+
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
25+
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
26+
('content', models.TextField()),
27+
('lft', models.PositiveIntegerField(editable=False)),
28+
('rght', models.PositiveIntegerField(editable=False)),
29+
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
30+
('level', models.PositiveIntegerField(editable=False)),
31+
('in_reply_to', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='replies', to='activitypub.note')),
32+
],
33+
options={
34+
'ordering': ['-created'],
35+
},
36+
),
37+
migrations.CreateModel(
38+
name='Like',
39+
fields=[
40+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
41+
('created', models.DateTimeField(auto_now_add=True)),
42+
('note', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='activitypub.note')),
43+
],
44+
options={
45+
'ordering': ['-created'],
46+
},
47+
),
48+
migrations.CreateModel(
49+
name='Person',
50+
fields=[
51+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
52+
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
53+
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
54+
('private_key', models.TextField()),
55+
('public_key', models.TextField()),
56+
('bio', models.TextField(blank=True, help_text='A short biography or description of the person')),
57+
('following', models.ManyToManyField(blank=True, help_text='People this person is following', related_name='followers', to='activitypub.person')),
58+
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
59+
],
60+
options={
61+
'verbose_name_plural': 'People',
62+
},
63+
),
64+
migrations.AddField(
65+
model_name='note',
66+
name='author',
67+
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='notes', to='activitypub.person'),
68+
),
69+
migrations.AddField(
70+
model_name='note',
71+
name='likes',
72+
field=models.ManyToManyField(blank=True, related_name='likes', through='activitypub.Like', to='activitypub.person'),
73+
),
74+
migrations.AddField(
75+
model_name='like',
76+
name='person',
77+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='activitypub.person'),
78+
),
79+
migrations.CreateModel(
80+
name='Repost',
81+
fields=[
82+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
83+
('created', models.DateTimeField(auto_now_add=True)),
84+
('note', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='activitypub.note')),
85+
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='activitypub.person')),
86+
],
87+
options={
88+
'ordering': ['-created'],
89+
'unique_together': {('person', 'note')},
90+
},
91+
),
92+
migrations.AddField(
93+
model_name='note',
94+
name='reposts',
95+
field=models.ManyToManyField(blank=True, related_name='reposts', through='activitypub.Repost', to='activitypub.person'),
96+
),
97+
migrations.AlterUniqueTogether(
98+
name='like',
99+
unique_together={('person', 'note')},
100+
),
101+
]

0 commit comments

Comments
 (0)