diff options
author | Andrew Godwin | 2022-11-18 00:09:04 -0700 |
---|---|---|
committer | Andrew Godwin | 2022-11-18 00:09:04 -0700 |
commit | 1b44a253316a84f40070264ea8134c86d1223441 (patch) | |
tree | 2caa56d54a8e81f14649b826f9f3ef5a7c1326ae /users | |
parent | b3072c81ba73a16381366960841b6c294cc1fa6e (diff) | |
download | takahe-1b44a253316a84f40070264ea8134c86d1223441.tar.gz takahe-1b44a253316a84f40070264ea8134c86d1223441.tar.bz2 takahe-1b44a253316a84f40070264ea8134c86d1223441.zip |
Signup and invite tweaks
Diffstat (limited to 'users')
-rw-r--r-- | users/admin.py | 6 | ||||
-rw-r--r-- | users/migrations/0005_invite.py | 32 | ||||
-rw-r--r-- | users/models/__init__.py | 1 | ||||
-rw-r--r-- | users/models/invite.py | 30 | ||||
-rw-r--r-- | users/models/password_reset.py | 2 | ||||
-rw-r--r-- | users/views/admin/__init__.py | 56 | ||||
-rw-r--r-- | users/views/admin/domains.py (renamed from users/views/admin.py) | 107 | ||||
-rw-r--r-- | users/views/admin/settings.py | 83 | ||||
-rw-r--r-- | users/views/auth.py | 18 |
9 files changed, 232 insertions, 103 deletions
diff --git a/users/admin.py b/users/admin.py index de07e5c..0901307 100644 --- a/users/admin.py +++ b/users/admin.py @@ -5,6 +5,7 @@ from users.models import ( Follow, Identity, InboxMessage, + Invite, PasswordReset, User, UserEvent, @@ -66,3 +67,8 @@ class InboxMessageAdmin(admin.ModelAdmin): def reset_state(self, request, queryset): for instance in queryset: instance.transition_perform("received") + + +@admin.register(Invite) +class InviteAdmin(admin.ModelAdmin): + list_display = ["id", "created", "token", "note"] diff --git a/users/migrations/0005_invite.py b/users/migrations/0005_invite.py new file mode 100644 index 0000000..bb18841 --- /dev/null +++ b/users/migrations/0005_invite.py @@ -0,0 +1,32 @@ +# Generated by Django 4.1.3 on 2022-11-18 06:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0004_passwordreset"), + ] + + operations = [ + migrations.CreateModel( + name="Invite", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("token", models.CharField(max_length=500, unique=True)), + ("email", models.EmailField(blank=True, max_length=254, null=True)), + ("note", models.TextField(blank=True, null=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ], + ), + ] diff --git a/users/models/__init__.py b/users/models/__init__.py index e46860e..fc0d402 100644 --- a/users/models/__init__.py +++ b/users/models/__init__.py @@ -3,6 +3,7 @@ from .domain import Domain # noqa from .follow import Follow, FollowStates # noqa from .identity import Identity, IdentityStates # noqa from .inbox_message import InboxMessage, InboxMessageStates # noqa +from .invite import Invite # noqa from .password_reset import PasswordReset # noqa from .user import User # noqa from .user_event import UserEvent # noqa diff --git a/users/models/invite.py b/users/models/invite.py new file mode 100644 index 0000000..5d69b18 --- /dev/null +++ b/users/models/invite.py @@ -0,0 +1,30 @@ +import random + +from django.db import models + + +class Invite(models.Model): + """ + An invite token, good for one signup. + """ + + # Should always be lowercase + token = models.CharField(max_length=500, unique=True) + + # Is it limited to a specific email? + email = models.EmailField(null=True, blank=True) + + # Admin note about this code + note = models.TextField(null=True, blank=True) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + @classmethod + def create_random(cls, email=None): + return cls.objects.create( + token="".join( + random.choice("abcdefghkmnpqrstuvwxyz23456789") for i in range(20) + ), + email=email, + ) diff --git a/users/models/password_reset.py b/users/models/password_reset.py index 90062d3..628efa6 100644 --- a/users/models/password_reset.py +++ b/users/models/password_reset.py @@ -27,7 +27,7 @@ class PasswordResetStates(StateGraph): await sync_to_async(send_mail)( subject=f"{Config.system.site_name}: Confirm new account", message=render_to_string( - "emails/new_account.txt", + "emails/account_new.txt", { "reset": reset, "config": Config.system, diff --git a/users/views/admin/__init__.py b/users/views/admin/__init__.py new file mode 100644 index 0000000..231e027 --- /dev/null +++ b/users/views/admin/__init__.py @@ -0,0 +1,56 @@ +from django import forms +from django.utils.decorators import method_decorator +from django.views.generic import FormView, RedirectView, TemplateView + +from users.decorators import admin_required +from users.models import Identity, User +from users.views.admin.domains import ( # noqa + DomainCreate, + DomainDelete, + DomainEdit, + Domains, +) +from users.views.admin.settings import BasicSettings # noqa + + +@method_decorator(admin_required, name="dispatch") +class AdminRoot(RedirectView): + pattern_name = "admin_basic" + + +@method_decorator(admin_required, name="dispatch") +class Users(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 Identities(TemplateView): + + template_name = "admin/identities.html" + + def get_context_data(self): + return { + "identities": Identity.objects.order_by("username"), + "section": "identities", + } + + +@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.py b/users/views/admin/domains.py index 93bf4ec..e1a011b 100644 --- a/users/views/admin.py +++ b/users/views/admin/domains.py @@ -4,85 +4,14 @@ from django import forms from django.db import models from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator -from django.views.generic import FormView, RedirectView, TemplateView +from django.views.generic import FormView, TemplateView -from core.models import Config from users.decorators import admin_required -from users.models import Domain, Identity, User -from users.views.settings import SettingsPage +from users.models import Domain @method_decorator(admin_required, name="dispatch") -class AdminRoot(RedirectView): - pattern_name = "admin_basic" - - -@method_decorator(admin_required, name="dispatch") -class AdminSettingsPage(SettingsPage): - """ - 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! - """ - - options_class = Config.SystemOptions - - def load_config(self): - return Config.load_system() - - def save_config(self, key, value): - Config.set_system(key, value) - - -class BasicPage(AdminSettingsPage): - - section = "basic" - - options = { - "site_name": { - "title": "Site Name", - }, - "highlight_color": { - "title": "Highlight Color", - "help_text": "Used for logo background and other highlights", - }, - "post_length": { - "title": "Maximum Post Length", - "help_text": "The maximum number of characters allowed per post", - }, - "site_about": { - "title": "About This Site", - "help_text": "Displayed on the homepage and the about page", - "display": "textarea", - }, - "site_icon": { - "title": "Site Icon", - "help_text": "Minimum size 64x64px. Should be square.", - }, - "site_banner": { - "title": "Site Banner", - "help_text": "Must be at least 650px wide. 3:1 ratio of width:height recommended.", - }, - "identity_max_per_user": { - "title": "Maximum Identities Per User", - "help_text": "Non-admins will be blocked from creating more than this", - }, - } - - layout = { - "Branding": [ - "site_name", - "site_about", - "site_icon", - "site_banner", - "highlight_color", - ], - "Posts": ["post_length"], - "Identities": ["identity_max_per_user"], - } - - -@method_decorator(admin_required, name="dispatch") -class DomainsPage(TemplateView): +class Domains(TemplateView): template_name = "admin/domains.html" @@ -94,7 +23,7 @@ class DomainsPage(TemplateView): @method_decorator(admin_required, name="dispatch") -class DomainCreatePage(FormView): +class DomainCreate(FormView): template_name = "admin/domain_create.html" extra_context = {"section": "domains"} @@ -154,7 +83,7 @@ class DomainCreatePage(FormView): @method_decorator(admin_required, name="dispatch") -class DomainEditPage(FormView): +class DomainEdit(FormView): template_name = "admin/domain_edit.html" extra_context = {"section": "domains"} @@ -200,7 +129,7 @@ class DomainEditPage(FormView): @method_decorator(admin_required, name="dispatch") -class DomainDeletePage(TemplateView): +class DomainDelete(TemplateView): template_name = "admin/domain_delete.html" @@ -222,27 +151,3 @@ class DomainDeletePage(TemplateView): raise ValueError("Tried to delete domain with identities!") self.domain.delete() 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", - } diff --git a/users/views/admin/settings.py b/users/views/admin/settings.py new file mode 100644 index 0000000..a528f93 --- /dev/null +++ b/users/views/admin/settings.py @@ -0,0 +1,83 @@ +from django.utils.decorators import method_decorator + +from core.models import Config +from users.decorators import admin_required +from users.views.settings import SettingsPage + + +@method_decorator(admin_required, name="dispatch") +class AdminSettingsPage(SettingsPage): + """ + 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! + """ + + options_class = Config.SystemOptions + + def load_config(self): + return Config.load_system() + + def save_config(self, key, value): + Config.set_system(key, value) + + +class BasicSettings(AdminSettingsPage): + + section = "basic" + + options = { + "site_name": { + "title": "Site Name", + }, + "highlight_color": { + "title": "Highlight Color", + "help_text": "Used for logo background and other highlights", + }, + "post_length": { + "title": "Maximum Post Length", + "help_text": "The maximum number of characters allowed per post", + }, + "site_about": { + "title": "About This Site", + "help_text": "Displayed on the homepage and the about page", + "display": "textarea", + }, + "site_icon": { + "title": "Site Icon", + "help_text": "Minimum size 64x64px. Should be square.", + }, + "site_banner": { + "title": "Site Banner", + "help_text": "Must be at least 650px wide. 3:1 ratio of width:height recommended.", + }, + "identity_max_per_user": { + "title": "Maximum Identities Per User", + "help_text": "Non-admins will be blocked from creating more than this", + }, + "signup_allowed": { + "title": "Signups Allowed", + "help_text": "If signups are allowed at all", + }, + "signup_invite_only": { + "title": "Invite-Only", + "help_text": "If signups require an invite code", + }, + "signup_text": { + "title": "Signup Page Text", + "help_text": "Shown above the signup form", + "display": "textarea", + }, + } + + layout = { + "Branding": [ + "site_name", + "site_about", + "site_icon", + "site_banner", + "highlight_color", + ], + "Signups": ["signup_allowed", "signup_invite_only", "signup_text"], + "Posts": ["post_length"], + "Identities": ["identity_max_per_user"], + } diff --git a/users/views/auth.py b/users/views/auth.py index 7f51d45..a04b1b1 100644 --- a/users/views/auth.py +++ b/users/views/auth.py @@ -4,7 +4,8 @@ from django.contrib.auth.views import LoginView, LogoutView from django.shortcuts import get_object_or_404, render from django.views.generic import FormView -from users.models import PasswordReset, User +from core.models import Config +from users.models import Invite, PasswordReset, User class Login(LoginView): @@ -26,6 +27,13 @@ class Signup(FormView): help_text="We will send a link to this email to set your password and create your account", ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if Config.system.signup_invite_only: + self.fields["invite_code"] = forms.CharField( + help_text="Your invite code from one of our admins" + ) + def clean_email(self): email = self.cleaned_data.get("email").lower() if not email: @@ -34,9 +42,17 @@ class Signup(FormView): raise forms.ValidationError("This email already has an account") return email + def clean_invite_code(self): + invite_code = self.cleaned_data["invite_code"].lower().strip() + if not Invite.objects.filter(token=invite_code).exists(): + raise forms.ValidationError("That is not a valid invite code") + return invite_code + def form_valid(self, form): user = User.objects.create(email=form.cleaned_data["email"]) PasswordReset.create_for_user(user) + if "invite_code" in form.cleaned_data: + Invite.objects.filter(token=form.cleaned_data["invite_code"]).delete() return render( self.request, "auth/signup_success.html", |