Call it admin rather than system settings

This commit is contained in:
Andrew Godwin 2022-11-16 21:42:25 -07:00
parent 9d97fc92d8
commit 5b34ea46c3
27 changed files with 273 additions and 111 deletions

View file

@ -0,0 +1,18 @@
# 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),
),
]

View file

@ -117,7 +117,7 @@ class Post(StatorModel):
)
# Hashtags in the post
hashtags = models.JSONField(default=[])
hashtags = models.JSONField(blank=True, null=True)
# When the post was originally created (as opposed to when we received it)
published = models.DateTimeField(default=timezone.now)
@ -296,36 +296,38 @@ class Post(StatorModel):
"""
Handles an incoming create request
"""
# Ensure the Create actor is the Post's attributedTo
if data["actor"] != data["object"]["attributedTo"]:
raise ValueError("Create actor does not match its Post object", data)
# Create it
post = cls.by_ap(data["object"], create=True, update=True)
# Make timeline events for followers
for follow in Follow.objects.filter(target=post.author, source__local=True):
TimelineEvent.add_post(follow.source, post)
# Make timeline events for mentions if they're local
for mention in post.mentions.all():
if mention.local:
TimelineEvent.add_mentioned(mention, post)
# Force it into fanned_out as it's not ours
post.transition_perform(PostStates.fanned_out)
with transaction.atomic():
# Ensure the Create actor is the Post's attributedTo
if data["actor"] != data["object"]["attributedTo"]:
raise ValueError("Create actor does not match its Post object", data)
# Create it
post = cls.by_ap(data["object"], create=True, update=True)
# Make timeline events for followers
for follow in Follow.objects.filter(target=post.author, source__local=True):
TimelineEvent.add_post(follow.source, post)
# Make timeline events for mentions if they're local
for mention in post.mentions.all():
if mention.local:
TimelineEvent.add_mentioned(mention, post)
# Force it into fanned_out as it's not ours
post.transition_perform(PostStates.fanned_out)
@classmethod
def handle_delete_ap(cls, data):
"""
Handles an incoming create request
"""
# Find our post by ID if we have one
try:
post = cls.by_object_uri(data["object"]["id"])
except cls.DoesNotExist:
# It's already been deleted
return
# Ensure the actor on the request authored the post
if not post.author.actor_uri == data["actor"]:
raise ValueError("Actor on delete does not match object")
post.delete()
with transaction.atomic():
# Find our post by ID if we have one
try:
post = cls.by_object_uri(data["object"]["id"])
except cls.DoesNotExist:
# It's already been deleted
return
# Ensure the actor on the request authored the post
if not post.author.actor_uri == data["actor"]:
raise ValueError("Actor on delete does not match object")
post.delete()
def debug_fetch(self):
"""

View file

@ -1,6 +1,6 @@
from typing import Dict
from django.db import models
from django.db import models, transaction
from django.utils import timezone
from activities.models.fan_out import FanOut
@ -272,31 +272,33 @@ class PostInteraction(StatorModel):
"""
Handles an incoming announce/like
"""
# Create it
interaction = cls.by_ap(data, create=True)
# Boosts (announces) go to everyone who follows locally
if interaction.type == cls.Types.boost:
for follow in Follow.objects.filter(
target=interaction.identity, source__local=True
):
TimelineEvent.add_post_interaction(follow.source, interaction)
# Likes go to just the author of the post
elif interaction.type == cls.Types.like:
TimelineEvent.add_post_interaction(interaction.post.author, interaction)
# Force it into fanned_out as it's not ours
interaction.transition_perform(PostInteractionStates.fanned_out)
with transaction.atomic():
# Create it
interaction = cls.by_ap(data, create=True)
# Boosts (announces) go to everyone who follows locally
if interaction.type == cls.Types.boost:
for follow in Follow.objects.filter(
target=interaction.identity, source__local=True
):
TimelineEvent.add_post_interaction(follow.source, interaction)
# Likes go to just the author of the post
elif interaction.type == cls.Types.like:
TimelineEvent.add_post_interaction(interaction.post.author, interaction)
# Force it into fanned_out as it's not ours
interaction.transition_perform(PostInteractionStates.fanned_out)
@classmethod
def handle_undo_ap(cls, data):
"""
Handles an incoming undo for a announce/like
"""
# Find it
interaction = cls.by_ap(data["object"])
# Verify the actor matches
if data["actor"] != interaction.identity.actor_uri:
raise ValueError("Actor mismatch on interaction undo")
# Delete all events that reference it
interaction.timeline_events.all().delete()
# Force it into undone_fanned_out as it's not ours
interaction.transition_perform(PostInteractionStates.undone_fanned_out)
with transaction.atomic():
# Find it
interaction = cls.by_ap(data["object"])
# Verify the actor matches
if data["actor"] != interaction.identity.actor_uri:
raise ValueError("Actor mismatch on interaction undo")
# Delete all events that reference it
interaction.timeline_events.all().delete()
# Force it into undone_fanned_out as it's not ours
interaction.transition_perform(PostInteractionStates.undone_fanned_out)

View file

@ -108,7 +108,6 @@ class Boost(View):
class Compose(FormView):
template_name = "activities/compose.html"
extra_context = {"top_section": "compose"}
class form_class(forms.Form):
text = forms.CharField(

View file

@ -7,4 +7,5 @@ def config_context(request):
"config_identity": (
Config.load_identity(request.identity) if request.identity else None
),
"top_section": request.path.strip("/").split("/")[0],
}

View file

@ -58,6 +58,10 @@ a {
text-decoration: none;
}
p a {
text-decoration: underline;
}
@media (prefers-reduced-motion: reduce) {
html:focus-within {
scroll-behavior: auto;

View file

@ -1,10 +1,10 @@
from django.contrib import admin
from django.contrib import admin as djadmin
from django.urls import path
from activities.views import posts, timelines
from core import views as core
from stator import views as stator
from users.views import activitypub, auth, identity, settings_identity, settings_system
from users.views import activitypub, admin, auth, identity, settings
urlpatterns = [
path("", core.homepage),
@ -13,16 +13,53 @@ urlpatterns = [
path("notifications/", timelines.Notifications.as_view()),
path("local/", timelines.Local.as_view()),
path("federated/", timelines.Federated.as_view()),
path("settings/", settings_identity.IdentitySettingsRoot.as_view()),
path("settings/interface/", settings_identity.InterfacePage.as_view()),
path("settings/system/", settings_system.SystemSettingsRoot.as_view()),
path("settings/system/basic/", settings_system.BasicPage.as_view()),
path("settings/system/domains/", settings_system.DomainsPage.as_view()),
path("settings/system/domains/create/", settings_system.DomainCreatePage.as_view()),
path("settings/system/domains/<domain>/", settings_system.DomainEditPage.as_view()),
path(
"settings/system/domains/<domain>/delete/",
settings_system.DomainDeletePage.as_view(),
"settings/",
settings.SettingsRoot.as_view(),
name="settings",
),
path(
"settings/interface/",
settings.InterfacePage.as_view(),
name="settings_interface",
),
path(
"admin/",
admin.AdminRoot.as_view(),
name="admin",
),
path(
"admin/basic/",
admin.BasicPage.as_view(),
name="admin_basic",
),
path(
"admin/domains/",
admin.DomainsPage.as_view(),
name="admin_domains",
),
path(
"admin/domains/create/",
admin.DomainCreatePage.as_view(),
name="admin_domains_create",
),
path(
"admin/domains/<domain>/",
admin.DomainEditPage.as_view(),
),
path(
"admin/domains/<domain>/delete/",
admin.DomainDeletePage.as_view(),
),
path(
"admin/users/",
admin.UsersPage.as_view(),
name="admin_users",
),
path(
"admin/identities/",
admin.IdentitiesPage.as_view(),
name="admin_identities",
),
# Identity views
path("@<handle>/", identity.ViewIdentity.as_view()),
@ -49,5 +86,5 @@ urlpatterns = [
# Task runner
path(".stator/runner/", stator.RequestRunner.as_view()),
# Django admin
path("djadmin/", admin.site.urls),
path("djadmin/", djadmin.site.urls),
]

View file

@ -0,0 +1,6 @@
<nav>
<a href="{% url "admin_basic" %}" {% if section == "basic" %}class="selected"{% endif %}>Basic</a>
<a href="{% url "admin_domains" %}" {% if section == "domains" %}class="selected"{% endif %}>Domains</a>
<a href="{% url "admin_users" %}" {% if section == "users" %}class="selected"{% endif %}>Users</a>
<a href="{% url "admin_identities" %}" {% if section == "identities" %}class="selected"{% endif %}>Identities</a>
</nav>

View file

@ -1,10 +1,10 @@
{% extends "base.html" %}
{% block title %}Add Domain - System Settings{% endblock %}
{% block title %}Add Domain - Admin{% endblock %}
{% block content %}
{% block menu %}
{% include "settings/_settings_system_menu.html" %}
{% include "admin/_menu.html" %}
{% endblock %}
<form action="." method="POST">
<h1>Add A Domain</h1>

View file

@ -1,10 +1,10 @@
{% extends "base.html" %}
{% block title %}Delete {{ domain.domain }} - System Settings{% endblock %}
{% block title %}Delete {{ domain.domain }} - Admin{% endblock %}
{% block content %}
{% block menu %}
{% include "settings/_settings_system_menu.html" %}
{% include "admin/_menu.html" %}
{% endblock %}
<form action="." method="POST">

View file

@ -1,10 +1,10 @@
{% extends "base.html" %}
{% block title %}{{ domain.domain }} - System Settings{% endblock %}
{% block title %}{{ domain.domain }} - Admin{% endblock %}
{% block content %}
{% block menu %}
{% include "settings/_settings_system_menu.html" %}
{% include "admin/_menu.html" %}
{% endblock %}
<form action="." method="POST">
{% csrf_token %}

View file

@ -1,10 +1,10 @@
{% extends "base.html" %}
{% block title %}{{ section.title }} - System Settings{% endblock %}
{% block title %}{{ section.title }} - Admin{% endblock %}
{% block content %}
{% block menu %}
{% include "settings/_settings_system_menu.html" %}
{% include "admin/_menu.html" %}
{% endblock %}
<section class="icon-menu">
{% for domain in domains %}
@ -21,7 +21,7 @@
{% empty %}
<p class="option empty">You have no domains set up.</p>
{% endfor %}
<a href="/settings/system/domains/create/" class="option new">
<a href="{% url "admin_domains_create" %}" class="option new">
<i class="fa-solid fa-plus"></i> Add a domain
</a>
</section>

View file

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block title %}Identities - Admin{% endblock %}
{% block content %}
{% block menu %}
{% include "admin/_menu.html" %}
{% endblock %}
<form>
<p>
Please use the <a href="/djadmin/users/identity/">Django Admin</a> for now.
</p>
</form>
{% endblock %}

View file

@ -1,10 +1,10 @@
{% extends "base.html" %}
{% block title %}{{ section.title }} - System Settings{% endblock %}
{% block title %}{{ section.title }} - Admin{% endblock %}
{% block content %}
{% block menu %}
{% include "settings/_settings_system_menu.html" %}
{% include "admin/_menu.html" %}
{% endblock %}
<form action="." method="POST">
{% csrf_token %}

View file

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block title %}Users - Admin{% endblock %}
{% block content %}
{% block menu %}
{% include "admin/_menu.html" %}
{% endblock %}
<form>
<p>
Please use the <a href="/djadmin/users/user/">Django Admin</a> for now.
</p>
</form>
{% endblock %}

View file

@ -31,11 +31,11 @@
<a href="/compose/" title="Compose" {% if top_section == "compose" %}class="selected"{% endif %}>
<i class="fa-solid fa-feather"></i> Compose
</a>
<a href="/settings/" title="Settings" {% if top_section == "settings" %}class="selected"{% endif %}>
<a href="{% url "settings" %}" title="Settings" {% if top_section == "settings" %}class="selected"{% endif %}>
<i class="fa-solid fa-gear"></i> Settings
</a>
{% if request.user.admin %}
<a href="/settings/system/" title="Admin" {% if top_section == "settings_system" %}class="selected"{% endif %}>
<a href="{% url "admin" %}" title="Admin" {% if top_section == "admin" %}class="selected"{% endif %}>
<i class="fa-solid fa-toolbox"></i> Admin
</a>
{% endif %}

View file

@ -1,5 +0,0 @@
<nav>
<a href="/settings/system/basic/" {% if section == "basic" %}class="selected"{% endif %}>Basic</a>
<a href="/settings/system/domains/" {% if section == "domains" %}class="selected"{% endif %}>Domains</a>
<a href="/settings/system/users/" {% if section == "users" %}class="selected"{% endif %}>Users</a>
</nav>

View file

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block title %}{{ section.title }} - Settings{% endblock %}
{% block content %}
{% block menu %}
{% include "settings/_menu.html" %}
{% endblock %}
<form action="." method="POST">
{% csrf_token %}
{% for field in form %}
{% include "forms/_field.html" %}
{% endfor %}
<div class="buttons">
<button>Save</button>
</div>
</form>
{% endblock %}

View file

@ -1,7 +0,0 @@
{% extends "settings/settings_system.html" %}
{% block title %}{{ section.title }} - Settings{% endblock %}
{% block menu %}
{% include "settings/_settings_identity_menu.html" %}
{% endblock %}

View file

@ -10,7 +10,7 @@ class DomainAdmin(admin.ModelAdmin):
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
pass
list_display = ["email", "created", "last_seen", "admin", "moderator", "banned"]
@admin.register(UserEvent)
@ -21,6 +21,7 @@ class UserEventAdmin(admin.ModelAdmin):
@admin.register(Identity)
class IdentityAdmin(admin.ModelAdmin):
list_display = ["id", "handle", "actor_uri", "state", "local"]
list_filter = ["local"]
raw_id_fields = ["users"]
actions = ["force_update"]
readonly_fields = ["actor_json"]

View file

@ -1,4 +1,6 @@
from users.models import Identity
from django.utils import timezone
from users.models import Identity, User
class IdentityMiddleware:
@ -17,6 +19,7 @@ class IdentityMiddleware:
else:
try:
request.identity = Identity.objects.get(id=identity_id)
User.objects.filter(pk=request.user.pk).update(last_seen=timezone.now())
except Identity.DoesNotExist:
request.identity = None

View file

@ -0,0 +1,34 @@
# 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",
),
),
]

View file

@ -49,10 +49,10 @@ class Domain(models.Model):
updated = models.DateTimeField(auto_now=True)
class urls(urlman.Urls):
root = "/settings/system/domains/"
create = "/settings/system/domains/create/"
edit = "/settings/system/domains/{self.domain}/"
delete = "/settings/system/domains/{self.domain}/delete/"
root = "/admin/domains/"
create = "/admin/domains/create/"
edit = "/admin/domains/{self.domain}/"
delete = "/admin/domains/{self.domain}/delete/"
@classmethod
def get_remote_domain(cls, domain: str) -> "Domain":

View file

@ -38,6 +38,7 @@ class User(AbstractBaseUser):
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
last_seen = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = "email"
EMAIL_FIELD = "email"

View file

@ -10,28 +10,26 @@ from django.views.generic import FormView, RedirectView, TemplateView
from core.models import Config
from users.decorators import admin_required
from users.models import Domain
from users.models import Domain, Identity, User
@method_decorator(admin_required, name="dispatch")
class SystemSettingsRoot(RedirectView):
url = "/settings/system/basic/"
class AdminRoot(RedirectView):
pattern_name = "admin_basic"
@method_decorator(admin_required, name="dispatch")
class SystemSettingsPage(FormView):
class AdminSettingsPage(FormView):
"""
Shows a settings page dynamically created from our settings layout
at the bottom of the page. Don't add this to a URL directly - subclass!
"""
template_name = "settings/settings_system.html"
template_name = "admin/settings.html"
options_class = Config.SystemOptions
section: ClassVar[str]
options: Dict[str, Dict[str, str]]
extra_context = {"top_section": "settings_system"}
def get_form_class(self):
# Create the fields dict from the config object
fields = {}
@ -84,7 +82,7 @@ class SystemSettingsPage(FormView):
return redirect(".")
class BasicPage(SystemSettingsPage):
class BasicPage(AdminSettingsPage):
section = "basic"
@ -103,7 +101,7 @@ class BasicPage(SystemSettingsPage):
@method_decorator(admin_required, name="dispatch")
class DomainsPage(TemplateView):
template_name = "settings/settings_system_domains.html"
template_name = "admin/domains.html"
def get_context_data(self):
return {
@ -115,7 +113,7 @@ class DomainsPage(TemplateView):
@method_decorator(admin_required, name="dispatch")
class DomainCreatePage(FormView):
template_name = "settings/settings_system_domain_create.html"
template_name = "admin/domain_create.html"
extra_context = {"section": "domains"}
class form_class(forms.Form):
@ -175,7 +173,7 @@ class DomainCreatePage(FormView):
@method_decorator(admin_required, name="dispatch")
class DomainEditPage(FormView):
template_name = "settings/settings_system_domain_edit.html"
template_name = "admin/domain_edit.html"
extra_context = {"section": "domains"}
class form_class(forms.Form):
@ -221,7 +219,7 @@ class DomainEditPage(FormView):
@method_decorator(admin_required, name="dispatch")
class DomainDeletePage(TemplateView):
template_name = "settings/settings_system_domain_delete.html"
template_name = "admin/domain_delete.html"
def dispatch(self, request, domain):
self.domain = get_object_or_404(
@ -241,3 +239,27 @@ class DomainDeletePage(TemplateView):
raise ValueError("Tried to delete domain with identities!")
self.domain.delete()
return redirect("/settings/system/domains/")
@method_decorator(admin_required, name="dispatch")
class UsersPage(TemplateView):
template_name = "admin/users.html"
def get_context_data(self):
return {
"users": User.objects.order_by("email"),
"section": "users",
}
@method_decorator(admin_required, name="dispatch")
class IdentitiesPage(TemplateView):
template_name = "admin/identities.html"
def get_context_data(self):
return {
"identities": Identity.objects.order_by("username"),
"section": "identities",
}

View file

@ -3,24 +3,22 @@ from django.views.generic import RedirectView
from core.models import Config
from users.decorators import identity_required
from users.views.settings_system import SystemSettingsPage
from users.views.admin import AdminSettingsPage
@method_decorator(identity_required, name="dispatch")
class IdentitySettingsRoot(RedirectView):
class SettingsRoot(RedirectView):
url = "/settings/interface/"
class IdentitySettingsPage(SystemSettingsPage):
class SettingsPage(AdminSettingsPage):
"""
Shows a settings page dynamically created from our settings layout
at the bottom of the page. Don't add this to a URL directly - subclass!
"""
extra_context = {"top_section": "settings"}
options_class = Config.IdentityOptions
template_name = "settings/settings_identity.html"
template_name = "settings/settings.html"
def load_config(self):
return Config.load_identity(self.request.identity)
@ -29,7 +27,7 @@ class IdentitySettingsPage(SystemSettingsPage):
Config.set_identity(self.request.identity, key, value)
class InterfacePage(IdentitySettingsPage):
class InterfacePage(SettingsPage):
section = "interface"