From 1b0a3fba9dcfbaf96dd8e5d55c32ed50f9504e2b Mon Sep 17 00:00:00 2001 From: Jason Cameron Date: Thu, 4 Jan 2024 09:17:22 -0500 Subject: [PATCH] updates --- core/forms.py | 16 +++- ...er_options_alter_user_managers_and_more.py | 27 +++++++ core/models/user.py | 18 +++-- core/urls.py | 80 +++++++++---------- core/utils/actions.py | 52 ------------ core/views/__init__.py | 7 -- core/views/mixins.py | 23 ++++++ core/views/timetable.py | 6 +- 8 files changed, 117 insertions(+), 112 deletions(-) create mode 100644 core/migrations/0071_alter_user_options_alter_user_managers_and_more.py delete mode 100644 core/views/__init__.py diff --git a/core/forms.py b/core/forms.py index a885e4e7..eeef863f 100644 --- a/core/forms.py +++ b/core/forms.py @@ -5,10 +5,16 @@ from django_select2 import forms as s2forms from martor.widgets import AdminMartorWidget -from . import models +from core import models +from django.contrib.auth.forms import ( + UserChangeForm as ContribUserChangeForm, + UserCreationForm as ContribUserCreationForm, +) +from core.views.mixins import CaseInsensitiveUsernameMixin -class MetropolisSignupForm(SignupForm): + +class MetropolisSignupForm(SignupForm, CaseInsensitiveUsernameMixin): first_name = forms.CharField( max_length=30, label="First Name", @@ -282,5 +288,9 @@ def __init__(self, *args, **kwargs): self.fields["status"].initial = "d" -class UserAdminForm(forms.ModelForm): +class UserAdminForm(CaseInsensitiveUsernameMixin, ContribUserChangeForm): expo_notif_tokens = forms.JSONField(required=False) + + +class UserCreationForm(CaseInsensitiveUsernameMixin, ContribUserCreationForm): + pass diff --git a/core/migrations/0071_alter_user_options_alter_user_managers_and_more.py b/core/migrations/0071_alter_user_options_alter_user_managers_and_more.py new file mode 100644 index 00000000..de316cb9 --- /dev/null +++ b/core/migrations/0071_alter_user_options_alter_user_managers_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0 on 2024-01-04 12:44 + +import core.models.user +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0070_remove_staffmember_unique_staff_member_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="user", + options={"verbose_name": "user", "verbose_name_plural": "users"}, + ), + migrations.AlterModelManagers( + name="user", + managers=[ + ("objects", core.models.user.CaseInsensitiveUserManager()), + ], + ), + migrations.RemoveConstraint( + model_name="user", + name="username-lower-check", + ), + ] diff --git a/core/models/user.py b/core/models/user.py index 80e19c83..d0b3c44a 100644 --- a/core/models/user.py +++ b/core/models/user.py @@ -1,9 +1,8 @@ from django.conf import settings -from django.contrib.auth.models import AbstractUser +from django.contrib.auth.models import AbstractUser, UserManager from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.db.models import Q, CharField -from django.db.models.functions import Lower from django.utils import timezone from .choices import graduating_year_choices, timezone_choices @@ -16,11 +15,21 @@ # Create your models here. +class CaseInsensitiveUserManager(UserManager): + def get_by_natural_key(self, username): + """ + By default, Django does a case-sensitive check on usernames. This is Wrong™. + Overriding this method fixes it. + """ + return self.get(**{self.model.USERNAME_FIELD + "__iexact": username}) + + def get_default_user_timezone(): return settings.DEFAULT_TIMEZONE class User(AbstractUser): + objects = CaseInsensitiveUserManager() bio = models.TextField(blank=True) timezone = models.CharField( max_length=50, choices=timezone_choices, default=get_default_user_timezone @@ -97,11 +106,6 @@ def can_approve(self, obj): def all(cls): return cls.objects.filter(is_active=True) - class Meta: - constraints = [ - models.UniqueConstraint(Lower("username"), name="username-lower-check") - ] - class StaffMember(models.Model): user = models.OneToOneField( diff --git a/core/urls.py b/core/urls.py index 8341f662..2e28c643 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,9 +1,15 @@ from django.contrib.sitemaps.views import sitemap from django.urls import include, path -from . import views -from .api.views import MartorImageUpload +from .api.views import * from .utils.sitemaps import * +from .views.index import * +from .views.organization import OrganizationShort +from .views.post import * +from .views.raffle import * +from .views.timetable import * +from .views.tv import * +from .views.user import * urlpatterns = [ path( @@ -11,7 +17,7 @@ MartorImageUpload.as_view(), name="api_martor_image_upload", ), - path("", views.Index.as_view(), name="index"), + path("", Index.as_view(), name="index"), path( "sitemap.xml", sitemap, @@ -22,63 +28,57 @@ "clubs": ClubsSitemap, } }, - name="django.contrib.sitemaps.views.sitemaps", + name="django.contrib.sitemaps.sitemaps", ), path("api/", include("core.api.urls")), - path("timetable", views.TimetableList.as_view(), name="timetable_list"), + path("timetable", TimetableList.as_view(), name="timetable_list"), path( "timetable/add/term/", - views.TimetableCreate.as_view(), + TimetableCreate.as_view(), name="timetable_create", ), path( "timetable/edit/", - views.TimetableUpdate.as_view(), + TimetableUpdate.as_view(), name="timetable_update", ), - path( - "course/add/term/", views.CourseCreate.as_view(), name="course_create" - ), - path("accounts/profile", views.ProfileRedirect.as_view(), name="profile_redirect"), - path( - "accounts/profile/update", views.ProfileUpdate.as_view(), name="profile_update" - ), - path("user/", views.Profile.as_view(), name="profile_detail"), - path("clubs", views.OrganizationList.as_view(), name="organization_list"), + path("course/add/term/", CourseCreate.as_view(), name="course_create"), + path("accounts/profile", ProfileRedirect.as_view(), name="profile_redirect"), + path("accounts/profile/update", ProfileUpdate.as_view(), name="profile_update"), + path("user/", Profile.as_view(), name="profile_detail"), + path("clubs", OrganizationList.as_view(), name="organization_list"), path( "club/", - views.OrganizationDetail.as_view(), + OrganizationDetail.as_view(), name="organization_detail", ), - path("announcements", views.AnnouncementList.as_view(), name="announcement_list"), + path("announcements", AnnouncementList.as_view(), name="announcement_list"), path( "announcement/", - views.AnnouncementDetail.as_view(), + AnnouncementDetail.as_view(), name="announcement_detail", ), path( "announcements/tag/", - views.AnnouncementTagList.as_view(), + AnnouncementTagList.as_view(), name="announcement_tag_list", ), - path("announcements/feed", views.AnnouncementFeed(), name="announcement_feed"), - path("gallery", views.ExhibitList.as_view(), name="exhibit_list"), - path("blog", views.BlogPostList.as_view(), name="blogpost_list"), - path("blog/", views.BlogPostDetail.as_view(), name="blogpost_detail"), - path( - "blog/tag/", views.BlogPostTagList.as_view(), name="blogpost_tag_list" - ), - path("calendar", views.CalendarView.as_view(), name="calendar"), - path("calendar.ics", views.CalendarFeed(), name="calendar_ical"), - path("map", views.MapView.as_view(), name="map"), - path("about", views.AboutView.as_view(), name="about"), - path("teapot", views.Teapot.as_view(), name="teapot"), - path("justinian", views.Justinian.as_view(), name="justinian"), - path("json", views.Json.as_view(), name="json"), - path("tv", views.TVView.as_view(), name="tv"), - path("tv/clubs", views.TVClubView.as_view(), name="tvclub"), - path("c/", views.OrganizationShort.as_view(), name="organization_short"), - path("raffle", views.RaffleRedirect.as_view(), name="raffle"), + path("announcements/feed", AnnouncementFeed(), name="announcement_feed"), + path("gallery", ExhibitList.as_view(), name="exhibit_list"), + path("blog", BlogPostList.as_view(), name="blogpost_list"), + path("blog/", BlogPostDetail.as_view(), name="blogpost_detail"), + path("blog/tag/", BlogPostTagList.as_view(), name="blogpost_tag_list"), + path("calendar", CalendarView.as_view(), name="calendar"), + path("calendar.ics", CalendarFeed(), name="calendar_ical"), + path("map", MapView.as_view(), name="map"), + path("about", AboutView.as_view(), name="about"), + path("teapot", Teapot.as_view(), name="teapot"), + path("justinian", Justinian.as_view(), name="justinian"), + path("json", Json.as_view(), name="json"), + path("tv", TVView.as_view(), name="tv"), + path("tv/clubs", TVClubView.as_view(), name="tvclub"), + path("c/", OrganizationShort.as_view(), name="organization_short"), + path("raffle", RaffleRedirect.as_view(), name="raffle"), path("hijack/", include("hijack.urls")), ] @@ -86,12 +86,12 @@ urlpatterns += [ path( "announcements/cards", - views.AnnouncementCards.as_view(), + AnnouncementCards.as_view(), name="api_announcements_card", ), path( "blogs/cards", - views.BlogPostCards.as_view(), + BlogPostCards.as_view(), name="api_blog_card", ), ] diff --git a/core/utils/actions.py b/core/utils/actions.py index 7ce934dd..e359eed7 100644 --- a/core/utils/actions.py +++ b/core/utils/actions.py @@ -119,58 +119,6 @@ def send_notif_singleday(modeladmin, request, queryset): notif_events_singleday.delay(date=dt.date.today()) -class AdminPasswordResetForm(ActionForm): - new_password = forms.CharField( - required=False, - label=_(" New password "), - help_text="The password to set for the user if you are using the reset password action", - ) - - -@admin_action_rate_limit( - rate_limit=1, time_period=60 * 60 * 6, scope="user" -) # specific SU can only reset one password every 12 hours, to prevent abuse. if more is needed contact the backend team to reset. -@admin.action( - permissions=["change"], description=_("Reset the password for the selected user") -) -def reset_password(modeladmin, request, queryset): - user = queryset.first() - if not request.user.is_superuser: - modeladmin.message_user( - request, - "You must be a superuser to reset passwords.", - level=messages.WARNING, - ) - return - if user.is_superuser: - modeladmin.message_user( - request, - "You cannot reset the password of a superuser, please contact the backend lead to reset the password.", - level=messages.ERROR, - ) - return - if len(queryset) > 1: - modeladmin.message_user( - request, "Please only select one user at a time.", level=messages.ERROR - ) - return - if not request.POST["new_password"]: - modeladmin.message_user( - request, - "Please enter a new password in the 'New Password' field.", - level=messages.ERROR, - ) - return - user.set_password(request.POST["new_password"]) - user.save() - modeladmin.message_user( - request, f"Password for {user} has been set to the specified password." - ) - - -# FlatPages - - @admin.action( permissions=["change"], description="Archive selected flatpages and download them as a JSON file", diff --git a/core/views/__init__.py b/core/views/__init__.py deleted file mode 100644 index 8d50ba9b..00000000 --- a/core/views/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .index import * -from .organization import * -from .post import * -from .raffle import * -from .timetable import * -from .tv import * -from .user import * diff --git a/core/views/mixins.py b/core/views/mixins.py index 22a6254d..557d97d7 100644 --- a/core/views/mixins.py +++ b/core/views/mixins.py @@ -1,4 +1,27 @@ from django.views.generic.base import ContextMixin +from django import forms +from django.contrib.auth import get_user_model +from django.utils.translation import gettext_lazy as _ + + +class CaseInsensitiveUsernameMixin: + """ + Disallow a username with a case-insensitive match of existing usernames. + Add this mixin to any forms that use the User object + """ + + def clean_username(self): + username = self.cleaned_data.get("username") + if ( + get_user_model() + .objects.filter(username__iexact=username) + .exclude(pk=self.instance.pk) + .exists() + ): + raise forms.ValidationError( + _("The username ‘{}’ is already in use.".format(username)) + ) + return username class TitleMixin(ContextMixin): diff --git a/core/views/timetable.py b/core/views/timetable.py index fb8657d0..328b356c 100644 --- a/core/views/timetable.py +++ b/core/views/timetable.py @@ -6,11 +6,11 @@ from django.views.generic.edit import CreateView, FormMixin, UpdateView from . import mixins -from .. import models -from ..forms import ( - AddCourseForm, +from core import models +from core.forms import ( AddTimetableSelectTermForm, TimetableSelectCoursesForm, + AddCourseForm, )