diff --git a/boofilsic/settings.py b/boofilsic/settings.py
index 9a0afe5a..3b31bfdd 100644
--- a/boofilsic/settings.py
+++ b/boofilsic/settings.py
@@ -297,7 +297,6 @@ INSTALLED_APPS = [
]
INSTALLED_APPS += [
- "management.apps.ManagementConfig",
"mastodon.apps.MastodonConfig",
"common.apps.CommonConfig",
"users.apps.UsersConfig",
diff --git a/boofilsic/urls.py b/boofilsic/urls.py
index b0be01c7..8fc3f434 100644
--- a/boofilsic/urls.py
+++ b/boofilsic/urls.py
@@ -41,7 +41,6 @@ urlpatterns = [
path("", include("catalog.urls")),
path("", include("journal.urls")),
path("timeline/", include("social.urls")),
- path("announcement/", include("management.urls")),
path("hijack/", include("hijack.urls")),
path("", include("common.urls")),
path("", include("legacy.urls")),
diff --git a/common/templates/_footer.html b/common/templates/_footer.html
index 0a4404df..b089d799 100644
--- a/common/templates/_footer.html
+++ b/common/templates/_footer.html
@@ -7,7 +7,7 @@
rel="noopener"
href="{{ link.url }}">{{ link.title }}
{% endfor %}
-
+
{% trans "Your applications" %}
- By using our APIs, you agree to our term and data policy.
+ By using our APIs, you agree to our Terms of Service.
diff --git a/management/__init__.py b/management/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/management/admin.py b/management/admin.py
deleted file mode 100644
index e26dfdf5..00000000
--- a/management/admin.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from django.contrib import admin
-
-from .models import *
-
-admin.site.register(Announcement)
diff --git a/management/apps.py b/management/apps.py
deleted file mode 100644
index f4ad1a62..00000000
--- a/management/apps.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from django.apps import AppConfig
-
-
-class ManagementConfig(AppConfig):
- name = "management"
diff --git a/management/migrations/0001_initial.py b/management/migrations/0001_initial.py
deleted file mode 100644
index c4d268ce..00000000
--- a/management/migrations/0001_initial.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Generated by Django 3.2.16 on 2023-01-12 01:32
-
-import markdownx.models
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
- initial = True
-
- dependencies = []
-
- operations = [
- migrations.CreateModel(
- name="Announcement",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("title", models.CharField(max_length=200)),
- ("content", markdownx.models.MarkdownxField()),
- (
- "slug",
- models.SlugField(
- allow_unicode=True,
- blank=True,
- max_length=300,
- null=True,
- unique=True,
- ),
- ),
- ("created_time", models.DateTimeField(auto_now_add=True)),
- ("edited_time", models.DateTimeField(auto_now_add=True)),
- ],
- options={
- "verbose_name": "Announcement",
- "verbose_name_plural": "Announcements",
- },
- ),
- ]
diff --git a/management/migrations/__init__.py b/management/migrations/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/management/models.py b/management/models.py
deleted file mode 100644
index 52cb6e0c..00000000
--- a/management/models.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import re
-
-from django.db import models
-from django.urls import reverse
-from django.utils.translation import gettext_lazy as _
-from markdown import markdown
-from markdownx.models import MarkdownxField
-
-RE_HTML_TAG = re.compile(r"<[^>]*>")
-
-
-class Announcement(models.Model):
- """Model definition for Announcement."""
-
- title = models.CharField(max_length=200)
- content = MarkdownxField()
- slug = models.SlugField(
- max_length=300, allow_unicode=True, unique=True, null=True, blank=True
- )
- created_time = models.DateTimeField(auto_now_add=True)
- edited_time = models.DateTimeField(auto_now_add=True)
-
- class Meta:
- """Meta definition for Announcement."""
-
- verbose_name = "Announcement"
- verbose_name_plural = "Announcements"
-
- def get_absolute_url(self):
- return reverse("management:retrieve", kwargs={"pk": self.pk})
-
- def get_html_content(self):
- html = markdown(self.content)
- return html
-
- def get_plain_content(self):
- """
- Get plain text format content
- """
- return RE_HTML_TAG.sub(" ", self.get_html_content())
-
- def __str__(self):
- """Unicode representation of Announcement."""
- return self.title
diff --git a/management/templates/management/create_update.html b/management/templates/management/create_update.html
deleted file mode 100644
index 929e1cdc..00000000
--- a/management/templates/management/create_update.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{% load static %}
-
-
-
-
-
- Create/Update Announcement
- {% include "common_libs.html" %}
-
-
-
-
Create/Update Announcement
-
-
- {{ form.media }}
-
-
-
-
-
diff --git a/management/templates/management/delete.html b/management/templates/management/delete.html
deleted file mode 100644
index e02dec2a..00000000
--- a/management/templates/management/delete.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{% load static %}
-
-
-
-
-
- Delete Announcement
- {% include "common_libs.html" %}
-
-
-
-
-
-
diff --git a/management/templates/management/detail.html b/management/templates/management/detail.html
deleted file mode 100644
index d6085ac8..00000000
--- a/management/templates/management/detail.html
+++ /dev/null
@@ -1,33 +0,0 @@
-{% load static %}
-{% load i18n %}
-
-
-
-
-
- {% include "common_libs.html" %}
- {{ site_name }} - {{ object.title }}
-
-
- {% include "_header.html" %}
-
-
-
- {{ object.get_html_content | safe }}
-
-
-
- {% include "_footer.html" %}
-
-
diff --git a/management/tests.py b/management/tests.py
deleted file mode 100644
index 7ce503c2..00000000
--- a/management/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/management/urls.py b/management/urls.py
deleted file mode 100644
index 6e77b67b..00000000
--- a/management/urls.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from django.urls import path
-
-from .views import *
-
-app_name = "management"
-urlpatterns = [
- path("", AnnouncementListView.as_view(), name="list"),
- path("/", AnnouncementDetailView.as_view(), name="retrieve"),
- path("create/", AnnouncementCreateView.as_view(), name="create"),
- path("/", AnnouncementDetailView.as_view(), name="retrieve_slug"),
- path("/update/", AnnouncementUpdateView.as_view(), name="update"),
- path("/delete/", AnnouncementDeleteView.as_view(), name="delete"),
-]
diff --git a/management/views.py b/management/views.py
deleted file mode 100644
index 2c1f0f7a..00000000
--- a/management/views.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from django.contrib.auth.decorators import login_required, user_passes_test
-from django.shortcuts import get_object_or_404
-from django.urls import reverse_lazy
-from django.utils import timezone
-from django.utils.decorators import method_decorator
-from django.views.generic import (
- CreateView,
- DeleteView,
- DetailView,
- ListView,
- UpdateView,
-)
-from django.views.generic.edit import ModelFormMixin
-
-from .models import Announcement
-
-# https://docs.djangoproject.com/en/3.1/topics/class-based-views/intro/
-decorators = [login_required, user_passes_test(lambda u: u.is_superuser)] # type:ignore
-
-
-class AnnouncementDetailView(DetailView, ModelFormMixin):
- model = Announcement
- fields = ["content"]
- template_name = "management/detail.html"
-
-
-class AnnouncementListView(ListView):
- model = Announcement
- # paginate_by = 1
- template_name = "management/list.html"
-
- def get_queryset(self):
- return Announcement.objects.all().order_by("-pk")
-
-
-@method_decorator(decorators, name="dispatch")
-class AnnouncementDeleteView(DeleteView):
- model = Announcement
- success_url = reverse_lazy("management:list")
- template_name = "management/delete.html"
-
-
-@method_decorator(decorators, name="dispatch")
-class AnnouncementCreateView(CreateView):
- model = Announcement
- fields = "__all__"
- template_name = "management/create_update.html"
-
-
-@method_decorator(decorators, name="dispatch")
-class AnnouncementUpdateView(UpdateView):
- model = Announcement
- fields = "__all__"
- template_name = "management/create_update.html"
-
- def form_valid(self, form):
- form.instance.edited_time = timezone.now()
- return super().form_valid(form)
diff --git a/takahe/migrations/0001_initial.py b/takahe/migrations/0001_initial.py
index 1c68d0e5..7d4ae0a6 100644
--- a/takahe/migrations/0001_initial.py
+++ b/takahe/migrations/0001_initial.py
@@ -913,4 +913,49 @@ class Migration(migrations.Migration):
"db_table": "users_relay",
},
),
+ migrations.CreateModel(
+ name="Announcement",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "text",
+ models.TextField(),
+ ),
+ (
+ "published",
+ models.BooleanField(
+ default=False,
+ ),
+ ),
+ (
+ "start",
+ models.DateTimeField(
+ blank=True,
+ null=True,
+ ),
+ ),
+ (
+ "end",
+ models.DateTimeField(
+ blank=True,
+ 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="takahe.user")),
+ ],
+ options={
+ "db_table": "users_announcement",
+ },
+ ),
]
diff --git a/takahe/models.py b/takahe/models.py
index 2f12d6ee..c766e597 100644
--- a/takahe/models.py
+++ b/takahe/models.py
@@ -1921,3 +1921,42 @@ class Relay(models.Model):
class Meta:
# managed = False
db_table = "users_relay"
+
+
+class Announcement(models.Model):
+ """
+ A server-wide announcement that users all see and can dismiss.
+ """
+
+ text = models.TextField()
+
+ published = models.BooleanField(
+ default=False,
+ )
+ start = models.DateTimeField(
+ null=True,
+ blank=True,
+ )
+ end = models.DateTimeField(
+ null=True,
+ blank=True,
+ )
+
+ 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("User", blank=True)
+
+ created = models.DateTimeField(auto_now_add=True)
+ updated = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ # managed = False
+ db_table = "users_announcement"
+
+ @property
+ def html(self) -> str:
+ from journal.models import render_md
+
+ return mark_safe(render_md(self.text))
diff --git a/takahe/utils.py b/takahe/utils.py
index 0fca5060..1e1cdc01 100644
--- a/takahe/utils.py
+++ b/takahe/utils.py
@@ -891,3 +891,54 @@ class Takahe:
return False
invite = Invite.objects.filter(token=token).first()
return invite and invite.valid
+
+ @staticmethod
+ def get_announcements():
+ 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")
+
+ @staticmethod
+ def get_announcements_for_user(u: "NeoUser"):
+ identity = (
+ Identity.objects.filter(pk=u.identity.pk, local=True).first()
+ if u and u.is_authenticated and u.identity
+ else None
+ )
+ user = identity.users.all().first() if identity else None
+ if not user:
+ return Announcement.objects.none()
+ 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")
+ .exclude(seen=user)
+ )
+
+ @staticmethod
+ def mark_announcements_seen(u: "NeoUser"):
+ identity = (
+ Identity.objects.filter(pk=u.identity.pk, local=True).first()
+ if u and u.is_authenticated and u.identity
+ else None
+ )
+ user = identity.users.all().first() if identity else None
+ if not user:
+ return
+ now = timezone.now()
+ for a in (
+ Announcement.objects.filter(
+ models.Q(start__lte=now) | models.Q(start__isnull=True),
+ published=True,
+ )
+ .order_by("-start", "-created")
+ .exclude(seen=user)
+ ):
+ a.seen.add(user)
diff --git a/users/models/preference.py b/users/models/preference.py
index 7cf5c87d..a4423d51 100644
--- a/users/models/preference.py
+++ b/users/models/preference.py
@@ -17,7 +17,6 @@ from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _
from loguru import logger
-from management.models import Announcement
from mastodon.api import *
from takahe.utils import Takahe
diff --git a/users/models/user.py b/users/models/user.py
index 569521d7..7f23841e 100644
--- a/users/models/user.py
+++ b/users/models/user.py
@@ -18,7 +18,6 @@ from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _
from loguru import logger
-from management.models import Announcement
from mastodon.api import *
from takahe.utils import Takahe
@@ -382,12 +381,11 @@ class User(AbstractUser):
self.sync_relationship()
return True
- @property
+ @cached_property
def unread_announcements(self):
- unread_announcements = Announcement.objects.filter(
- pk__gt=self.read_announcement_index
- ).order_by("-pk")
- return unread_announcements
+ from takahe.utils import Takahe
+
+ return Takahe.get_announcements_for_user(self)
@property
def activity_manager(self):
diff --git a/management/templates/management/list.html b/users/templates/users/announcements.html
similarity index 52%
rename from management/templates/management/list.html
rename to users/templates/users/announcements.html
index d8d23828..2231f691 100644
--- a/management/templates/management/list.html
+++ b/users/templates/users/announcements.html
@@ -1,5 +1,6 @@
{% load static %}
{% load i18n %}
+{% load humanize %}
@@ -40,29 +41,18 @@
{% if request.user.is_superuser %}🦹🏻{% endif %}
{% if request.user.is_staff %}🧙🏻{% endif %}
- {% if request.user.is_superuser %}
- {% trans '发布新公告' %}
- {% endif %}
- {% for announcement in object_list %}
+ {% for announcement in announcements %}
-
- {{ announcement.get_html_content | safe }}
-
- {% empty %}
- {% trans '暂无公告' %}
- {% endfor %}
-
- {% include "_footer.html" %}
-
+ {{ announcement.html | safe }}
+
+
+ {% empty %}
+ {% trans '暂无公告' %}
+ {% endfor %}
+
+ {% include "_footer.html" %}
+