diff --git a/static/css/style.css b/static/css/style.css index 749b7b6..3a59380 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -377,8 +377,8 @@ img.emoji { color: var(--color-text-dull); } -.icon-menu .option.empty:hover { - border: 0; +.icon-menu .option.empty:hover, +.icon-menu .option.static:hover { border: 2px solid rgba(255, 255, 255, 0); } @@ -1096,6 +1096,7 @@ table.metadata td .emoji { font-weight: lighter; font-size: small; } + .post .poll ul { list-style: none; } @@ -1104,14 +1105,17 @@ table.metadata td .emoji { padding: 6px 0; line-height: 18px; } + .poll-number { display: inline-block; width: 45px; } + .poll-footer { padding: 6px 0 6px; font-size: 12px; } + .post .poll ul li .votes { margin-left: 10px; font-size: small; diff --git a/takahe/urls.py b/takahe/urls.py index 7c8231c..a45be59 100644 --- a/takahe/urls.py +++ b/takahe/urls.py @@ -127,9 +127,19 @@ urlpatterns = [ ), path( "admin/invites/", - admin.Invites.as_view(), + admin.InvitesRoot.as_view(), name="admin_invites", ), + path( + "admin/invites/create/", + admin.InviteCreate.as_view(), + name="admin_invite_create", + ), + path( + "admin/invites//", + admin.InviteView.as_view(), + name="admin_invite_view", + ), path( "admin/hashtags/", admin.Hashtags.as_view(), diff --git a/templates/admin/invite_create.html b/templates/admin/invite_create.html new file mode 100644 index 0000000..f7d7e26 --- /dev/null +++ b/templates/admin/invite_create.html @@ -0,0 +1,18 @@ +{% extends "settings/base.html" %} + +{% block subtitle %}Create Invite{% endblock %} + +{% block content %} +
+ {% csrf_token %} +
+ Details + {% include "forms/_field.html" with field=form.email %} + {% include "forms/_field.html" with field=form.notes %} +
+
+ Back + +
+
+{% endblock %} diff --git a/templates/admin/invite_view.html b/templates/admin/invite_view.html new file mode 100644 index 0000000..c666af0 --- /dev/null +++ b/templates/admin/invite_view.html @@ -0,0 +1,20 @@ +{% extends "settings/base.html" %} + +{% block subtitle %}View Invite{% endblock %} + +{% block content %} +
+ {% csrf_token %} +
+ Details + {% include "forms/_field.html" with field=form.code %} + {% include "forms/_field.html" with field=form.email %} + {% include "forms/_field.html" with field=form.notes %} +
+
+ Back + + +
+
+{% endblock %} diff --git a/templates/admin/invites.html b/templates/admin/invites.html index 14e05f3..11433e1 100644 --- a/templates/admin/invites.html +++ b/templates/admin/invites.html @@ -1,9 +1,41 @@ {% extends "settings/base.html" %} +{% load activity_tags %} {% block subtitle %}Invites{% endblock %} {% block content %} -

- Please use the Django Admin for now. -

+
+ +
+
+ {% for invite in page_obj %} + + + + {{ invite.token }} + + + {% if invite.email %} + (email {{ invite.email }}) + {% endif %} + + + + + {% empty %} +

+ There are no unused invites. +

+ {% endfor %} +
+ {% if page_obj.has_previous %} + Previous Page + {% endif %} + {% if page_obj.has_next %} + Next Page + {% endif %} +
+
{% endblock %} diff --git a/users/models/invite.py b/users/models/invite.py index 5d69b18..b84b7af 100644 --- a/users/models/invite.py +++ b/users/models/invite.py @@ -1,5 +1,6 @@ import random +import urlman from django.db import models @@ -20,6 +21,11 @@ class Invite(models.Model): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) + class urls(urlman.Urls): + admin = "/admin/invites/" + admin_create = "{admin}create/" + admin_view = "{admin}{self.pk}/" + @classmethod def create_random(cls, email=None): return cls.objects.create( diff --git a/users/views/admin/__init__.py b/users/views/admin/__init__.py index 0e054d2..c4576b7 100644 --- a/users/views/admin/__init__.py +++ b/users/views/admin/__init__.py @@ -1,6 +1,5 @@ -from django import forms from django.utils.decorators import method_decorator -from django.views.generic import FormView, RedirectView +from django.views.generic import RedirectView from users.decorators import admin_required from users.views.admin.domains import ( # noqa @@ -17,6 +16,7 @@ from users.views.admin.hashtags import ( # noqa Hashtags, ) from users.views.admin.identities import IdentitiesRoot, IdentityEdit # noqa +from users.views.admin.invites import InviteCreate, InvitesRoot, InviteView # noqa from users.views.admin.reports import ReportsRoot, ReportView # noqa from users.views.admin.settings import ( # noqa BasicSettings, @@ -30,17 +30,3 @@ from users.views.admin.users import UserEdit, UsersRoot # noqa @method_decorator(admin_required, name="dispatch") class AdminRoot(RedirectView): pattern_name = "admin_basic" - - -@method_decorator(admin_required, name="dispatch") -class Invites(FormView): - - template_name = "admin/invites.html" - extra_context = {"section": "invites"} - - class form_class(forms.Form): - note = forms.CharField() - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) - return context diff --git a/users/views/admin/invites.py b/users/views/admin/invites.py new file mode 100644 index 0000000..2a68221 --- /dev/null +++ b/users/views/admin/invites.py @@ -0,0 +1,87 @@ +from django import forms +from django.shortcuts import get_object_or_404, redirect +from django.utils.decorators import method_decorator +from django.views.generic import FormView, ListView + +from users.decorators import admin_required +from users.models import Invite + + +@method_decorator(admin_required, name="dispatch") +class InvitesRoot(ListView): + + template_name = "admin/invites.html" + paginate_by = 30 + + def get(self, request, *args, **kwargs): + self.extra_context = { + "section": "invites", + } + return super().get(request, *args, **kwargs) + + def get_queryset(self): + return Invite.objects.order_by("created") + + +@method_decorator(admin_required, name="dispatch") +class InviteCreate(FormView): + + template_name = "admin/invite_create.html" + extra_context = { + "section": "invites", + } + + class form_class(forms.Form): + email = forms.EmailField( + required=False, + help_text="Optional email to tie the invite to.\nYou will still need to email the user this code yourself!", + ) + notes = forms.CharField( + required=False, + widget=forms.Textarea, + help_text="Notes for other admins", + ) + + def form_valid(self, form): + invite = Invite.create_random(email=form.cleaned_data.get("email") or None) + return redirect(invite.urls.admin) + + +@method_decorator(admin_required, name="dispatch") +class InviteView(FormView): + + template_name = "admin/invite_view.html" + extra_context = { + "section": "invites", + } + + class form_class(InviteCreate.form_class): + code = forms.CharField(disabled=True, required=False) + + def dispatch(self, request, id, *args, **kwargs): + self.invite = get_object_or_404(Invite, id=id) + return super().dispatch(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + if "delete" in request.POST: + self.invite.delete() + return redirect(self.invite.urls.admin) + return super().post(request, *args, **kwargs) + + def get_initial(self): + return { + "notes": self.invite.note, + "email": self.invite.email, + "code": self.invite.token, + } + + def form_valid(self, form): + self.invite.note = form.cleaned_data.get("notes") or "" + self.invite.email = form.cleaned_data.get("email") or None + self.invite.save() + return redirect(self.invite.urls.admin) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["invite"] = self.invite + return context