Compare commits

...

18 Commits

Author SHA1 Message Date
Sebastian Lohff 7ae53b6150 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.
2022-01-22 17:35:57 +01:00
Sebastian Lohff 13afffda02 staticfiles template tag libary is called static 2022-01-22 17:35:57 +01:00
Sebastian Lohff 1f39cc5b40 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.
2022-01-22 17:35:57 +01:00
Sebastian Lohff 933a337eac 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.
2022-01-22 17:35:57 +01:00
Sebastian Lohff 635289ed8f Set on_delete for all ForeignKey fields
Can't believe this has not been done yet.
2022-01-22 17:35:57 +01:00
Sebastian Lohff 970a3ae517 Remove some python2 --> python3 leftovers
Remove some __future__ imports and the six compability layer (which was
not mentioned in the requirements.txt anyway).
2022-01-22 17:35:57 +01:00
Sebastian Lohff e5465632b4 Pep8 fixes
Reformat codebase a bit so it is easier to work with.
2022-01-22 17:35:57 +01:00
Sebastian Lohff 613cf9c099 Make profile a modelviewset + add /me/ for current profile 2022-01-22 17:27:39 +01:00
Sebastian Lohff fc78b91c51 Reorganize imports of api views.py 2022-01-22 17:27:39 +01:00
Sebastian Lohff 3c6936ba44 Move QSO no validation into model 2022-01-22 17:27:39 +01:00
Sebastian Lohff a7f303e651 Remove broken check from QSO form
The check disallowed having a QSO with any callsign that is also a
reference. Obviously the user should also be able to log this kind
of QSO, even if it might not make sense. We don't judge.
2022-01-22 17:27:39 +01:00
Sebastian Lohff 0c8ba5eb87 Add REST API to webinterface 2022-01-22 17:27:39 +01:00
Sebastian Lohff b528d4dbb4 Add REST API to webinterface 2022-01-22 17:27:39 +01:00
Sebastian Lohff b0d7f9f2ec Allow regTime in user to be blank as well
Without regTime being blank a user with regTime NULL cannot be edited
via the admin interface
2022-01-22 17:27:39 +01:00
Sebastian Lohff c41a30c66b clear_contest: make python3 ready, remove unused import 2022-01-22 17:27:39 +01:00
Sebastian Lohff a00ae715ac Bump django version to 1.11 2022-01-22 17:27:39 +01:00
Sebastian Lohff ba9c99e0ce Move to consistent use of spaces instead of tabs
Required for python3 transition
2022-01-22 17:27:39 +01:00
Sebastian Lohff c9cff07432 Clarify choice of username at registration, again
Apparently it was still unclear to people what call they should use on
registration. Added extra message to explain this further.
2020-01-28 11:24:31 +01:00
25 changed files with 1003 additions and 707 deletions

0
api/__init__.py Normal file
View File

3
api/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
api/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = 'api'

View File

3
api/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

98
api/serializers.py Normal file
View File

@ -0,0 +1,98 @@
from django import forms
from django.utils import timezone
from rest_framework import serializers
from contest.models import Contest, Band, Frequency, QSO, EntryCategory, User, ShadowCall, Reference
from contest.validators import CallLogValidator
class ContestSerializer(serializers.ModelSerializer):
class Meta:
model = Contest
# FIXME: add callQrg
fields = ('id', 'shortName', 'deadline', 'qsoStartTime', 'qsoEndTime', 'callQrg')
class BandSerializer(serializers.ModelSerializer):
# contest = ContestSerializer()
class Meta:
model = Band
fields = ('id', 'name', 'contest')
class FrequencySerializer(serializers.ModelSerializer):
# band = BandSerializer()
class Meta:
model = Frequency
fields = ('id', 'channel', 'qrg', 'band', 'note')
class EntryCategorySerializer(serializers.ModelSerializer):
class Meta:
model = EntryCategory
fields = ('id', 'name', 'description')
class ReferenceSerializer(serializers.ModelSerializer):
class Meta:
model = Reference
fields = ('id', 'name', 'description')
class UserSerializer(serializers.ModelSerializer):
# ref = ReferenceSerializer()
# cat = EntryCategorySerializer()
class Meta:
model = User
fields = ('id', 'ref', 'cat', 'location', 'opName', 'regTime', 'dncall', 'qrv2m', 'qrv70cm', 'extra2m70cm')
read_only_fields = ('ref', 'location', 'regTime')
def validate(self, attrs):
contest = Contest.get_current_contest()
if contest.deadline < timezone.now():
raise serializers.ValidationError("The deadline for changing the entry category has passed")
return attrs
class QSOSerializer(serializers.ModelSerializer):
class Meta:
model = QSO
fields = ('id', 'owner', 'time', 'ownNo', 'band', 'call', 'reportTX', 'reportRX', 'refStr', 'remarks')
read_only_fields = ('owner',)
def validate_call(self, value):
val = value.strip().upper()
try:
CallLogValidator()(val)
except forms.ValidationError as e:
raise serializers.ValidationError({'errors': e.error_list})
return val
def validate(self, attrs):
ownNo = attrs['ownNo']
try:
o = QSO.objects.get(owner=self.context['request'].user, ownNo=ownNo)
if not (self.instance and self.instance.id and self.instance.id == o.id):
raise serializers.ValidationError("You already logged a QSO with the number %s" % ownNo)
except QSO.DoesNotExist:
pass
band = attrs.get('band')
if band:
if band.contest.deadline < timezone.now():
raise serializers.ValidationError("The deadline for logging and editing QSOs has passed")
return attrs
class ShadowCallSerializer(serializers.ModelSerializer):
ref = ReferenceSerializer()
class Meta:
model = ShadowCall
fields = ('id', 'username', 'ref', 'location', 'opName', 'regTime')

3
api/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

20
api/urls.py Normal file
View File

@ -0,0 +1,20 @@
from django.urls import include, path
from rest_framework import routers
from .views import ContestViewSet, BandViewSet, FrequencyViewSet, EntryCategoryViewSet, ReferenceViewSet, QSOViewSet, \
ShadowCallViewSet, UserProfileViewSet
router = routers.DefaultRouter()
router.register('contests', ContestViewSet)
router.register('bands', BandViewSet)
router.register('frequencies', FrequencyViewSet)
router.register('entrycategories', EntryCategoryViewSet)
router.register('references', ReferenceViewSet)
router.register('qsos', QSOViewSet, basename='qso')
router.register('shadowcalls', ShadowCallViewSet)
router.register('profile', UserProfileViewSet, basename='profile')
urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

91
api/views.py Normal file
View File

@ -0,0 +1,91 @@
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from rest_framework import viewsets
from .serializers import ContestSerializer, BandSerializer, FrequencySerializer, EntryCategorySerializer, \
ReferenceSerializer, QSOSerializer, ShadowCallSerializer, UserSerializer
from contest.models import Contest, Band, Frequency, EntryCategory, Reference, QSO, ShadowCall, User
class ContestViewSet(viewsets.ReadOnlyModelViewSet):
"""
Resource to list and view all available contests. Use `current/` to get the current Contest.
"""
queryset = Contest.objects.all()
serializer_class = ContestSerializer
filterset_fields = ['shortName']
def get_object(self):
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
if self.kwargs.get(lookup_url_kwarg) == "current":
obj = Contest.get_current_contest()
self.check_object_permissions(self.request, obj)
else:
obj = super(ContestViewSet, self).get_object()
return obj
class BandViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Band.objects.all()
serializer_class = BandSerializer
filterset_fields = ['name', 'contest']
class FrequencyViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Frequency.objects.all()
serializer_class = FrequencySerializer
filterset_fields = ['band', 'channel']
class EntryCategoryViewSet(viewsets.ReadOnlyModelViewSet):
queryset = EntryCategory.objects.all()
serializer_class = EntryCategorySerializer
filterset_fields = ['name']
class ReferenceViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAdminUser]
queryset = Reference.objects.all()
serializer_class = ReferenceSerializer
filterset_fields = ['name']
class QSOViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
serializer_class = QSOSerializer
filterset_fields = ['time', 'ownNo', 'band', 'call', 'refStr']
def get_queryset(self):
return QSO.objects.filter(owner=self.request.user)
def perform_create(self, serializer):
return serializer.save(owner=self.request.user)
class UserProfileViewSet(viewsets.ModelViewSet):
"""
Resource to view user-profiles, currently restricted to the current user's profile.
Use `me/` to get the profile of the currently logged in user.
"""
permission_classes = [IsAuthenticated]
serializer_class = UserSerializer
def get_queryset(self):
return User.objects.filter(id=self.request.user.id)
def get_object(self):
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
if self.kwargs.get(lookup_url_kwarg) == "me":
obj = self.request.user
self.check_object_permissions(self.request, obj)
else:
obj = super(ContestViewSet, self).get_object()
return obj
class ShadowCallViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAdminUser]
queryset = ShadowCall.objects.all()
serializer_class = ShadowCallSerializer
filterset_fields = ['username', 'ref']

View File

@ -1,8 +1,4 @@
#!/usr/bin/env python
from __future__ import print_function
import datetime
# prepare environment
import sys
sys.path.append("..")
@ -11,13 +7,16 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cqtu.settings")
import django
django.setup()
confirm = raw_input("Do are you sure you want to clear all contest data? Answer with uppercase YES: ")
confirm_msg = "Do are you sure you want to clear all contest data? Answer with uppercase YES: "
try:
confirm = raw_input(confirm_msg)
except NameError:
confirm = input(confirm_msg)
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,5 +1,3 @@
from __future__ import unicode_literals
from django.apps import AppConfig

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,6 +74,7 @@ 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
@ -90,11 +97,8 @@ class QSOForm(forms.ModelForm):
# #*(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()
if Reference.objects.filter(name=data).count() > 0:
raise forms.ValidationError("Reference already exists")
try:
CallLogValidator()(data)
@ -143,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", "refStr", "remarks"]
class ShadowCallAddForm(forms.ModelForm):
class Meta:

View File

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

View File

@ -1,19 +1,18 @@
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
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)
callQrg = models.ForeignKey("Frequency", on_delete=models.SET_NULL, null=True, blank=True)
deadline = models.DateTimeField()
qsoStartTime = models.DateTimeField()
@ -22,6 +21,11 @@ class Contest(models.Model):
def __str__(self):
return self.name
@classmethod
def get_current_contest(cls):
return cls.objects.get(id=1)
class Reference(models.Model):
name = models.CharField(max_length=20, unique=True, db_index=True)
description = models.TextField()
@ -29,6 +33,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)
@ -36,6 +41,7 @@ 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)
@ -47,13 +53,14 @@ 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)
location = models.CharField(max_length=128, default="", blank=True)
opName = models.CharField(max_length=128, default="", blank=True)
regTime = models.DateTimeField(null=True, default=None)
regTime = models.DateTimeField(null=True, default=None, blank=True)
# because of cbr parsing bug, we sometimes have users who only have 70cm qsos
# we ignore the band for them when checking QSOs
@ -72,7 +79,8 @@ class User(AbstractUser):
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?")
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)
@ -119,28 +127,34 @@ 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]")
class Meta:
@ -148,24 +162,25 @@ 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])
ownNo = models.IntegerField(verbose_name='No')
otherNo = models.IntegerField(verbose_name='No-R', null=True, blank=True)
ownNo = models.IntegerField(verbose_name='No', validators=[MinValueValidator(1), MaxValueValidator(MAX_NO_VALUE)])
otherNo = models.IntegerField(verbose_name='No-R', null=True, blank=True,
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
@ -221,7 +236,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,
@ -243,4 +259,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

@ -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<uid>\d+)/$', contest_views.updateRef, {"shadow": False}, name='updateRef'),
url(r'^regref/shadow/edit/(?P<uid>\d+)/$', contest_views.updateRef, {"shadow": True}, name='updateShadowRef'),
url(r'^regref/qsos/all/$', contest_views.viewAllQSOs, name='viewAllQSOs'),
url(r'^regref/qsos/user/(?P<uid>\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<qsoid>\d+)/$', contest_views.logEdit, name='logEdit'),
url(r'^log/delete/(?P<qsoid>\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<uid>\d+)/$', contest_views.updateRef, {"shadow": False}, name='updateRef'),
re_path(r'^regref/shadow/edit/(?P<uid>\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<uid>\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<qsoid>\d+)/$', contest_views.logEdit, name='logEdit'),
re_path(r'^log/delete/(?P<qsoid>\d+)/$', contest_views.logDelete, name='logDelete'),
re_path(r'^uploadcbr/$', uploadCBR, name='uploadCBR'),
re_path(r'^regref/recheckqsos/$', contest_views.recheckAllQSOs, name='recheckAllQSOs'),
]

View File

@ -1,28 +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

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

View File

@ -40,11 +40,13 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'crispy_forms',
'rest_framework',
'django_filters',
# local
'contest',
'api',
]
MIDDLEWARE = [
@ -105,12 +107,6 @@ AUTH_PASSWORD_VALIDATORS = [
'min_length': 4,
},
},
#{
# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
#},
#{
# 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
#},
]
@ -143,3 +139,6 @@ MESSAGE_TAGS = {
messages.ERROR: 'danger',
}
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}

View File

@ -1,42 +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('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'^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')),
]

View File

@ -1,2 +1,4 @@
Django==1.10
Django==4.0.1
django-crispy-forms
django-rest-framework
django-filter

View File

@ -8,7 +8,7 @@
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="/favicon.ico">
{% load staticfiles %}
{% load static %}
<title>CQTUFM2019 - CQ TU FM Contest 2019</title>

View File

@ -10,7 +10,10 @@
<div class="panel-body">
<p>
Please register with your (uppercase) Callsign as Usernames.
For DN-Calls, -[0-9] is allowed.
For DN-Calls, -[0-9] is allowed (e.g. DN1ABC-2 for the second group).
</p>
<p>
Note: If you are a <strong>Ham/OM/YL</strong> please with your <strong>own</strong> callsign (e.g. DL7DOC). If you are a <strong>SWL</strong>, please use the <strong>DN-Call provided</strong> by your operator.
</p>
<form method="POST" action="{% url 'register' %}">
{% csrf_token %}