From e5465632b4c95cd5c938993a027235acaf324b76 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sat, 22 Jan 2022 16:40:30 +0100 Subject: [PATCH 1/9] Pep8 fixes Reformat codebase a bit so it is easier to work with. --- clear_contest.py | 1 - contest/admin.py | 3 +++ contest/cbrparser.py | 37 ++++++++++++++++++++++------------- contest/forms.py | 42 ++++++++++++++++++++++++---------------- contest/models.py | 45 ++++++++++++++++++++++++++++--------------- contest/validators.py | 2 -- contest/views.py | 31 ++++++++++++++++++----------- cqtu/settings.py | 6 ------ cqtu/urls.py | 10 ---------- 9 files changed, 101 insertions(+), 76 deletions(-) diff --git a/clear_contest.py b/clear_contest.py index 102aa28..5fc5996 100755 --- a/clear_contest.py +++ b/clear_contest.py @@ -19,7 +19,6 @@ if confirm != "YES": print("Aborting") sys.exit(1) - from contest.models import QSO, ShadowCall, Reference, User print("{0} QSOs deleted".format(*QSO.objects.all().delete())) diff --git a/contest/admin.py b/contest/admin.py index 6236378..3638a67 100644 --- a/contest/admin.py +++ b/contest/admin.py @@ -1,9 +1,12 @@ from django.contrib import admin + from .models import Frequency, Band, Reference, QSO, User, Contest, ShadowCall, EntryCategory class UserAdmin(admin.ModelAdmin): list_display = ('username', 'dncall', 'qrv2m', 'qrv70cm', 'extra2m70cm') + + admin.site.register(User, UserAdmin) admin.site.register(QSO) admin.site.register(Band) diff --git a/contest/cbrparser.py b/contest/cbrparser.py index 887c6f2..05030df 100644 --- a/contest/cbrparser.py +++ b/contest/cbrparser.py @@ -1,16 +1,16 @@ +import re + from django.shortcuts import render from django.contrib.auth.decorators import login_required -from .models import Contest, Band from django.utils import timezone from django.contrib import messages from django import forms from django.urls import reverse from django.http import HttpResponseRedirect - from .forms import QSOFormWithTime +from .models import Contest, Band -import re def parseCBR(raw): """ Parse a CBR file for the CQTU @@ -20,8 +20,9 @@ def parseCBR(raw): inside them. """ kvlinere = re.compile(r"^(?P[A-Z-]+):(?: (?P.*))?$") - qsore = re.compile(r"^(?P144|432)\s+(?P[A-Z]{2})\s+(?P\d{4}-\d{2}-\d{2} \d{4}) (?P[A-Z0-9/-]+)\s+(?P\d{2,3})\s+(?P[A-Z0-9-]+)\s+(?P[A-Z0-9/-]+)\s+(?P\d{2,3})\s+(?P[A-Z0-9-]+)\s+0$") - + qsore = re.compile(r"^(?P144|432)\s+(?P[A-Z]{2})\s+(?P\d{4}-\d{2}-\d{2} \d{4}) " + r"(?P[A-Z0-9/-]+)\s+(?P\d{2,3})\s+(?P[A-Z0-9-]+)\s+" + r"(?P[A-Z0-9/-]+)\s+(?P\d{2,3})\s+(?P[A-Z0-9-]+)\s+0$") qsoNo = 1 info = { @@ -59,14 +60,17 @@ def parseCBR(raw): elif qsoData["band"] == "432": qsoData["band"] = "70cm" else: - raise forms.ValidationError("Error parsing band, needs to be either 144 or 432 (as we only support 2m and 70cm in this contest") + raise forms.ValidationError("Error parsing band, needs to be either 144 or 432 " + "(as we only support 2m and 70cm in this contest") info["qsos"].append(qsoData) if info["call"] != qsoData["call_s"]: - raise forms.ValidationError("Error in line %d: qso was not made by you? (callsigns do not match)" % n) + raise forms.ValidationError("Error in line %d: qso was not made by you? " + "(callsigns do not match)" % n) if info["location"] != qsoData["exc_s"]: - raise forms.ValidationError("Error in line %d: exchange does not match your location? (callsigns do not match)" % n) + raise forms.ValidationError("Error in line %d: exchange does not match your location? " + "(callsigns do not match)" % n) else: raise forms.ValidationError("Error in line %d: qso was broken, regex did not match" % n) @@ -79,8 +83,10 @@ def parseCBR(raw): return info + class CBRForm(forms.Form): - data = forms.CharField(widget=forms.Textarea, label="Cabrillo data", help_text="Paste your cabrillo file contents here") + data = forms.CharField(widget=forms.Textarea, label="Cabrillo data", + help_text="Paste your cabrillo file contents here") def clean_data(self): rawData = self.cleaned_data["data"] @@ -88,6 +94,7 @@ class CBRForm(forms.Form): return parsedData + def checkCBRConsistency(contest, user, info): errors = [] qsos = [] @@ -95,11 +102,11 @@ def checkCBRConsistency(contest, user, info): errors.append("You are not the owner of this logfile! (%s != %s)" % (user.username, info["call"])) if user.ref.name != info["location"]: - errors.append("Location of logfile and registered exchange do not match! (%s != %s)" % (user.ref.name, info["location"])) + errors.append("Location of logfile and registered exchange do not match! (%s != %s)" % (user.ref.name, + info["location"])) for n, qsoData in enumerate(info["qsos"], 1): qsoFormData = { - #"owner": user, "time": qsoData["datetime"], "call": qsoData["call_r"], "band": Band.objects.get(contest=contest, name=qsoData["band"]).id, @@ -119,6 +126,7 @@ def checkCBRConsistency(contest, user, info): return qsos, errors + @login_required def uploadCBR(request): if not request.user.ref: @@ -149,7 +157,8 @@ def uploadCBR(request): if cnt > 0: messages.success(request, "%d QSOs have been saved from the cbr file" % cnt) else: - messages.warnnig(request, "CBR file was parsed, but no QSOs could be saved, as all cointained errors.") + messages.warnnig(request, "CBR file was parsed, but no QSOs could be saved, " + "as all cointained errors.") return HttpResponseRedirect(reverse("contest:uploadCBR")) else: @@ -157,4 +166,6 @@ def uploadCBR(request): else: deadline = True - return render(request, "contest/uploadCBR.html", {"deadline": deadline, 'form': form, 'verifyData': verifyData, 'verifyErrors': verifyErrors, 'save': save, 'saved': saved}) + return render(request, "contest/uploadCBR.html", + {"deadline": deadline, 'form': form, 'verifyData': verifyData, 'verifyErrors': verifyErrors, + 'save': save, 'saved': saved}) diff --git a/contest/forms.py b/contest/forms.py index 3dcf8d8..c2a0798 100644 --- a/contest/forms.py +++ b/contest/forms.py @@ -1,14 +1,14 @@ -from django import forms -from django.contrib.auth.forms import UserCreationForm -from django.utils import timezone - from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit, Layout +from django import forms +from django.contrib.auth.forms import UserCreationForm from django.urls import reverse +from django.utils import timezone from .models import User, Reference, QSO, ShadowCall, EntryCategory, Contest from .validators import CallUsernameValidator, CallLogValidator + class CustomUserCreationForm(UserCreationForm): class Meta: model = User @@ -17,11 +17,15 @@ class CustomUserCreationForm(UserCreationForm): username = forms.CharField(max_length=50, validators=[CallUsernameValidator()]) email = forms.EmailField(required=True) + class UpdateRefForm(forms.Form): - existingRef = forms.ModelChoiceField(label="Existing Exchange", queryset=Reference.objects.all(), help_text="If exchange already exists, select it here.", required=False) - newRefName = forms.CharField(max_length=50, label="New Exchange", help_text="Enter name of new exchange, if we should create a new", required=False) + existingRef = forms.ModelChoiceField(label="Existing Exchange", queryset=Reference.objects.all(), + help_text="If exchange already exists, select it here.", required=False) + newRefName = forms.CharField(max_length=50, label="New Exchange", + help_text="Enter name of new exchange, if we should create a new", required=False) - location = forms.CharField(max_length=128, label='Exact Location', help_text="E.g. MAR bei den Fahrstuehlen, TEL 15. OG", required=False) + location = forms.CharField(max_length=128, label='Exact Location', + help_text="E.g. MAR bei den Fahrstuehlen, TEL 15. OG", required=False) opName = forms.CharField(max_length=128, label='Operators', help_text="Name of operator(s)", required=False) regTime = forms.DateTimeField(label="Registration time", help_text="Time of Registration") @@ -49,8 +53,10 @@ class UpdateRefForm(forms.Form): if not existingRef and not newRefName: raise forms.ValidationError("Select either an existing exchange or create a new one!") + class UpdateCategoryForm(forms.Form): entry = forms.ModelChoiceField(label="Entry category", queryset=EntryCategory.objects.all()) + def __init__(self, *args, **kwargs): super(UpdateCategoryForm, self).__init__(*args, **kwargs) @@ -68,10 +74,11 @@ class UpdateCategoryForm(forms.Form): if contest.deadline < timezone.now(): raise forms.ValidationError("The deadline for setting your contest category has passed") + class QSOForm(forms.ModelForm): class Meta: model = QSO - #fields = ["ownNo", "band", "call", "reportTX", "reportRX", "refStr", "otherNo", "remarks"] + # fields = ["ownNo", "band", "call", "reportTX", "reportRX", "refStr", "otherNo", "remarks"] fields = ["ownNo", "band", "call", "reportTX", "reportRX", "refStr", "remarks"] def __init__(self, user, *args, **kwargs): @@ -80,16 +87,15 @@ class QSOForm(forms.ModelForm): self.helper = FormHelper() self.helper.form_id = "qso-log-form" - #self.helper.form_class = "form-inline " - #self.helper.form_class = "form-horizontal" - #self.helper.form_style = 'inline' - #self.helper.field_template = "bootstrap3/layout/inline_field.html" + # self.helper.form_class = "form-inline " + # self.helper.form_class = "form-horizontal" + # self.helper.form_style = 'inline' + # self.helper.field_template = "bootstrap3/layout/inline_field.html" self.helper.action = reverse("contest:log") self.helper.add_input(Submit('submit', 'Log')) - #self.helper.layout = Layout( - # #*(QSOForm.Meta.fields + [ButtonHolder(Submit('submit', 'Submit', css_class='button white'))])) - # *(QSOForm.Meta.fields + [FormActions(Submit('submit', 'Log!'))])) - + # self.helper.layout = Layout( + # #*(QSOForm.Meta.fields + [ButtonHolder(Submit('submit', 'Submit', css_class='button white'))])) + # *(QSOForm.Meta.fields + [FormActions(Submit('submit', 'Log!'))])) def clean_call(self): data = self.cleaned_data["call"].upper().strip() @@ -141,12 +147,14 @@ class QSOForm(forms.ModelForm): if band.contest.deadline < timezone.now(): raise forms.ValidationError("The deadline for logging and editing QSOs has passed") + class QSOFormWithTime(QSOForm): class Meta: model = QSO - #fields = ["time", "ownNo", "band", "call", "reportTX", "reportRX", "otherNo", "refStr", "remarks"] + # fields = ["time", "ownNo", "band", "call", "reportTX", "reportRX", "otherNo", "refStr", "remarks"] fields = ["time", "ownNo", "band", "call", "reportTX", "reportRX", "refStr", "remarks"] + class ShadowCallAddForm(forms.ModelForm): class Meta: diff --git a/contest/models.py b/contest/models.py index 404772e..a612032 100644 --- a/contest/models.py +++ b/contest/models.py @@ -2,13 +2,14 @@ from __future__ import unicode_literals import datetime -from django.db import models from django.contrib.auth.models import AbstractUser from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator +from django.db import models from django.db.models import Q, signals -from .validators import CallUsernameValidator from .signals import checkForShadowCall +from .validators import CallUsernameValidator + class Contest(models.Model): name = models.CharField(max_length=20) @@ -34,6 +35,7 @@ class Reference(models.Model): def __str__(self): return self.name + class EntryCategory(models.Model): name = models.CharField(max_length=64, unique=True) description = models.TextField(blank=True) @@ -41,9 +43,10 @@ class EntryCategory(models.Model): def __str__(self): return self.name + class ShadowCall(models.Model): username = models.CharField(max_length=20, unique=True, db_index=True, validators=[CallUsernameValidator()]) - ref = models.ForeignKey(Reference, models.SET_NULL,null=True, blank=True) + ref = models.ForeignKey(Reference, models.SET_NULL, null=True, blank=True) location = models.CharField(max_length=128, default="", blank=True) opName = models.CharField(max_length=128, default="", blank=True) @@ -52,6 +55,7 @@ class ShadowCall(models.Model): def __str__(self): return self.username + class User(AbstractUser): ref = models.ForeignKey(Reference, models.SET_NULL, null=True, blank=True) cat = models.ForeignKey(EntryCategory, models.SET_NULL, null=True, blank=True) @@ -67,17 +71,18 @@ class User(AbstractUser): # extra profile stuff so DL7BST can sleep well without his doodles editedProfile = models.BooleanField(default=False) dncall = models.CharField(max_length=16, default='', blank=True, - verbose_name="DN-Call", - help_text="If you have a DN call that you will offer to SWLs please enter it here") + verbose_name="DN-Call", + help_text="If you have a DN call that you will offer to SWLs please enter it here") qrv2m = models.BooleanField(default=False, verbose_name="QRV on 2m", help_text="Will you be QRV on 2m during the contest?") qrv70cm = models.BooleanField(default=False, - verbose_name="QRV on 70cm", - help_text="Will you be QRV on 70cm during the contest?") + verbose_name="QRV on 70cm", + help_text="Will you be QRV on 70cm during the contest?") extra2m70cm = models.BooleanField(default=False, - verbose_name="Additional 2m/70cm TRX", - help_text="Will you bring an additional 2m/70cm TRX to lend to other participants?") + verbose_name="Additional 2m/70cm TRX", + help_text="Will you bring an additional 2m/70cm TRX to lend to " + "other participants?") def __init__(self, *args, **kwargs): super(User, self).__init__(*args, **kwargs) @@ -124,8 +129,11 @@ class User(AbstractUser): "qsoCount": qsos.count(), "refCount": len(refs) } + + signals.post_save.connect(checkForShadowCall, sender=User) + class Band(models.Model): name = models.CharField(max_length=10) contest = models.ForeignKey(Contest) @@ -133,6 +141,7 @@ class Band(models.Model): def __str__(self): return self.name + class Frequency(models.Model): # qrg # band @@ -145,6 +154,7 @@ class Frequency(models.Model): def __str__(self): return "Channel %s: %s MHz" % (self.channel, self.qrg) + class QSO(models.Model): MAX_NO_VALUE = 1000000 reportValidator = RegexValidator("[1-5][1-9]") @@ -174,7 +184,7 @@ class QSO(models.Model): cfmdQSO = models.ForeignKey("QSO", models.SET_NULL, null=True, blank=True, default=None) - CFMD_SEC = 5*60 + CFMD_SEC = 5 * 60 def checkQSOData(self): """ Match strdata to log rows. Only call, if you intent to save this object if we return True! """ @@ -213,10 +223,10 @@ class QSO(models.Model): # check if this still checks out q = self.cfmdQSO if abs((self.time - q.time).total_seconds()) <= self.CFMD_SEC and \ - self.ref and self.owner.ref and self.callRef and q.callRef and \ - q.owner == self.callRef and q.callRef == self.owner and \ - self.ref == q.owner.ref and self.owner.ref == q.ref and \ - self.band == q.band: + self.ref and self.owner.ref and self.callRef and q.callRef and \ + q.owner == self.callRef and q.callRef == self.owner and \ + self.ref == q.owner.ref and self.owner.ref == q.ref and \ + self.band == q.band: # checks out pass else: @@ -228,7 +238,8 @@ class QSO(models.Model): if self.ref and self.callRef and self.callRef.ref and not self.cfmdQSO: # look for a matching line q = QSO.objects.filter( - (Q(time__lte=self.time + datetime.timedelta(seconds=self.CFMD_SEC)) & Q(time__gte=self.time - datetime.timedelta(seconds=self.CFMD_SEC))), + (Q(time__lte=self.time + datetime.timedelta(seconds=self.CFMD_SEC)) & + Q(time__gte=self.time - datetime.timedelta(seconds=self.CFMD_SEC))), owner=self.callRef, callRef=self.owner, owner__ref=self.ref, @@ -250,4 +261,6 @@ class QSO(models.Model): super(QSO, self).save(*args, **kwargs) def __str__(self): - return "QSO no %s at %s on band %s from %s with %s@%s %s/%s" % (self.ownNo, self.time.strftime("%H:%M"), self.band, self.owner.username, self.call, self.refStr, self.reportTX, self.reportRX) + return "QSO no %s at %s on band %s from %s with %s@%s %s/%s" % (self.ownNo, self.time.strftime("%H:%M"), + self.band, self.owner.username, self.call, + self.refStr, self.reportTX, self.reportRX) diff --git a/contest/validators.py b/contest/validators.py index c157429..fecd7ed 100644 --- a/contest/validators.py +++ b/contest/validators.py @@ -8,7 +8,6 @@ import re @deconstructible class CallUsernameValidator(validators.RegexValidator): - #regex = r'^[\w.@+-]+$' regex = r'^(?:[A-Z]+/)?[A-Z]{1,2}[0-9][A-Z]{1,4}(?:-[0-9])??$' message = _( 'Enter a valid Callsign as Username, ALL UPPERCASE, if needed with -1 / -2,' @@ -18,7 +17,6 @@ class CallUsernameValidator(validators.RegexValidator): @deconstructible class CallLogValidator(validators.RegexValidator): - #regex = r'^[\w.@+-]+$' regex = r'^(?:[A-Z]+/)?[A-Z]{1,2}[0-9][A-Z]{1,4}(?:-[0-9])?(?:/[A-Z]{1,3})?$' message = _( 'Enter a valid callsign, ALL UPPERCASE, if needed with -1 / -2,' diff --git a/contest/views.py b/contest/views.py index 462e25f..f16751f 100644 --- a/contest/views.py +++ b/contest/views.py @@ -1,8 +1,8 @@ -from django.shortcuts import render, get_object_or_404 +import datetime +from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib.admin.views.decorators import staff_member_required -#from django.db.models import Q from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm from django.http import HttpResponseRedirect from django.contrib import messages @@ -11,12 +11,10 @@ from django.contrib.auth import login as auth_login from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.utils import timezone - - -import datetime - from .models import User, Contest, Frequency, Reference, QSO, ShadowCall -from .forms import UpdateRefForm, QSOForm, QSOFormWithTime, CustomUserCreationForm, ShadowCallAddForm, UpdateCategoryForm +from .forms import UpdateRefForm, QSOForm, QSOFormWithTime, CustomUserCreationForm, ShadowCallAddForm, \ + UpdateCategoryForm + def index(request): if request.user.is_authenticated(): @@ -24,6 +22,7 @@ def index(request): return render(request, "index.html", {"loginForm": AuthenticationForm()}) + @login_required def contestIndex(request): qsoform = QSOForm(request.user) @@ -32,6 +31,7 @@ def contestIndex(request): return render(request, 'contest/index.html', {"qsoform": qsoform, "contest": contest, "qrgs": qrgs}) + @login_required def log(request): if not request.user.ref: @@ -68,9 +68,9 @@ def log(request): form = QSOForm(request.user, initial=data) form.helper.form_tag = False - return render(request, 'contest/log.html', {'form': form, 'qsos': qsos}) + @login_required def logEdit(request, qsoid): if not request.user.ref: @@ -91,6 +91,7 @@ def logEdit(request, qsoid): return render(request, 'contest/logEdit.html', {'form': form, "qso": qso}) + def logDelete(request, qsoid): if not request.user.ref: return HttpResponseRedirect(reverse("contest:index")) @@ -109,7 +110,6 @@ def logDelete(request, qsoid): return render(request, 'contest/logDelete.html', {"qso": qso}) - @staff_member_required def registerRefs(request): allUser = User.objects.all() @@ -127,7 +127,9 @@ def registerRefs(request): else: shadowForm = ShadowCallAddForm() - return render(request, 'contest/registerRefs.html', {'alluser': allUser, "qsos": qsos, "shadowForm": shadowForm, "shadows": shadows}) + return render(request, 'contest/registerRefs.html', + {'alluser': allUser, "qsos": qsos, "shadowForm": shadowForm, "shadows": shadows}) + def getPage(paginator, pageNo): try: @@ -153,6 +155,7 @@ def recheckAllQSOs(request): return render(request, "contest/checkAllQSOs.html", {}) + @staff_member_required def viewUserQSOs(request, uid, page=1): user = get_object_or_404(User, id=uid) @@ -162,7 +165,9 @@ def viewUserQSOs(request, uid, page=1): userRefs = set(map(lambda _x: _x["refStr"], user.qso_set.filter(ref__isnull=False).values("ref", "refStr"))) - return render(request, "contest/viewUserQSOs.html", {'owner': user, 'qsos': qsos, 'qsoPage': qsoPage, 'userRefs': userRefs}) + return render(request, "contest/viewUserQSOs.html", + {'owner': user, 'qsos': qsos, 'qsoPage': qsoPage, 'userRefs': userRefs}) + @staff_member_required def updateRef(request, shadow, uid): @@ -208,6 +213,7 @@ def updateRef(request, shadow, uid): return render(request, 'contest/updateRef.html', {'userobj': user, 'form': form, "shadow": shadow}) + @staff_member_required def viewAllQSOs(request, page=1): qsos = QSO.objects.all().order_by("-time") @@ -216,12 +222,14 @@ def viewAllQSOs(request, page=1): return render(request, 'contest/viewAllQSOs.html', {'qsoPage': qsoPage}) + def overview(request): # FIXME: Hardcoded for cqtu... everywhere c = Contest.objects.get(id=1) qrgs = Frequency.objects.filter(band__contest=c).order_by("channel") return render(request, 'contest/overview.html', {'contest': c, 'qrgs': qrgs}) + def register(request): form = None if request.method == 'POST': @@ -237,6 +245,7 @@ def register(request): return render(request, 'registration/register.html', {"form": form}) + @login_required def profile(request): pwForm = None diff --git a/cqtu/settings.py b/cqtu/settings.py index 35d3b9a..9573812 100644 --- a/cqtu/settings.py +++ b/cqtu/settings.py @@ -107,12 +107,6 @@ AUTH_PASSWORD_VALIDATORS = [ 'min_length': 4, }, }, - #{ - # 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - #}, - #{ - # 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - #}, ] diff --git a/cqtu/urls.py b/cqtu/urls.py index 66d856c..a954be5 100644 --- a/cqtu/urls.py +++ b/cqtu/urls.py @@ -15,16 +15,11 @@ Including another URLconf """ from django.conf.urls import url, include from django.contrib import admin - from django.contrib.auth import views as auth_views -#from django.views.generic.edit import CreateView -#from django.contrib.auth.forms import UserCreationForm -#from contest.forms import CustomUserCreationForm from contest.views import index, register, profile - urlpatterns = [ url('^$', index, name="index"), url('^cqtufm2019/', include('contest.urls', namespace='contest')), @@ -35,9 +30,4 @@ urlpatterns = [ url(r'^register/$', register, name='register'), url(r'^profile/$', profile, name='profile'), url(r'^api/', include('api.urls')), - #url(r'^register/$', CreateView.as_view( - # template_name='registration/register.html', - # form_class=CustomUserCreationForm, - # success_url='/', - #), name='register'), ] -- 2.39.1 From 970a3ae5171e9c416dcab5afad1f2b6c8a039648 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sat, 22 Jan 2022 16:47:07 +0100 Subject: [PATCH 2/9] Remove some python2 --> python3 leftovers Remove some __future__ imports and the six compability layer (which was not mentioned in the requirements.txt anyway). --- clear_contest.py | 2 -- contest/apps.py | 2 -- contest/models.py | 2 -- contest/validators.py | 8 ++++---- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/clear_contest.py b/clear_contest.py index 5fc5996..6b07c16 100755 --- a/clear_contest.py +++ b/clear_contest.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -from __future__ import print_function - # prepare environment import sys sys.path.append("..") diff --git a/contest/apps.py b/contest/apps.py index b77a40b..dbe17cd 100644 --- a/contest/apps.py +++ b/contest/apps.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.apps import AppConfig diff --git a/contest/models.py b/contest/models.py index a612032..674b004 100644 --- a/contest/models.py +++ b/contest/models.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import datetime from django.contrib.auth.models import AbstractUser diff --git a/contest/validators.py b/contest/validators.py index fecd7ed..75f5730 100644 --- a/contest/validators.py +++ b/contest/validators.py @@ -1,7 +1,6 @@ from django.core import validators -from django.utils import six from django.utils.deconstruct import deconstructible -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ import re @@ -13,7 +12,8 @@ class CallUsernameValidator(validators.RegexValidator): 'Enter a valid Callsign as Username, ALL UPPERCASE, if needed with -1 / -2,' 'e.g. DL7BST, DN1BER-1, DL/OE1FOO.' ) - flags = re.ASCII if six.PY3 else 0 + flags = re.ASCII + @deconstructible class CallLogValidator(validators.RegexValidator): @@ -22,4 +22,4 @@ class CallLogValidator(validators.RegexValidator): 'Enter a valid callsign, ALL UPPERCASE, if needed with -1 / -2,' 'e.g. DL7BST, DN1BER-1, DL/OE1FOO, DN1FTW-1/p' ) - flags = re.ASCII if six.PY3 else 0 + flags = re.ASCII -- 2.39.1 From 635289ed8f97ff786f3f060a27e83f77d00f43fb Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sat, 22 Jan 2022 16:54:13 +0100 Subject: [PATCH 3/9] Set on_delete for all ForeignKey fields Can't believe this has not been done yet. --- contest/models.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contest/models.py b/contest/models.py index 674b004..b7bc648 100644 --- a/contest/models.py +++ b/contest/models.py @@ -12,7 +12,7 @@ from .validators import CallUsernameValidator class Contest(models.Model): name = models.CharField(max_length=20) shortName = models.CharField(max_length=20, unique=True) - callQrg = models.ForeignKey("Frequency", models.SET_NULL, null=True, blank=True) + callQrg = models.ForeignKey("Frequency", on_delete=models.SET_NULL, null=True, blank=True) deadline = models.DateTimeField() qsoStartTime = models.DateTimeField() @@ -134,7 +134,7 @@ signals.post_save.connect(checkForShadowCall, sender=User) class Band(models.Model): name = models.CharField(max_length=10) - contest = models.ForeignKey(Contest) + contest = models.ForeignKey(Contest, on_delete=models.CASCADE) def __str__(self): return self.name @@ -145,7 +145,7 @@ class Frequency(models.Model): # band channel = models.CharField(max_length=3) qrg = models.DecimalField(max_digits=7, decimal_places=3) - band = models.ForeignKey(Band) + band = models.ForeignKey(Band, on_delete=models.CASCADE) note = models.CharField(max_length=50, blank=True) @@ -162,11 +162,11 @@ class QSO(models.Model): ["owner", "call"], ] - owner = models.ForeignKey(User, db_index=True) + owner = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True) time = models.DateTimeField(blank=True) call = models.CharField(max_length=20, db_index=True) - callRef = models.ForeignKey(User, models.SET_NULL, related_name='qsoref', null=True, blank=True, default=None) - band = models.ForeignKey(Band) + callRef = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='qsoref', null=True, blank=True, default=None) + band = models.ForeignKey(Band, on_delete=models.CASCADE) reportTX = models.CharField(max_length=7, default=59, verbose_name='RS-S', validators=[reportValidator]) reportRX = models.CharField(max_length=7, default=59, verbose_name='RS-R', validators=[reportValidator]) @@ -176,11 +176,11 @@ class QSO(models.Model): validators=[MinValueValidator(1), MaxValueValidator(MAX_NO_VALUE)]) refStr = models.CharField(max_length=20, verbose_name="EXC") - ref = models.ForeignKey(Reference, models.SET_NULL, null=True, blank=True) + ref = models.ForeignKey(Reference, on_delete=models.SET_NULL, null=True, blank=True) remarks = models.CharField(max_length=50, blank=True, default=None) - cfmdQSO = models.ForeignKey("QSO", models.SET_NULL, null=True, blank=True, default=None) + cfmdQSO = models.ForeignKey("QSO", on_delete=models.SET_NULL, null=True, blank=True, default=None) CFMD_SEC = 5 * 60 -- 2.39.1 From 933a337eaccf4f3301873d782d5224152ad5b074 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sat, 22 Jan 2022 17:04:38 +0100 Subject: [PATCH 4/9] Fix url config for Django4 url() is no longer available, has to be replaced with path() or re_path(). For proper include() we also need an app_name specified in the urls.py we're including. --- api/urls.py | 6 +++--- contest/urls.py | 43 +++++++++++++++---------------------------- cqtu/urls.py | 33 +++++++++------------------------ 3 files changed, 27 insertions(+), 55 deletions(-) diff --git a/api/urls.py b/api/urls.py index 57fd854..00877cc 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import include, url +from django.urls import include, path from rest_framework import routers from .views import ContestViewSet, BandViewSet, FrequencyViewSet, EntryCategoryViewSet, ReferenceViewSet, QSOViewSet, \ @@ -15,6 +15,6 @@ router.register('shadowcalls', ShadowCallViewSet) router.register('profile', UserProfileViewSet, basename='profile') urlpatterns = [ - url(r'^', include(router.urls)), - url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), + path('', include(router.urls)), + path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), ] diff --git a/contest/urls.py b/contest/urls.py index 0ce817a..9283ca5 100644 --- a/contest/urls.py +++ b/contest/urls.py @@ -1,35 +1,22 @@ -"""cqtu URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.10/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.conf.urls import url, include - 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) -""" -from django.conf.urls import url +from django.urls import re_path import contest.views as contest_views from contest.cbrparser import uploadCBR +app_name = 'context' + urlpatterns = [ - url(r'^$', contest_views.contestIndex, name='index'), - url(r'^regref/$', contest_views.registerRefs, name='registerRefs'), - url(r'^regref/edit/(?P\d+)/$', contest_views.updateRef, {"shadow": False}, name='updateRef'), - url(r'^regref/shadow/edit/(?P\d+)/$', contest_views.updateRef, {"shadow": True}, name='updateShadowRef'), - url(r'^regref/qsos/all/$', contest_views.viewAllQSOs, name='viewAllQSOs'), - url(r'^regref/qsos/user/(?P\d+)/$', contest_views.viewUserQSOs, name='viewUserQSOs'), - url(r'^overview/$', contest_views.overview, name='overview'), - url(r'^log/$', contest_views.log, name='log'), - url(r'^log/edit/(?P\d+)/$', contest_views.logEdit, name='logEdit'), - url(r'^log/delete/(?P\d+)/$', contest_views.logDelete, name='logDelete'), - url(r'^uploadcbr/$', uploadCBR, name='uploadCBR'), - url(r'^regref/recheckqsos/$', contest_views.recheckAllQSOs, name='recheckAllQSOs'), + re_path(r'^$', contest_views.contestIndex, name='index'), + re_path(r'^regref/$', contest_views.registerRefs, name='registerRefs'), + re_path(r'^regref/edit/(?P\d+)/$', contest_views.updateRef, {"shadow": False}, name='updateRef'), + re_path(r'^regref/shadow/edit/(?P\d+)/$', contest_views.updateRef, {"shadow": True}, name='updateShadowRef'), + re_path(r'^regref/qsos/all/$', contest_views.viewAllQSOs, name='viewAllQSOs'), + re_path(r'^regref/qsos/user/(?P\d+)/$', contest_views.viewUserQSOs, name='viewUserQSOs'), + re_path(r'^overview/$', contest_views.overview, name='overview'), + re_path(r'^log/$', contest_views.log, name='log'), + re_path(r'^log/edit/(?P\d+)/$', contest_views.logEdit, name='logEdit'), + re_path(r'^log/delete/(?P\d+)/$', contest_views.logDelete, name='logDelete'), + re_path(r'^uploadcbr/$', uploadCBR, name='uploadCBR'), + re_path(r'^regref/recheckqsos/$', contest_views.recheckAllQSOs, name='recheckAllQSOs'), ] diff --git a/cqtu/urls.py b/cqtu/urls.py index a954be5..72501c0 100644 --- a/cqtu/urls.py +++ b/cqtu/urls.py @@ -1,19 +1,4 @@ -"""cqtu URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.10/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.conf.urls import url, include - 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) -""" -from django.conf.urls import url, include +from django.urls import include, path from django.contrib import admin from django.contrib.auth import views as auth_views @@ -21,13 +6,13 @@ from contest.views import index, register, profile urlpatterns = [ - url('^$', index, name="index"), - url('^cqtufm2019/', include('contest.urls', namespace='contest')), + path('', index, name="index"), + path('cqtufm2019/', include('contest.urls', namespace='contest')), - url(r'^admin/', admin.site.urls), - url(r'^login/$', auth_views.login, name='login'), - url(r'^logout/$', auth_views.logout, {'next_page': '/'}, name='logout'), - url(r'^register/$', register, name='register'), - url(r'^profile/$', profile, name='profile'), - url(r'^api/', include('api.urls')), + path('admin/', admin.site.urls), + path('login/', auth_views.LoginView.as_view(), name='login'), + path('logout/', auth_views.LogoutView.as_view(), {'next_page': '/'}, name='logout'), + path('register/', register, name='register'), + path('profile/', profile, name='profile'), + path('api/', include('api.urls')), ] -- 2.39.1 From 1f39cc5b40109bf15b16908ef574a76159c9f180 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sat, 22 Jan 2022 17:16:43 +0100 Subject: [PATCH 5/9] user.is_authenticated is no longer a method Apparently this is now a property, so we don't need to call it. We can just use it. --- contest/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/views.py b/contest/views.py index f16751f..80e41b9 100644 --- a/contest/views.py +++ b/contest/views.py @@ -17,7 +17,7 @@ from .forms import UpdateRefForm, QSOForm, QSOFormWithTime, CustomUserCreationFo def index(request): - if request.user.is_authenticated(): + if request.user.is_authenticated: return HttpResponseRedirect(reverse("contest:index")) return render(request, "index.html", {"loginForm": AuthenticationForm()}) -- 2.39.1 From 13afffda02a7e255c919f90cdd00261fdf911084 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sat, 22 Jan 2022 17:17:12 +0100 Subject: [PATCH 6/9] staticfiles template tag libary is called static --- templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base.html b/templates/base.html index f11b8e7..3cdcdca 100644 --- a/templates/base.html +++ b/templates/base.html @@ -8,7 +8,7 @@ - {% load staticfiles %} + {% load static %} CQTUFM2019 - CQ TU FM Contest 2019 -- 2.39.1 From 7ae53b61508ac7e9cb788cdf76b872a3f25d6fd7 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sat, 22 Jan 2022 17:18:25 +0100 Subject: [PATCH 7/9] Set Django to 4.0.1, add DB migrations Now that we're somewhat Django4 compatible it's time to advertise our newfound skill! Not all functionality has been tested, but it is starting again. --- ...er_qso_ownno_alter_user_dncall_and_more.py | 65 +++++++++++++++++++ requirements.txt | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 contest/migrations/0021_alter_qso_otherno_alter_qso_ownno_alter_user_dncall_and_more.py diff --git a/contest/migrations/0021_alter_qso_otherno_alter_qso_ownno_alter_user_dncall_and_more.py b/contest/migrations/0021_alter_qso_otherno_alter_qso_ownno_alter_user_dncall_and_more.py new file mode 100644 index 0000000..0e96451 --- /dev/null +++ b/contest/migrations/0021_alter_qso_otherno_alter_qso_ownno_alter_user_dncall_and_more.py @@ -0,0 +1,65 @@ +# Generated by Django 4.0.1 on 2022-01-22 16:18 + +import django.contrib.auth.validators +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0020_auto_20190122_2348'), + ] + + operations = [ + migrations.AlterField( + model_name='qso', + name='otherNo', + field=models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1000000)], verbose_name='No-R'), + ), + migrations.AlterField( + model_name='qso', + name='ownNo', + field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1000000)], verbose_name='No'), + ), + migrations.AlterField( + model_name='user', + name='dncall', + field=models.CharField(blank=True, default='', help_text='If you have a DN call that you will offer to SWLs please enter it here', max_length=16, verbose_name='DN-Call'), + ), + migrations.AlterField( + model_name='user', + name='extra2m70cm', + field=models.BooleanField(default=False, help_text='Will you bring an additional 2m/70cm TRX to lend to other participants?', verbose_name='Additional 2m/70cm TRX'), + ), + migrations.AlterField( + model_name='user', + name='first_name', + field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + ), + migrations.AlterField( + model_name='user', + name='last_name', + field=models.CharField(blank=True, max_length=150, verbose_name='last name'), + ), + migrations.AlterField( + model_name='user', + name='qrv2m', + field=models.BooleanField(default=False, help_text='Will you be QRV on 2m during the contest?', verbose_name='QRV on 2m'), + ), + migrations.AlterField( + model_name='user', + name='qrv70cm', + field=models.BooleanField(default=False, help_text='Will you be QRV on 70cm during the contest?', verbose_name='QRV on 70cm'), + ), + migrations.AlterField( + model_name='user', + name='regTime', + field=models.DateTimeField(blank=True, default=None, null=True), + ), + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), + ), + ] diff --git a/requirements.txt b/requirements.txt index 66fed1a..633b7d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django<1.12 +Django==4.0.1 django-crispy-forms django-rest-framework django-filter -- 2.39.1 From 1f93f9e7bdef573c49efa2dc221f0af459e6b0a4 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sat, 22 Jan 2022 19:11:47 +0100 Subject: [PATCH 8/9] Dynamically find currently active Contest Previously we had a lot of hardcoded contest information in the templates. Name, ruleset and number of contest are now all taken from the currently active contest and rendered into the templates. Instead of an URL containing the current contest name, we just use a generic /contest/. The API will no longer use "the contest with id=1", but the currently active contest as well. The currently active contest is - for now - the contest with the latest deadline. --- contest/context_processors.py | 7 +++++++ contest/migrations/0022_contest_contestno.py | 20 +++++++++++++++++++ .../migrations/0023_contest_rulesetlink.py | 19 ++++++++++++++++++ contest/models.py | 10 +++++++++- cqtu/settings.py | 2 ++ cqtu/urls.py | 2 +- templates/base.html | 6 +++--- templates/contest/contestPanel.html | 2 +- templates/index.html | 7 ++++++- 9 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 contest/context_processors.py create mode 100644 contest/migrations/0022_contest_contestno.py create mode 100644 contest/migrations/0023_contest_rulesetlink.py diff --git a/contest/context_processors.py b/contest/context_processors.py new file mode 100644 index 0000000..86b18b1 --- /dev/null +++ b/contest/context_processors.py @@ -0,0 +1,7 @@ +from .models import Contest + + +def current_contest(self): + return { + 'current_contest': Contest.get_current_contest(), + } diff --git a/contest/migrations/0022_contest_contestno.py b/contest/migrations/0022_contest_contestno.py new file mode 100644 index 0000000..04f65a4 --- /dev/null +++ b/contest/migrations/0022_contest_contestno.py @@ -0,0 +1,20 @@ +# Generated by Django 4.0.1 on 2022-01-22 17:52 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0021_alter_qso_otherno_alter_qso_ownno_alter_user_dncall_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='contest', + name='contestNo', + field=models.IntegerField(default=1, help_text='Running number of contest (for vanity reasons)', validators=[django.core.validators.MinValueValidator(1)]), + preserve_default=False, + ), + ] diff --git a/contest/migrations/0023_contest_rulesetlink.py b/contest/migrations/0023_contest_rulesetlink.py new file mode 100644 index 0000000..4cbdca7 --- /dev/null +++ b/contest/migrations/0023_contest_rulesetlink.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.1 on 2022-01-22 17:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0022_contest_contestno'), + ] + + operations = [ + migrations.AddField( + model_name='contest', + name='rulesetLink', + field=models.TextField(default='', help_text='URL to the ruleset pdf for this contest'), + preserve_default=False, + ), + ] diff --git a/contest/models.py b/contest/models.py index b7bc648..e746fbb 100644 --- a/contest/models.py +++ b/contest/models.py @@ -12,6 +12,9 @@ from .validators import CallUsernameValidator class Contest(models.Model): name = models.CharField(max_length=20) shortName = models.CharField(max_length=20, unique=True) + contestNo = models.IntegerField(validators=[MinValueValidator(1)], + help_text="Running number of contest (for vanity reasons)") + rulesetLink = models.TextField(help_text="URL to the ruleset pdf for this contest") callQrg = models.ForeignKey("Frequency", on_delete=models.SET_NULL, null=True, blank=True) deadline = models.DateTimeField() @@ -23,7 +26,12 @@ class Contest(models.Model): @classmethod def get_current_contest(cls): - return cls.objects.get(id=1) + # Currently the contest with the latest deadline is the active one + # This definitely has potential for improvement, but it's better than a hardcoded contest + contests = cls.objects.order_by("-deadline") + if len(contests) > 0: + return contests[0] + return None class Reference(models.Model): diff --git a/cqtu/settings.py b/cqtu/settings.py index 9573812..f4b008f 100644 --- a/cqtu/settings.py +++ b/cqtu/settings.py @@ -40,6 +40,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.humanize', 'crispy_forms', 'rest_framework', 'django_filters', @@ -72,6 +73,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'contest.context_processors.current_contest', ], }, }, diff --git a/cqtu/urls.py b/cqtu/urls.py index 72501c0..c0fb3fc 100644 --- a/cqtu/urls.py +++ b/cqtu/urls.py @@ -7,7 +7,7 @@ from contest.views import index, register, profile urlpatterns = [ path('', index, name="index"), - path('cqtufm2019/', include('contest.urls', namespace='contest')), + path('contest/', include('contest.urls', namespace='contest')), path('admin/', admin.site.urls), path('login/', auth_views.LoginView.as_view(), name='login'), diff --git a/templates/base.html b/templates/base.html index 3cdcdca..0b20c2e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -10,7 +10,7 @@ {% load static %} - CQTUFM2019 - CQ TU FM Contest 2019 + {{ current_contest.name }} @@ -35,7 +35,7 @@ - CQ TU FM 2019 + {{ current_contest.name | default:"NO CONTEST" }}