From e5549cfc24eccf14b87bd78d3a689c3293530db5 Mon Sep 17 00:00:00 2001 From: C4 Patino Date: Sat, 3 Sep 2022 04:18:47 -0500 Subject: [PATCH] More dynamic forms and templates --- housewars/admin/Activity.py | 55 +++++++++++++++++++++------- housewars/admin/House.py | 32 +++++----------- housewars/admin/PointsEntry.py | 26 ++++++++++++- housewars/admin/UserEntry.py | 48 ++++++++++++++++++------ housewars/admin/__init__.py | 8 ++-- housewars/forms.py | 36 ++++++++---------- housewars/managers.py | 29 --------------- housewars/utils/__init__.py | 1 + housewars/utils/get_random_string.py | 9 +++++ housewars/views.py | 19 ++++++---- requirements.txt | 1 + templates/base.html | 4 ++ 12 files changed, 159 insertions(+), 109 deletions(-) delete mode 100644 housewars/managers.py create mode 100644 housewars/utils/__init__.py create mode 100644 housewars/utils/get_random_string.py diff --git a/housewars/admin/Activity.py b/housewars/admin/Activity.py index fce4c49..12107a2 100644 --- a/housewars/admin/Activity.py +++ b/housewars/admin/Activity.py @@ -1,6 +1,19 @@ -from django.contrib import admin +from django.http import HttpResponse +from django.contrib import admin, messages +from django.utils.translation import ngettext -from ..models import Activity, UserEntry +from ..utils import get_random_string +from ..models import Activity, Award, Quota, UserEntry + + +class QuotaInline(admin.TabularInline): + model = Quota + extra = 0 + + +class AwardsInline(admin.TabularInline): + model = Award + extra = 0 @admin.register(Activity) @@ -8,32 +21,48 @@ class ActivityAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['name']}), ('Activity Information', { - 'fields': ['quota', 'time']}), + 'fields': ['default_quota', 'time', 'password']}), ] - list_display = ['name', 'time', 'quota', 'teacher', 'password', 'hawk_signups', - 'great_grey_signups', 'snowy_signups', 'eagle_signups'] + list_display = ['name', 'time', 'teacher', 'password', + 'default_quota', 'hawk_signups', 'great_grey_signups', 'snowy_signups', 'eagle_signups'] - @admin.display(description='Hawk Signups') + inlines = [QuotaInline, AwardsInline] + + actions = ['generate_passwords'] + + @admin.display(description='Hawk') def hawk_signups(self, obj): if (obj.time == None): return "- | -" - return str(UserEntry.hawk.filterActivity1(obj).count()) + " | " + str((UserEntry.hawk.filterActivity2(obj).count(), "-")[obj.time == 60]) + return str(UserEntry.objects.filter(house__name='Hawk', activity1=obj).count()) + ' | ' + str((UserEntry.objects.filter(house__name='Hawk', activity2=obj).count(), '-')[obj.time == 60]) - @admin.display(description='Great Grey Signups') + @admin.display(description='Great Grey') def great_grey_signups(self, obj): if (obj.time == None): return "- | -" - return str(UserEntry.great_grey.filterActivity1(obj).count()) + " | " + str((UserEntry.great_grey.filterActivity2(obj).count(), "-")[obj.time == 60]) + return str(UserEntry.objects.filter(house__name='Great Grey', activity1=obj).count()) + ' | ' + str((UserEntry.objects.filter(house__name='Great Grey', activity2=obj).count(), '-')[obj.time == 60]) - @admin.display(description='Snowy Signups') + @admin.display(description='Snowy') def snowy_signups(self, obj): if (obj.time == None): return "- | -" - return str(UserEntry.snowy.filterActivity1(obj).count()) + " | " + str((UserEntry.snowy.filterActivity2(obj).count(), "-")[obj.time == 60]) + return str(UserEntry.objects.filter(house__name='Snowy', activity1=obj).count()) + ' | ' + str((UserEntry.objects.filter(house__name='Snowy', activity2=obj).count(), '-')[obj.time == 60]) - @admin.display(description='Eagle Signups') + @admin.display(description='Eagle') def eagle_signups(self, obj): if (obj.time == None): return "- | -" - return str(UserEntry.eagle.filterActivity1(obj).count()) + " | " + str((UserEntry.eagle.filterActivity2(obj).count(), "-")[obj.time == 60]) + return str(UserEntry.objects.filter(house__name='Eagle', activity1=obj).count()) + ' | ' + str((UserEntry.objects.filter(house__name='Eagle', activity2=obj).count(), '-')[obj.time == 60]) + + @admin.action(description='Generate passwords for selected activities') + def generate_passwords(self, request, queryset): + for item in queryset: + item.password = get_random_string(8) + item.save() + + self.message_user(request, ngettext( + 'Generated password for %d activity.', + 'Generated passwords for %d activities.', + queryset.count(), + ) % queryset.count(), messages.SUCCESS) diff --git a/housewars/admin/House.py b/housewars/admin/House.py index 72a95bf..2c90c64 100644 --- a/housewars/admin/House.py +++ b/housewars/admin/House.py @@ -1,31 +1,17 @@ -from django.contrib import admin, messages -from django.utils.translation import ngettext -from django.db.models import Sum, F -from django.db.models.functions import Coalesce +from django.contrib import admin -from ..models import House, PointsEntry +from ..models import House @admin.register(House) class HouseAdmin(admin.ModelAdmin): - list_display = ('name', 'points', 'current_points', 'total_points') + list_display = ('name', 'points') actions = ['finalize_points'] - @admin.display(description='Current Housewars Points') - def current_points(self, obj): - return obj.current_points + # @admin.display(description='Current Housewars Points') + # def current_points(self, obj): + # return obj.current_points - @admin.display(description='Total Points') - def total_points(self, obj): - return obj.total_points - - @admin.action(description='Finalize selected house points') - def finalize_points(self, request, queryset): - updated = queryset.update( - points=queryset.aggregate(points__sum=Coalesce(Sum('pointsentry__points'), 0)).get('points__sum') + F('points')) - PointsEntry.objects.filter(pk__in=queryset).delete() - self.message_user(request, ngettext( - 'Finalized house points for %d house.', - 'Finalized house points for %d houses.', - updated, - ) % updated, messages.SUCCESS) + # @admin.display(description='Total Points') + # def total_points(self, obj): + # return obj.total_points diff --git a/housewars/admin/PointsEntry.py b/housewars/admin/PointsEntry.py index e89ce56..670bf84 100644 --- a/housewars/admin/PointsEntry.py +++ b/housewars/admin/PointsEntry.py @@ -1,8 +1,32 @@ from django.contrib import admin +from django.db.models import F from ..models import PointsEntry +class ValidatedPasswordFilter(admin.SimpleListFilter): + title = 'valid password' + + parameter_name = 'valid' + + def lookups(self, request, model_admin): + return (('True', True), ('False', False)) + + def queryset(self, request, queryset): + if (self.value() == 'True'): + return queryset.filter(password=F('activity__password')) + elif (self.value() == 'False'): + return queryset.exclude(password=F('activity__password')) + else: + return queryset + + @admin.register(PointsEntry) class PointsEntryAdmin(admin.ModelAdmin): - list_display = ('activity', 'name', 'house', 'points') + list_display = ('activity', 'award', 'house', 'validated') + + list_filter = ('activity', ValidatedPasswordFilter) + + @admin.display(description='Validated', boolean=True) + def validated(self, obj): + return obj.password == obj.activity.password diff --git a/housewars/admin/UserEntry.py b/housewars/admin/UserEntry.py index e9d6a11..18fa1c1 100644 --- a/housewars/admin/UserEntry.py +++ b/housewars/admin/UserEntry.py @@ -1,6 +1,9 @@ from django.contrib import admin from django.db.models import F, Value from django.db.models.functions import Concat +from django.http import HttpResponse + +import csv from ..models import UserEntry @@ -8,17 +11,9 @@ from ..models import UserEntry class MentorListFilter(admin.SimpleListFilter): title = 'mentor teacher' - # Parameter for the filter that will be used in the URL query. parameter_name = 'mentor' def lookups(self, request, model_admin): - """ - Returns a list of tuples. The first element in each - tuple is the coded value for the option that will - appear in the URL query. The second element is the - human-readable name for the option that will appear - in the right sidebar. - """ qs = model_admin.get_queryset(request).annotate(full_name=Concat(F( 'house__teacher__first_name'), Value(' '), F('house__teacher__last_name'))).values_list('house__teacher', 'full_name').distinct() @@ -31,7 +26,7 @@ class MentorListFilter(admin.SimpleListFilter): return queryset.filter(house__teacher=self.value(), grade=F('house__teacher__grade')) -@ admin.register(UserEntry) +@admin.register(UserEntry) class UserEntryAdmin(admin.ModelAdmin): fieldsets = [ ('Personal Information', {'fields': [ @@ -46,20 +41,49 @@ class UserEntryAdmin(admin.ModelAdmin): list_filter = ['house', 'grade', MentorListFilter, 'activity1', 'activity1__teacher', 'activity2', 'activity2__teacher'] - @ admin.display(description='Activity 1 Teacher') + actions = ['export_to_csv'] + + @admin.display(description='Activity 1 Teacher') def activity1_teacher(self, obj): if (obj.activity1 == None): return None return obj.activity1.teacher - @ admin.display(description='Mentor Teacher') + @admin.display(description='Mentor Teacher') def mentor_teacher(self, obj): return obj.mentor - @ admin.display(description='Activity 2 Teacher') + @admin.display(description='Activity 2 Teacher') def activity2_teacher(self, obj): if (obj.activity2 == None): return None return obj.activity2.teacher + + @admin.action(description='Export selected to csv') + def export_to_csv(self, request, queryset): + field_names = [field.name for field in queryset.model._meta.fields] + + response = HttpResponse(content_type='text/csv', headers={ + 'Content-Disposition': 'attachment; filename="export.csv"'},) + + writer = csv.writer(response, delimiter=";") + # Write a first row with header information + writer.writerow(field_names) + # Write data rows + for row in queryset: + values = [] + for field in field_names: + value = getattr(row, field) + if callable(value): + try: + value = value() or '' + except: + value = 'Error retrieving value' + if value is None: + value = '' + values.append(value) + writer.writerow(values) + + return response diff --git a/housewars/admin/__init__.py b/housewars/admin/__init__.py index 459d145..e8eaabf 100644 --- a/housewars/admin/__init__.py +++ b/housewars/admin/__init__.py @@ -1,5 +1,5 @@ from .Activity import ActivityAdmin -from .House import HouseAdmin -from .Teacher import TeacherAdmin -from .UserEntry import UserEntryAdmin -from .PointsEntry import PointsEntryAdmin +from .House import House +from .PointsEntry import PointsEntry +from .Teacher import Teacher +from .UserEntry import UserEntry diff --git a/housewars/forms.py b/housewars/forms.py index f0e7fbd..d5c6b0a 100644 --- a/housewars/forms.py +++ b/housewars/forms.py @@ -1,6 +1,7 @@ from django.forms import (CharField, ChoiceField, EmailField, EmailInput, Form, - ModelChoiceField, ModelForm, NumberInput, Select, - Textarea, TextInput, ValidationError) + ModelChoiceField, ModelForm, Select, + TextInput, ValidationError) +from smart_selects.form_fields import ChainedModelChoiceField from .models import Activity, House, PointsEntry, UserEntry @@ -32,6 +33,11 @@ class UserEntryForm(Form): 'placeholder': 'House' })) + def clean(self): + email = self.cleaned_data['email'] + if UserEntry.objects.filter(email=email).exists(): + raise ValidationError("A signup with this email already exists") + class ActivityForm(Form): activity1 = ModelChoiceField(Activity.objects.all(), widget=Select(attrs={ @@ -69,30 +75,20 @@ class PointsEntryForm(ModelForm): model = PointsEntry fields = '__all__' widgets = { - 'name': TextInput(attrs={ - 'class': 'form-control', - 'id': 'first-name', - 'placeholder': 'Full Name' - }), - 'activity': Select(attrs={ - 'class': "form-select", - 'id': 'house', - 'placeholder': 'Activity' - }), 'house': Select(attrs={ 'class': "form-select", 'id': 'house', 'placeholder': 'House' }), - 'points': NumberInput(attrs={ - 'class': "form-control", - 'id': 'last-name', - 'placeholder': 'Number of Points' - }), - 'comment': Textarea(attrs={ + 'password': TextInput(attrs={ 'class': 'form-control', 'id': 'first-name', - 'placeholder': 'Comments', - 'style': 'height:10vh;' + 'placeholder': 'Password', + 'type': 'password' }) } + + def __init__(self, *args, **kwargs): + super(PointsEntryForm, self).__init__(*args, **kwargs) + self.fields['activity'].widget.attrs.update({'class': 'form-select'}) + self.fields['award'].widget.attrs.update({'class': 'form-select'}) diff --git a/housewars/managers.py b/housewars/managers.py deleted file mode 100644 index 2cc516d..0000000 --- a/housewars/managers.py +++ /dev/null @@ -1,29 +0,0 @@ -from django.db.models import Manager - - -class EntryManager(Manager): - def filterActivity1(self, act): - return self.filter(activity1=act) - - def filterActivity2(self, act): - return self.filter(activity2=act) - - -class HawkEntryManager(EntryManager): - def get_queryset(self): - return super().get_queryset().filter(house__name='Hawk') - - -class EagleEntryManager(EntryManager): - def get_queryset(self): - return super().get_queryset().filter(house__name='Eagle') - - -class GreatGreyEntryManager(EntryManager): - def get_queryset(self): - return super().get_queryset().filter(house__name='Great Grey') - - -class SnowyEntryManager(EntryManager): - def get_queryset(self): - return super().get_queryset().filter(house__name='Snowy') diff --git a/housewars/utils/__init__.py b/housewars/utils/__init__.py new file mode 100644 index 0000000..64801bb --- /dev/null +++ b/housewars/utils/__init__.py @@ -0,0 +1 @@ +from .get_random_string import get_random_string diff --git a/housewars/utils/get_random_string.py b/housewars/utils/get_random_string.py new file mode 100644 index 0000000..c034ce4 --- /dev/null +++ b/housewars/utils/get_random_string.py @@ -0,0 +1,9 @@ +import random +import string + + +def get_random_string(length): + # choose from all lowercase letter + letters = string.ascii_lowercase + result_str = ''.join(random.choice(letters) for i in range(length)) + return result_str diff --git a/housewars/views.py b/housewars/views.py index d44bbee..51cd9b8 100644 --- a/housewars/views.py +++ b/housewars/views.py @@ -1,12 +1,13 @@ from django.contrib import messages -from django.db.models import Count, F, Q +from django.db.models import Count, F, Q, Subquery, OuterRef +from django.db.models.functions import Coalesce from django.shortcuts import redirect from django.urls import reverse from django.views.generic import CreateView from formtools.wizard.views import SessionWizardView from .forms import ActivityForm, PointsEntryForm, UserEntryForm -from .models import Activity, PointsEntry, UserEntry +from .models import Activity, Quota, PointsEntry, UserEntry FORMS = [("user", UserEntryForm), ("activity", ActivityForm)] @@ -37,10 +38,14 @@ class EntryCreateView(SessionWizardView): if step == 'activity': cd = self.storage.get_step_data('user') - filtered1 = Activity.objects.filter(time__isnull=False).annotate(count=Count( - 'activity1', filter=Q(activity1__house=cd.get('user-house')))).filter(quota__gt=F('count')) - filtered2 = Activity.objects.filter(time__isnull=False).annotate(count=Count( - 'activity2', filter=Q(activity2__house=cd.get('user-house')))).filter(quota__gt=F('count'), time=30) + quota = Quota.objects.filter( + house=cd.get('user-house'), activity=OuterRef('id')).values('quota') + + filtered1 = Activity.objects.filter(time__isnull=False).annotate(final_quota=Coalesce(Subquery(quota[:1]), F('default_quota'))).annotate(count=Count( + 'activity1', filter=Q(activity1__house=cd.get('user-house')))).filter(final_quota__gt=F('count')) + + filtered2 = Activity.objects.filter(time__isnull=False).annotate(final_quota=Coalesce(Subquery(quota[:1]), F('default_quota'))).annotate(count=Count( + 'activity1', filter=Q(activity1__house=cd.get('user-house')))).filter(final_quota__gt=F('count'), time=30) form.fields['activity1'].queryset = filtered1 form.fields['activity2'].queryset = filtered2 @@ -48,7 +53,7 @@ class EntryCreateView(SessionWizardView): return form def done(self, form_list, **kwargs): - UserEntry.objects.create(self.get_all_cleaned_data()) + UserEntry.objects.create(**self.get_all_cleaned_data()) messages.success(self.request, 'Your entry has been submitted') return redirect('housewars:signup') diff --git a/requirements.txt b/requirements.txt index 43a367f..df684d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ asgiref==3.5.2 autopep8==1.6.0 Django==4.0.4 django-formtools==2.3 +django-smart-selects==1.6.0 gunicorn==20.1.0 mysqlclient==2.1.1 pycodestyle==2.8.0 diff --git a/templates/base.html b/templates/base.html index d93e526..17f0f1e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,6 +9,10 @@ integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous"> + + + + {% block title %} House Wars