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/clear_contest.py b/clear_contest.py index 102aa28..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("..") @@ -19,7 +17,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/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/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/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/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/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/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 404772e..e746fbb 100644 --- a/contest/models.py +++ b/contest/models.py @@ -1,19 +1,21 @@ -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) shortName = models.CharField(max_length=20, unique=True) - callQrg = models.ForeignKey("Frequency", models.SET_NULL, null=True, blank=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() qsoStartTime = models.DateTimeField() @@ -24,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): @@ -34,6 +41,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 +49,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 +61,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 +77,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,27 +135,32 @@ 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) + contest = models.ForeignKey(Contest, on_delete=models.CASCADE) def __str__(self): return self.name + class Frequency(models.Model): # qrg # 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) 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]") @@ -154,11 +170,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]) @@ -168,13 +184,13 @@ 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 + 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 +229,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 +244,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 +267,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/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/contest/validators.py b/contest/validators.py index c157429..75f5730 100644 --- a/contest/validators.py +++ b/contest/validators.py @@ -1,27 +1,25 @@ 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 @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,' 'e.g. DL7BST, DN1BER-1, DL/OE1FOO.' ) - flags = re.ASCII if six.PY3 else 0 + flags = re.ASCII + @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,' 'e.g. DL7BST, DN1BER-1, DL/OE1FOO, DN1FTW-1/p' ) - flags = re.ASCII if six.PY3 else 0 + flags = re.ASCII diff --git a/contest/views.py b/contest/views.py index 462e25f..80e41b9 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,19 +11,18 @@ 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(): + if request.user.is_authenticated: return HttpResponseRedirect(reverse("contest:index")) 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..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', ], }, }, @@ -107,12 +109,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..c0fb3fc 100644 --- a/cqtu/urls.py +++ b/cqtu/urls.py @@ -1,43 +1,18 @@ -"""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 -#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')), + path('', index, name="index"), + path('contest/', 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')), - #url(r'^register/$', CreateView.as_view( - # template_name='registration/register.html', - # form_class=CustomUserCreationForm, - # success_url='/', - #), name='register'), + 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')), ] 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 diff --git a/templates/base.html b/templates/base.html index f11b8e7..d54c4ae 100644 --- a/templates/base.html +++ b/templates/base.html @@ -8,9 +8,9 @@ - {% load staticfiles %} + {% 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" }}