Make the webinterface ready for 2022 #1

Open
seba wants to merge 9 commits from cleanup-and-fixes-2022 into master
9 changed files with 102 additions and 77 deletions
Showing only changes of commit e5465632b4 - Show all commits

View File

@ -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()))

View File

@ -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)

View File

@ -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<key>[A-Z-]+):(?: (?P<value>.*))?$")
qsore = re.compile(r"^(?P<band>144|432)\s+(?P<mode>[A-Z]{2})\s+(?P<datetime>\d{4}-\d{2}-\d{2} \d{4}) (?P<call_s>[A-Z0-9/-]+)\s+(?P<rst_s>\d{2,3})\s+(?P<exc_s>[A-Z0-9-]+)\s+(?P<call_r>[A-Z0-9/-]+)\s+(?P<rst_r>\d{2,3})\s+(?P<exc_r>[A-Z0-9-]+)\s+0$")
qsore = re.compile(r"^(?P<band>144|432)\s+(?P<mode>[A-Z]{2})\s+(?P<datetime>\d{4}-\d{2}-\d{2} \d{4}) "
r"(?P<call_s>[A-Z0-9/-]+)\s+(?P<rst_s>\d{2,3})\s+(?P<exc_s>[A-Z0-9-]+)\s+"
r"(?P<call_r>[A-Z0-9/-]+)\s+(?P<rst_r>\d{2,3})\s+(?P<exc_r>[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})

View File

@ -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)
location = forms.CharField(max_length=128, label='Exact Location', help_text="E.g. MAR bei den Fahrstuehlen, TEL 15. OG", required=False)
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)
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:

View File

@ -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)

View File

@ -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,'

View File

@ -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

View File

@ -107,12 +107,6 @@ AUTH_PASSWORD_VALIDATORS = [
'min_length': 4,
},
},
#{
# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
#},
#{
# 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
#},
]

View File

@ -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'),
]