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

View file

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

View file

@ -7,4 +7,5 @@ def config_context(request):
"config_identity": ( "config_identity": (
Config.load_identity(request.identity) if request.identity else None 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; text-decoration: none;
} }
p a {
text-decoration: underline;
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
html:focus-within { html:focus-within {
scroll-behavior: auto; 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 django.urls import path
from activities.views import posts, timelines from activities.views import posts, timelines
from core import views as core from core import views as core
from stator import views as stator 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 = [ urlpatterns = [
path("", core.homepage), path("", core.homepage),
@ -13,16 +13,53 @@ urlpatterns = [
path("notifications/", timelines.Notifications.as_view()), path("notifications/", timelines.Notifications.as_view()),
path("local/", timelines.Local.as_view()), path("local/", timelines.Local.as_view()),
path("federated/", timelines.Federated.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( path(
"settings/system/domains/<domain>/delete/", "settings/",
settings_system.DomainDeletePage.as_view(), 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 # Identity views
path("@<handle>/", identity.ViewIdentity.as_view()), path("@<handle>/", identity.ViewIdentity.as_view()),
@ -49,5 +86,5 @@ urlpatterns = [
# Task runner # Task runner
path(".stator/runner/", stator.RequestRunner.as_view()), path(".stator/runner/", stator.RequestRunner.as_view()),
# Django admin # 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" %} {% extends "base.html" %}
{% block title %}Add Domain - System Settings{% endblock %} {% block title %}Add Domain - Admin{% endblock %}
{% block content %} {% block content %}
{% block menu %} {% block menu %}
{% include "settings/_settings_system_menu.html" %} {% include "admin/_menu.html" %}
{% endblock %} {% endblock %}
<form action="." method="POST"> <form action="." method="POST">
<h1>Add A Domain</h1> <h1>Add A Domain</h1>

View file

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

View file

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

View file

@ -1,10 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ section.title }} - System Settings{% endblock %} {% block title %}{{ section.title }} - Admin{% endblock %}
{% block content %} {% block content %}
{% block menu %} {% block menu %}
{% include "settings/_settings_system_menu.html" %} {% include "admin/_menu.html" %}
{% endblock %} {% endblock %}
<section class="icon-menu"> <section class="icon-menu">
{% for domain in domains %} {% for domain in domains %}
@ -21,7 +21,7 @@
{% empty %} {% empty %}
<p class="option empty">You have no domains set up.</p> <p class="option empty">You have no domains set up.</p>
{% endfor %} {% 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 <i class="fa-solid fa-plus"></i> Add a domain
</a> </a>
</section> </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" %} {% extends "base.html" %}
{% block title %}{{ section.title }} - System Settings{% endblock %} {% block title %}{{ section.title }} - Admin{% endblock %}
{% block content %} {% block content %}
{% block menu %} {% block menu %}
{% include "settings/_settings_system_menu.html" %} {% include "admin/_menu.html" %}
{% endblock %} {% endblock %}
<form action="." method="POST"> <form action="." method="POST">
{% csrf_token %} {% 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 %}> <a href="/compose/" title="Compose" {% if top_section == "compose" %}class="selected"{% endif %}>
<i class="fa-solid fa-feather"></i> Compose <i class="fa-solid fa-feather"></i> Compose
</a> </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 <i class="fa-solid fa-gear"></i> Settings
</a> </a>
{% if request.user.admin %} {% 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 <i class="fa-solid fa-toolbox"></i> Admin
</a> </a>
{% endif %} {% 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) @admin.register(User)
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
pass list_display = ["email", "created", "last_seen", "admin", "moderator", "banned"]
@admin.register(UserEvent) @admin.register(UserEvent)
@ -21,6 +21,7 @@ class UserEventAdmin(admin.ModelAdmin):
@admin.register(Identity) @admin.register(Identity)
class IdentityAdmin(admin.ModelAdmin): class IdentityAdmin(admin.ModelAdmin):
list_display = ["id", "handle", "actor_uri", "state", "local"] list_display = ["id", "handle", "actor_uri", "state", "local"]
list_filter = ["local"]
raw_id_fields = ["users"] raw_id_fields = ["users"]
actions = ["force_update"] actions = ["force_update"]
readonly_fields = ["actor_json"] 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: class IdentityMiddleware:
@ -17,6 +19,7 @@ class IdentityMiddleware:
else: else:
try: try:
request.identity = Identity.objects.get(id=identity_id) request.identity = Identity.objects.get(id=identity_id)
User.objects.filter(pk=request.user.pk).update(last_seen=timezone.now())
except Identity.DoesNotExist: except Identity.DoesNotExist:
request.identity = None 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) updated = models.DateTimeField(auto_now=True)
class urls(urlman.Urls): class urls(urlman.Urls):
root = "/settings/system/domains/" root = "/admin/domains/"
create = "/settings/system/domains/create/" create = "/admin/domains/create/"
edit = "/settings/system/domains/{self.domain}/" edit = "/admin/domains/{self.domain}/"
delete = "/settings/system/domains/{self.domain}/delete/" delete = "/admin/domains/{self.domain}/delete/"
@classmethod @classmethod
def get_remote_domain(cls, domain: str) -> "Domain": def get_remote_domain(cls, domain: str) -> "Domain":

View file

@ -38,6 +38,7 @@ class User(AbstractBaseUser):
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True) updated = models.DateTimeField(auto_now=True)
last_seen = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = "email" USERNAME_FIELD = "email"
EMAIL_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 core.models import Config
from users.decorators import admin_required from users.decorators import admin_required
from users.models import Domain from users.models import Domain, Identity, User
@method_decorator(admin_required, name="dispatch") @method_decorator(admin_required, name="dispatch")
class SystemSettingsRoot(RedirectView): class AdminRoot(RedirectView):
url = "/settings/system/basic/" pattern_name = "admin_basic"
@method_decorator(admin_required, name="dispatch") @method_decorator(admin_required, name="dispatch")
class SystemSettingsPage(FormView): class AdminSettingsPage(FormView):
""" """
Shows a settings page dynamically created from our settings layout 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! 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 options_class = Config.SystemOptions
section: ClassVar[str] section: ClassVar[str]
options: Dict[str, Dict[str, str]] options: Dict[str, Dict[str, str]]
extra_context = {"top_section": "settings_system"}
def get_form_class(self): def get_form_class(self):
# Create the fields dict from the config object # Create the fields dict from the config object
fields = {} fields = {}
@ -84,7 +82,7 @@ class SystemSettingsPage(FormView):
return redirect(".") return redirect(".")
class BasicPage(SystemSettingsPage): class BasicPage(AdminSettingsPage):
section = "basic" section = "basic"
@ -103,7 +101,7 @@ class BasicPage(SystemSettingsPage):
@method_decorator(admin_required, name="dispatch") @method_decorator(admin_required, name="dispatch")
class DomainsPage(TemplateView): class DomainsPage(TemplateView):
template_name = "settings/settings_system_domains.html" template_name = "admin/domains.html"
def get_context_data(self): def get_context_data(self):
return { return {
@ -115,7 +113,7 @@ class DomainsPage(TemplateView):
@method_decorator(admin_required, name="dispatch") @method_decorator(admin_required, name="dispatch")
class DomainCreatePage(FormView): class DomainCreatePage(FormView):
template_name = "settings/settings_system_domain_create.html" template_name = "admin/domain_create.html"
extra_context = {"section": "domains"} extra_context = {"section": "domains"}
class form_class(forms.Form): class form_class(forms.Form):
@ -175,7 +173,7 @@ class DomainCreatePage(FormView):
@method_decorator(admin_required, name="dispatch") @method_decorator(admin_required, name="dispatch")
class DomainEditPage(FormView): class DomainEditPage(FormView):
template_name = "settings/settings_system_domain_edit.html" template_name = "admin/domain_edit.html"
extra_context = {"section": "domains"} extra_context = {"section": "domains"}
class form_class(forms.Form): class form_class(forms.Form):
@ -221,7 +219,7 @@ class DomainEditPage(FormView):
@method_decorator(admin_required, name="dispatch") @method_decorator(admin_required, name="dispatch")
class DomainDeletePage(TemplateView): class DomainDeletePage(TemplateView):
template_name = "settings/settings_system_domain_delete.html" template_name = "admin/domain_delete.html"
def dispatch(self, request, domain): def dispatch(self, request, domain):
self.domain = get_object_or_404( self.domain = get_object_or_404(
@ -241,3 +239,27 @@ class DomainDeletePage(TemplateView):
raise ValueError("Tried to delete domain with identities!") raise ValueError("Tried to delete domain with identities!")
self.domain.delete() self.domain.delete()
return redirect("/settings/system/domains/") 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 core.models import Config
from users.decorators import identity_required 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") @method_decorator(identity_required, name="dispatch")
class IdentitySettingsRoot(RedirectView): class SettingsRoot(RedirectView):
url = "/settings/interface/" url = "/settings/interface/"
class IdentitySettingsPage(SystemSettingsPage): class SettingsPage(AdminSettingsPage):
""" """
Shows a settings page dynamically created from our settings layout 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! at the bottom of the page. Don't add this to a URL directly - subclass!
""" """
extra_context = {"top_section": "settings"}
options_class = Config.IdentityOptions options_class = Config.IdentityOptions
template_name = "settings/settings_identity.html" template_name = "settings/settings.html"
def load_config(self): def load_config(self):
return Config.load_identity(self.request.identity) return Config.load_identity(self.request.identity)
@ -29,7 +27,7 @@ class IdentitySettingsPage(SystemSettingsPage):
Config.set_identity(self.request.identity, key, value) Config.set_identity(self.request.identity, key, value)
class InterfacePage(IdentitySettingsPage): class InterfacePage(SettingsPage):
section = "interface" section = "interface"