mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-21 23:01:00 +00:00
parent
81fa9a6d34
commit
8b3106b852
32 changed files with 558 additions and 95 deletions
|
@ -1115,6 +1115,11 @@ form .option-row .right button {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
padding-left: 20px;
|
||||||
|
border-left: 2px solid var(--color-bg-menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Logged out homepage */
|
/* Logged out homepage */
|
||||||
|
|
||||||
|
@ -1309,6 +1314,23 @@ table.metadata td .emoji {
|
||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Announcements */
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
background-color: var(--color-highlight);
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding: 5px 30px 5px 8px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement .dismiss {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Identity banner */
|
/* Identity banner */
|
||||||
|
|
||||||
.identity-banner {
|
.identity-banner {
|
||||||
|
|
|
@ -232,6 +232,7 @@ TEMPLATES = [
|
||||||
"django.contrib.auth.context_processors.auth",
|
"django.contrib.auth.context_processors.auth",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
"core.context.config_context",
|
"core.context.config_context",
|
||||||
|
"users.context.user_context",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,15 @@ from api.views import api_router, oauth
|
||||||
from core import views as core
|
from core import views as core
|
||||||
from mediaproxy import views as mediaproxy
|
from mediaproxy import views as mediaproxy
|
||||||
from stator import views as stator
|
from stator import views as stator
|
||||||
from users.views import activitypub, admin, auth, identity, report, settings
|
from users.views import (
|
||||||
|
activitypub,
|
||||||
|
admin,
|
||||||
|
announcements,
|
||||||
|
auth,
|
||||||
|
identity,
|
||||||
|
report,
|
||||||
|
settings,
|
||||||
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", core.homepage),
|
path("", core.homepage),
|
||||||
|
@ -162,9 +170,35 @@ urlpatterns = [
|
||||||
admin.EmojiCreate.as_view(),
|
admin.EmojiCreate.as_view(),
|
||||||
name="admin_emoji_create",
|
name="admin_emoji_create",
|
||||||
),
|
),
|
||||||
path("admin/emoji/<id>/enable/", admin.EmojiEnable.as_view()),
|
path("admin/emoji/<pk>/enable/", admin.EmojiEnable.as_view()),
|
||||||
path("admin/emoji/<id>/disable/", admin.EmojiEnable.as_view(enable=False)),
|
path("admin/emoji/<pk>/disable/", admin.EmojiEnable.as_view(enable=False)),
|
||||||
path("admin/emoji/<id>/delete/", admin.EmojiDelete.as_view()),
|
path("admin/emoji/<pk>/delete/", admin.EmojiDelete.as_view()),
|
||||||
|
path(
|
||||||
|
"admin/announcements/",
|
||||||
|
admin.AnnouncementsRoot.as_view(),
|
||||||
|
name="admin_announcements",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/announcements/create/",
|
||||||
|
admin.AnnouncementCreate.as_view(),
|
||||||
|
name="admin_announcement_create",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/announcements/<pk>/",
|
||||||
|
admin.AnnouncementEdit.as_view(),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/announcements/<pk>/delete/",
|
||||||
|
admin.AnnouncementDelete.as_view(),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/announcements/<pk>/publish/",
|
||||||
|
admin.AnnouncementPublish.as_view(),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/announcements/<pk>/unpublish/",
|
||||||
|
admin.AnnouncementUnpublish.as_view(),
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"admin/stator/",
|
"admin/stator/",
|
||||||
admin.Stator.as_view(),
|
admin.Stator.as_view(),
|
||||||
|
@ -222,6 +256,8 @@ urlpatterns = [
|
||||||
core.FlatPage.as_view(title="Server Rules", config_option="policy_rules"),
|
core.FlatPage.as_view(title="Server Rules", config_option="policy_rules"),
|
||||||
name="rules",
|
name="rules",
|
||||||
),
|
),
|
||||||
|
# Annoucements
|
||||||
|
path("announcements/<id>/dismiss/", announcements.AnnouncementDismiss.as_view()),
|
||||||
# Debug aids
|
# Debug aids
|
||||||
path("debug/json/", debug.JsonViewer.as_view()),
|
path("debug/json/", debug.JsonViewer.as_view()),
|
||||||
path("debug/404/", debug.NotFound.as_view()),
|
path("debug/404/", debug.NotFound.as_view()),
|
||||||
|
|
6
templates/_announcements.html
Normal file
6
templates/_announcements.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% for announcement in announcements %}
|
||||||
|
<div class="announcement">
|
||||||
|
<a hx-post="{{ announcement.urls.dismiss }}" hx-target="closest .announcement" hx-swap="delete" class="dismiss" title="Dismiss announcement"><i class="fa-solid fa-xmark"></i></a>
|
||||||
|
{{ announcement.html }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
|
@ -4,6 +4,9 @@
|
||||||
{% block title %}Home{% endblock %}
|
{% block title %}Home{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if page_obj.number == 1 %}
|
||||||
|
{% include "_announcements.html" %}
|
||||||
|
{% endif %}
|
||||||
{% for event in page_obj %}
|
{% for event in page_obj %}
|
||||||
{% if event.type == "post" %}
|
{% if event.type == "post" %}
|
||||||
{% include "activities/_post.html" with post=event.subject_post %}
|
{% include "activities/_post.html" with post=event.subject_post %}
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
{% block title %}Local Timeline{% endblock %}
|
{% block title %}Local Timeline{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if page_obj.number == 1 %}
|
||||||
|
{% include "_announcements.html" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% for post in page_obj %}
|
{% for post in page_obj %}
|
||||||
{% include "activities/_post.html" with feedindex=forloop.counter %}
|
{% include "activities/_post.html" with feedindex=forloop.counter %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
{% block title %}Notifications{% endblock %}
|
{% block title %}Notifications{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if page_obj.number == 1 %}
|
||||||
|
{% include "_announcements.html" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="view-options">
|
<div class="view-options">
|
||||||
{% if notification_options.followed %}
|
{% if notification_options.followed %}
|
||||||
<a href=".?followed=false" class="selected"><i class="fa-solid fa-check"></i> Followers</a>
|
<a href=".?followed=false" class="selected"><i class="fa-solid fa-check"></i> Followers</a>
|
||||||
|
|
13
templates/admin/_pagination.html
Normal file
13
templates/admin/_pagination.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% load activity_tags %}
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if page_obj.paginator.count %}
|
||||||
|
<span class="count">{{ page_obj.paginator.count }} {{page_obj.paginator.count|pluralize:nouns }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
23
templates/admin/announcement_create.html
Normal file
23
templates/admin/announcement_create.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{% extends "settings/base.html" %}
|
||||||
|
|
||||||
|
{% block subtitle %}Create Announcement{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form action="." method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>Announcement</legend>
|
||||||
|
{% include "forms/_field.html" with field=form.text %}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Visibility</legend>
|
||||||
|
{% include "forms/_field.html" with field=form.published %}
|
||||||
|
{% include "forms/_field.html" with field=form.start %}
|
||||||
|
{% include "forms/_field.html" with field=form.end %}
|
||||||
|
</fieldset>
|
||||||
|
<div class="buttons">
|
||||||
|
<a href="{{ announcement.urls.admin_root }}" class="button secondary left">Back</a>
|
||||||
|
<button>Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
21
templates/admin/announcement_delete.html
Normal file
21
templates/admin/announcement_delete.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends "base_plain.html" %}
|
||||||
|
|
||||||
|
{% block title %}Delete Announcement - Admin{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Confirm Delete</h1>
|
||||||
|
<section class="">
|
||||||
|
<form action="." method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<p>Do you want to delete this announcement?</p>
|
||||||
|
|
||||||
|
<blockquote>{{ announcement.html }}</blockquote>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<a class="button" href="javascript:history.back()">Cancel</a>
|
||||||
|
<button class="delete">Delete</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
24
templates/admin/announcement_edit.html
Normal file
24
templates/admin/announcement_edit.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "settings/base.html" %}
|
||||||
|
|
||||||
|
{% block subtitle %}Announcement #{{ announcement.pk }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form action="." method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>Announcement</legend>
|
||||||
|
{% include "forms/_field.html" with field=form.text %}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Visibility</legend>
|
||||||
|
{% include "forms/_field.html" with field=form.published %}
|
||||||
|
{% include "forms/_field.html" with field=form.start %}
|
||||||
|
{% include "forms/_field.html" with field=form.end %}
|
||||||
|
</fieldset>
|
||||||
|
<div class="buttons">
|
||||||
|
<a href="{{ announcement.urls.admin_root }}" class="button secondary left">Back</a>
|
||||||
|
<a href="{{ announcement.urls.admin_delete }}" class="button delete">Delete</a>
|
||||||
|
<button>Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
49
templates/admin/announcements.html
Normal file
49
templates/admin/announcements.html
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{% extends "settings/base.html" %}
|
||||||
|
|
||||||
|
{% block subtitle %}Announcements{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="view-options">
|
||||||
|
<a href="{% url "admin_announcement_create" %}" class="button"><i class="fa-solid fa-plus"></i> Create</a>
|
||||||
|
</div>
|
||||||
|
<table class="items">
|
||||||
|
{% for announcement in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td class="icon">
|
||||||
|
<a href="{{ announcement.urls.admin_edit }}" class="overlay"></a>
|
||||||
|
<i class="fa-solid fa-bullhorn"></i>
|
||||||
|
</td>
|
||||||
|
<td class="name">
|
||||||
|
<a href="{{ announcement.urls.admin_edit }}" class="overlay"></a>
|
||||||
|
{{ announcement.html|truncatewords_html:"10" }}
|
||||||
|
<small>
|
||||||
|
{% if announcement.service_announcement %}{{ domain.service_domain }}{% endif %}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
<td class="stat">
|
||||||
|
{% if not announcement.published %}
|
||||||
|
Draft
|
||||||
|
{% elif not announcement.after_start %}
|
||||||
|
Awaiting Start
|
||||||
|
{% elif not announcement.before_end %}
|
||||||
|
Past End
|
||||||
|
{% else %}
|
||||||
|
Visible
|
||||||
|
{% endif %}
|
||||||
|
<small>State</small>
|
||||||
|
</td>
|
||||||
|
<td class="actions">
|
||||||
|
{% if not announcement.published %}
|
||||||
|
<a hx-post="{{ announcement.urls.admin_publish }}" title="Publish"><i class="fa-solid fa-bullhorn"></i></a>
|
||||||
|
{% else %}
|
||||||
|
<a hx-post="{{ announcement.urls.admin_unpublish }}" title="Unpublish"><i class="fa-solid fa-rotate-left"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ announcement.urls.admin_delete }}" title="Delete" class="danger"><i class="fa-solid fa-trash"></i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr class="empty"><td>You have no announcements.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% include "admin/_pagination.html" with nouns="announcement,announcements" %}
|
||||||
|
{% endblock %}
|
|
@ -53,15 +53,5 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
<div class="pagination">
|
{% include "admin/_pagination.html" with nouns="emoji,emoji" %}
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<a class="button" href=".?{% urlparams page=page_obj.previous_page_number %}">Previous Page</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.paginator.count %}
|
|
||||||
<span class="count">{{ page_obj.paginator.count }} emoji</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<a class="button" href=".?{% urlparams page=page_obj.next_page_number %}">Next Page</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -42,15 +42,5 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
<div class="pagination">
|
{% include "admin/_pagination.html" with nouns="domain,domains" %}
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<a class="button" href=".?{% urlparams page=page_obj.previous_page_number %}">Previous Page</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.paginator.count %}
|
|
||||||
<span class="count">{{ page_obj.paginator.count }} domain{{page_obj.paginator.count|pluralize }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<a class="button" href=".?{% urlparams page=page_obj.next_page_number %}">Next Page</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -50,15 +50,5 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
<div class="pagination">
|
{% include "admin/_pagination.html" with nouns="hashtag,hashtags" %}
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.paginator.count %}
|
|
||||||
<span class="count">{{ page_obj.paginator.count }} hashtag{{page_obj.paginator.count|pluralize }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -69,15 +69,5 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
<div class="pagination">
|
{% include "admin/_pagination.html" with nouns="identity,identities" %}
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<a class="button" href=".?{% urlparams page=page_obj.previous_page_number %}">Previous Page</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.paginator.count %}
|
|
||||||
<span class="count">{{ page_obj.paginator.count }} identit{{page_obj.paginator.count|pluralize:"y,ies" }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<a class="button" href=".?{% urlparams page=page_obj.next_page_number %}">Next Page</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -51,15 +51,5 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
<div class="pagination">
|
{% include "admin/_pagination.html" with nouns="invite,invites" %}
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.paginator.count %}
|
|
||||||
<span class="count">{{ page_obj.paginator.count }} invite{{page_obj.paginator.count|pluralize }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -44,15 +44,5 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
<div class="pagination">
|
{% include "admin/_pagination.html" with nouns="report,reports" %}
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}{% if all %}&all=true{% endif %}">Previous Page</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.paginator.count %}
|
|
||||||
<span class="count">{{ page_obj.paginator.count }} report{{page_obj.paginator.count|pluralize }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<a class="button" href=".?page={{ page_obj.next_page_number }}{% if all %}&all=true{% endif %}">Next Page</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -44,15 +44,5 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
<div class="pagination">
|
{% include "admin/_pagination.html" with nouns="user,users" %}
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<a class="button" href=".?{% urlparams page=page_obj.previous_page_number %}">Previous Page</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.paginator.count %}
|
|
||||||
<span class="count">{{ page_obj.paginator.count }} user{{page_obj.paginator.count|pluralize }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<a class="button" href=".?{% urlparams page=page_obj.next_page_number %}">Next Page</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -39,6 +39,9 @@
|
||||||
<a href="{% url "admin_policies" %}" {% if section == "policies" %}class="selected"{% endif %} title="Policies">
|
<a href="{% url "admin_policies" %}" {% if section == "policies" %}class="selected"{% endif %} title="Policies">
|
||||||
<i class="fa-solid fa-file-lines"></i> Policies
|
<i class="fa-solid fa-file-lines"></i> Policies
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url "admin_announcements" %}" {% if section == "announcements" %}class="selected"{% endif %} title="Announcements">
|
||||||
|
<i class="fa-solid fa-bullhorn"></i> Announcements
|
||||||
|
</a>
|
||||||
<a href="{% url "admin_domains" %}" {% if section == "domains" %}class="selected"{% endif %} title="Domains">
|
<a href="{% url "admin_domains" %}" {% if section == "domains" %}class="selected"{% endif %} title="Domains">
|
||||||
<i class="fa-solid fa-globe"></i> Domains
|
<i class="fa-solid fa-globe"></i> Domains
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from activities.admin import IdentityLocalFilter
|
from activities.admin import IdentityLocalFilter
|
||||||
from users.models import (
|
from users.models import (
|
||||||
|
Announcement,
|
||||||
Domain,
|
Domain,
|
||||||
Follow,
|
Follow,
|
||||||
Identity,
|
Identity,
|
||||||
|
@ -197,3 +198,9 @@ class InviteAdmin(admin.ModelAdmin):
|
||||||
@admin.register(Report)
|
@admin.register(Report)
|
||||||
class ReportAdmin(admin.ModelAdmin):
|
class ReportAdmin(admin.ModelAdmin):
|
||||||
list_display = ["id", "created", "resolved", "type", "subject_identity"]
|
list_display = ["id", "created", "resolved", "type", "subject_identity"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Announcement)
|
||||||
|
class AnnouncementAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["id", "published", "start", "end", "text"]
|
||||||
|
raw_id_fields = ["seen"]
|
||||||
|
|
11
users/context.py
Normal file
11
users/context.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from users.services import AnnouncementService
|
||||||
|
|
||||||
|
|
||||||
|
def user_context(request):
|
||||||
|
return {
|
||||||
|
"announcements": (
|
||||||
|
AnnouncementService(request.user).visible()
|
||||||
|
if request.user.is_authenticated
|
||||||
|
else AnnouncementService.visible_anonymous()
|
||||||
|
)
|
||||||
|
}
|
64
users/migrations/0011_announcement.py
Normal file
64
users/migrations/0011_announcement.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
# Generated by Django 4.1.4 on 2023-01-13 22:27
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("users", "0010_domain_state"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Announcement",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"text",
|
||||||
|
models.TextField(
|
||||||
|
help_text="The text of your announcement.\nAccepts Markdown for formatting."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"published",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="If this announcement will appear on the site.\nIt must still be between start and end times, if provided.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"start",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
help_text="When the announcement will start appearing.\nLeave blank to have it begin as soon as it is published.",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"end",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
help_text="When the announcement will stop appearing.\nLeave blank to have it display indefinitely.",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("include_unauthenticated", models.BooleanField(default=False)),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"seen",
|
||||||
|
models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,3 +1,4 @@
|
||||||
|
from .announcement import Announcement # noqa
|
||||||
from .block import Block # noqa
|
from .block import Block # noqa
|
||||||
from .domain import Domain # noqa
|
from .domain import Domain # noqa
|
||||||
from .follow import Follow, FollowStates # noqa
|
from .follow import Follow, FollowStates # noqa
|
||||||
|
|
63
users/models/announcement.py
Normal file
63
users/models/announcement.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import markdown_it
|
||||||
|
import urlman
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
|
class Announcement(models.Model):
|
||||||
|
"""
|
||||||
|
A server-wide announcement that users all see and can dismiss.
|
||||||
|
"""
|
||||||
|
|
||||||
|
text = models.TextField(
|
||||||
|
help_text="The text of your announcement.\nAccepts Markdown for formatting."
|
||||||
|
)
|
||||||
|
|
||||||
|
published = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="If this announcement will appear on the site.\nIt must still be between start and end times, if provided.",
|
||||||
|
)
|
||||||
|
start = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="When the announcement will start appearing.\nLeave blank to have it begin as soon as it is published.\nFormat: <tt>2023-01-01</tt> or <tt>2023-01-01 12:30:00</tt>",
|
||||||
|
)
|
||||||
|
end = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="When the announcement will stop appearing.\nLeave blank to have it display indefinitely.\nFormat: <tt>2023-01-01</tt> or <tt>2023-01-01 12:30:00</tt>",
|
||||||
|
)
|
||||||
|
|
||||||
|
include_unauthenticated = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# Note that this is against User, not Identity - it's one of the few places
|
||||||
|
# where we want it to be per login.
|
||||||
|
seen = models.ManyToManyField("users.User", blank=True)
|
||||||
|
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class urls(urlman.Urls):
|
||||||
|
dismiss = "/announcements/{self.pk}/dismiss/"
|
||||||
|
admin_root = "/admin/announcements/"
|
||||||
|
admin_edit = "{admin_root}{self.pk}/"
|
||||||
|
admin_delete = "{admin_edit}delete/"
|
||||||
|
admin_publish = "{admin_root}{self.pk}/publish/"
|
||||||
|
admin_unpublish = "{admin_root}{self.pk}/unpublish/"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def html(self) -> str:
|
||||||
|
return mark_safe(markdown_it.MarkdownIt().render(self.text))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def visible(self) -> bool:
|
||||||
|
return self.published and self.after_start and self.before_end
|
||||||
|
|
||||||
|
@property
|
||||||
|
def after_start(self) -> bool:
|
||||||
|
return timezone.now() >= self.start if self.start else True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def before_end(self) -> bool:
|
||||||
|
return timezone.now() <= self.end if self.end else True
|
|
@ -1 +1,2 @@
|
||||||
|
from .announcement import AnnouncementService # noqa
|
||||||
from .identity import IdentityService # noqa
|
from .identity import IdentityService # noqa
|
||||||
|
|
45
users/services/announcement.py
Normal file
45
users/services/announcement.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from users.models import Announcement, User
|
||||||
|
|
||||||
|
|
||||||
|
class AnnouncementService:
|
||||||
|
"""
|
||||||
|
Handles viewing and dismissing announcements
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, user: User):
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def visible_queryset(cls) -> models.QuerySet[Announcement]:
|
||||||
|
"""
|
||||||
|
Common visibility query
|
||||||
|
"""
|
||||||
|
now = timezone.now()
|
||||||
|
return Announcement.objects.filter(
|
||||||
|
models.Q(start__lte=now) | models.Q(start__isnull=True),
|
||||||
|
models.Q(end__gte=now) | models.Q(end__isnull=True),
|
||||||
|
published=True,
|
||||||
|
).order_by("-start", "-created")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def visible_anonymous(cls) -> models.QuerySet[Announcement]:
|
||||||
|
"""
|
||||||
|
Returns all announcements marked as being showable to all visitors
|
||||||
|
"""
|
||||||
|
return cls.visible_queryset().filter(include_unauthenticated=True)
|
||||||
|
|
||||||
|
def visible(self) -> models.QuerySet[Announcement]:
|
||||||
|
"""
|
||||||
|
Returns all announcements that are currently valid and should be shown
|
||||||
|
to a given user.
|
||||||
|
"""
|
||||||
|
return self.visible_queryset().exclude(seen=self.user)
|
||||||
|
|
||||||
|
def mark_seen(self, announcement: Announcement):
|
||||||
|
"""
|
||||||
|
Marks an announcement as seen by the user
|
||||||
|
"""
|
||||||
|
announcement.seen.add(self.user)
|
|
@ -2,6 +2,14 @@ from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
from users.decorators import admin_required
|
from users.decorators import admin_required
|
||||||
|
from users.views.admin.announcements import ( # noqa
|
||||||
|
AnnouncementCreate,
|
||||||
|
AnnouncementDelete,
|
||||||
|
AnnouncementEdit,
|
||||||
|
AnnouncementPublish,
|
||||||
|
AnnouncementsRoot,
|
||||||
|
AnnouncementUnpublish,
|
||||||
|
)
|
||||||
from users.views.admin.domains import ( # noqa
|
from users.views.admin.domains import ( # noqa
|
||||||
DomainCreate,
|
DomainCreate,
|
||||||
DomainDelete,
|
DomainDelete,
|
||||||
|
|
87
users/views/admin/announcements.py
Normal file
87
users/views/admin/announcements.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
from django import forms
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||||||
|
|
||||||
|
from users.decorators import admin_required
|
||||||
|
from users.models import Announcement
|
||||||
|
from users.views.admin.generic import HTMXActionView
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(admin_required, name="dispatch")
|
||||||
|
class AnnouncementsRoot(ListView):
|
||||||
|
|
||||||
|
template_name = "admin/announcements.html"
|
||||||
|
paginate_by = 30
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.extra_context = {
|
||||||
|
"section": "announcements",
|
||||||
|
}
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
reports = Announcement.objects.order_by("created")
|
||||||
|
return reports
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(admin_required, name="dispatch")
|
||||||
|
class AnnouncementCreate(CreateView):
|
||||||
|
|
||||||
|
model = Announcement
|
||||||
|
template_name = "admin/announcement_create.html"
|
||||||
|
extra_context = {"section": "announcements"}
|
||||||
|
success_url = Announcement.urls.admin_root
|
||||||
|
|
||||||
|
class form_class(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Announcement
|
||||||
|
fields = ["text", "published", "start", "end"]
|
||||||
|
widgets = {
|
||||||
|
"published": forms.Select(
|
||||||
|
choices=[(True, "Published"), (False, "Draft")]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(admin_required, name="dispatch")
|
||||||
|
class AnnouncementEdit(UpdateView):
|
||||||
|
|
||||||
|
model = Announcement
|
||||||
|
template_name = "admin/announcement_edit.html"
|
||||||
|
extra_context = {"section": "announcements"}
|
||||||
|
success_url = Announcement.urls.admin_root
|
||||||
|
|
||||||
|
class form_class(AnnouncementCreate.form_class):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(admin_required, name="dispatch")
|
||||||
|
class AnnouncementDelete(DeleteView):
|
||||||
|
|
||||||
|
model = Announcement
|
||||||
|
template_name = "admin/announcement_delete.html"
|
||||||
|
success_url = Announcement.urls.admin_root
|
||||||
|
|
||||||
|
|
||||||
|
class AnnouncementPublish(HTMXActionView):
|
||||||
|
"""
|
||||||
|
Marks the announcement as published.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = Announcement
|
||||||
|
|
||||||
|
def action(self, announcement: Announcement):
|
||||||
|
announcement.published = True
|
||||||
|
announcement.save()
|
||||||
|
|
||||||
|
|
||||||
|
class AnnouncementUnpublish(HTMXActionView):
|
||||||
|
"""
|
||||||
|
Marks the announcement as unpublished.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = Announcement
|
||||||
|
|
||||||
|
def action(self, announcement: Announcement):
|
||||||
|
announcement.published = False
|
||||||
|
announcement.save()
|
|
@ -1,13 +1,13 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import FormView, ListView, View
|
from django.views.generic import FormView, ListView
|
||||||
from django_htmx.http import HttpResponseClientRefresh
|
|
||||||
|
|
||||||
from activities.models import Emoji
|
from activities.models import Emoji
|
||||||
from users.decorators import moderator_required
|
from users.decorators import moderator_required
|
||||||
|
from users.views.admin.generic import HTMXActionView
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(moderator_required, name="dispatch")
|
@method_decorator(moderator_required, name="dispatch")
|
||||||
|
@ -70,27 +70,26 @@ class EmojiCreate(FormView):
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(moderator_required, name="dispatch")
|
@method_decorator(moderator_required, name="dispatch")
|
||||||
class EmojiDelete(View):
|
class EmojiDelete(HTMXActionView):
|
||||||
"""
|
"""
|
||||||
Deletes an emoji
|
Deletes an emoji
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def post(self, request, id):
|
model = Emoji
|
||||||
self.emoji = get_object_or_404(Emoji, pk=id)
|
|
||||||
self.emoji.delete()
|
def action(self, emoji: Emoji):
|
||||||
return HttpResponseClientRefresh()
|
emoji.delete()
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(moderator_required, name="dispatch")
|
@method_decorator(moderator_required, name="dispatch")
|
||||||
class EmojiEnable(View):
|
class EmojiEnable(HTMXActionView):
|
||||||
"""
|
"""
|
||||||
Sets an emoji to be enabled (or not!)
|
Sets an emoji to be enabled (or not!)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
model = Emoji
|
||||||
enable = True
|
enable = True
|
||||||
|
|
||||||
def post(self, request, id):
|
def action(self, emoji: Emoji):
|
||||||
self.emoji = get_object_or_404(Emoji, pk=id)
|
emoji.public = self.enable
|
||||||
self.emoji.public = self.enable
|
emoji.save()
|
||||||
self.emoji.save()
|
|
||||||
return HttpResponseClientRefresh()
|
|
||||||
|
|
17
users/views/admin/generic.py
Normal file
17
users/views/admin/generic.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from django.views.generic import View
|
||||||
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
from django_htmx.http import HttpResponseClientRefresh
|
||||||
|
|
||||||
|
|
||||||
|
class HTMXActionView(SingleObjectMixin, View):
|
||||||
|
"""
|
||||||
|
Generic view that performs an action when called via HTMX and then causes
|
||||||
|
a full page refresh.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self, request, pk):
|
||||||
|
self.action(self.get_object())
|
||||||
|
return HttpResponseClientRefresh()
|
||||||
|
|
||||||
|
def action(self, instance):
|
||||||
|
raise NotImplementedError()
|
21
users/views/announcements.py
Normal file
21
users/views/announcements.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.generic import View
|
||||||
|
|
||||||
|
from users.decorators import identity_required
|
||||||
|
from users.models import Announcement
|
||||||
|
from users.services import AnnouncementService
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(identity_required, name="dispatch")
|
||||||
|
class AnnouncementDismiss(View):
|
||||||
|
"""
|
||||||
|
Dismisses an announcement for the current user
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self, request, id):
|
||||||
|
announcement = get_object_or_404(Announcement, pk=id)
|
||||||
|
AnnouncementService(request.user).mark_seen(announcement)
|
||||||
|
# In the UI we replace it with nothing anyway
|
||||||
|
return HttpResponse("")
|
Loading…
Reference in a new issue