mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-25 00:30:59 +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;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: 20px;
|
||||
border-left: 2px solid var(--color-bg-menu);
|
||||
}
|
||||
|
||||
|
||||
/* Logged out homepage */
|
||||
|
||||
|
@ -1309,6 +1314,23 @@ table.metadata td .emoji {
|
|||
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 {
|
||||
|
|
|
@ -232,6 +232,7 @@ TEMPLATES = [
|
|||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
"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 mediaproxy import views as mediaproxy
|
||||
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 = [
|
||||
path("", core.homepage),
|
||||
|
@ -162,9 +170,35 @@ urlpatterns = [
|
|||
admin.EmojiCreate.as_view(),
|
||||
name="admin_emoji_create",
|
||||
),
|
||||
path("admin/emoji/<id>/enable/", admin.EmojiEnable.as_view()),
|
||||
path("admin/emoji/<id>/disable/", admin.EmojiEnable.as_view(enable=False)),
|
||||
path("admin/emoji/<id>/delete/", admin.EmojiDelete.as_view()),
|
||||
path("admin/emoji/<pk>/enable/", admin.EmojiEnable.as_view()),
|
||||
path("admin/emoji/<pk>/disable/", admin.EmojiEnable.as_view(enable=False)),
|
||||
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(
|
||||
"admin/stator/",
|
||||
admin.Stator.as_view(),
|
||||
|
@ -222,6 +256,8 @@ urlpatterns = [
|
|||
core.FlatPage.as_view(title="Server Rules", config_option="policy_rules"),
|
||||
name="rules",
|
||||
),
|
||||
# Annoucements
|
||||
path("announcements/<id>/dismiss/", announcements.AnnouncementDismiss.as_view()),
|
||||
# Debug aids
|
||||
path("debug/json/", debug.JsonViewer.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 content %}
|
||||
{% if page_obj.number == 1 %}
|
||||
{% include "_announcements.html" %}
|
||||
{% endif %}
|
||||
{% for event in page_obj %}
|
||||
{% if event.type == "post" %}
|
||||
{% include "activities/_post.html" with post=event.subject_post %}
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
{% block title %}Local Timeline{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if page_obj.number == 1 %}
|
||||
{% include "_announcements.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% for post in page_obj %}
|
||||
{% include "activities/_post.html" with feedindex=forloop.counter %}
|
||||
{% empty %}
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
{% block title %}Notifications{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if page_obj.number == 1 %}
|
||||
{% include "_announcements.html" %}
|
||||
{% endif %}
|
||||
|
||||
<div class="view-options">
|
||||
{% if notification_options.followed %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="pagination">
|
||||
{% 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>
|
||||
{% include "admin/_pagination.html" with nouns="emoji,emoji" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -42,15 +42,5 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="pagination">
|
||||
{% 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>
|
||||
{% include "admin/_pagination.html" with nouns="domain,domains" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -50,15 +50,5 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<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 }} 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>
|
||||
{% include "admin/_pagination.html" with nouns="hashtag,hashtags" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -69,15 +69,5 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="pagination">
|
||||
{% 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>
|
||||
{% include "admin/_pagination.html" with nouns="identity,identities" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -51,15 +51,5 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<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 }} 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>
|
||||
{% include "admin/_pagination.html" with nouns="invite,invites" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -44,15 +44,5 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="pagination">
|
||||
{% 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>
|
||||
{% include "admin/_pagination.html" with nouns="report,reports" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -44,15 +44,5 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="pagination">
|
||||
{% 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>
|
||||
{% include "admin/_pagination.html" with nouns="user,users" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -39,6 +39,9 @@
|
|||
<a href="{% url "admin_policies" %}" {% if section == "policies" %}class="selected"{% endif %} title="Policies">
|
||||
<i class="fa-solid fa-file-lines"></i> Policies
|
||||
</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">
|
||||
<i class="fa-solid fa-globe"></i> Domains
|
||||
</a>
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from activities.admin import IdentityLocalFilter
|
||||
from users.models import (
|
||||
Announcement,
|
||||
Domain,
|
||||
Follow,
|
||||
Identity,
|
||||
|
@ -197,3 +198,9 @@ class InviteAdmin(admin.ModelAdmin):
|
|||
@admin.register(Report)
|
||||
class ReportAdmin(admin.ModelAdmin):
|
||||
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 .domain import Domain # 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
|
||||
|
|
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 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
|
||||
DomainCreate,
|
||||
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.conf import settings
|
||||
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.views.generic import FormView, ListView, View
|
||||
from django_htmx.http import HttpResponseClientRefresh
|
||||
from django.views.generic import FormView, ListView
|
||||
|
||||
from activities.models import Emoji
|
||||
from users.decorators import moderator_required
|
||||
from users.views.admin.generic import HTMXActionView
|
||||
|
||||
|
||||
@method_decorator(moderator_required, name="dispatch")
|
||||
|
@ -70,27 +70,26 @@ class EmojiCreate(FormView):
|
|||
|
||||
|
||||
@method_decorator(moderator_required, name="dispatch")
|
||||
class EmojiDelete(View):
|
||||
class EmojiDelete(HTMXActionView):
|
||||
"""
|
||||
Deletes an emoji
|
||||
"""
|
||||
|
||||
def post(self, request, id):
|
||||
self.emoji = get_object_or_404(Emoji, pk=id)
|
||||
self.emoji.delete()
|
||||
return HttpResponseClientRefresh()
|
||||
model = Emoji
|
||||
|
||||
def action(self, emoji: Emoji):
|
||||
emoji.delete()
|
||||
|
||||
|
||||
@method_decorator(moderator_required, name="dispatch")
|
||||
class EmojiEnable(View):
|
||||
class EmojiEnable(HTMXActionView):
|
||||
"""
|
||||
Sets an emoji to be enabled (or not!)
|
||||
"""
|
||||
|
||||
model = Emoji
|
||||
enable = True
|
||||
|
||||
def post(self, request, id):
|
||||
self.emoji = get_object_or_404(Emoji, pk=id)
|
||||
self.emoji.public = self.enable
|
||||
self.emoji.save()
|
||||
return HttpResponseClientRefresh()
|
||||
def action(self, emoji: Emoji):
|
||||
emoji.public = self.enable
|
||||
emoji.save()
|
||||
|
|
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