Compare commits

...

11 Commits

Author SHA1 Message Date
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
21 changed files with 868 additions and 628 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.conf.urls import include, url
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 = [
url(r'^', include(router.urls)),
url(r'^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,6 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import print_function from __future__ import print_function
import datetime
# prepare environment # prepare environment
import sys import sys
sys.path.append("..") sys.path.append("..")
@ -11,7 +9,11 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cqtu.settings")
import django import django
django.setup() 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": if confirm != "YES":
print("Aborting") print("Aborting")

View File

@ -93,8 +93,6 @@ class QSOForm(forms.ModelForm):
def clean_call(self): def clean_call(self):
data = self.cleaned_data["call"].upper().strip() data = self.cleaned_data["call"].upper().strip()
if Reference.objects.filter(name=data).count() > 0:
raise forms.ValidationError("Reference already exists")
try: try:
CallLogValidator()(data) CallLogValidator()(data)

View File

@ -4,7 +4,7 @@ import datetime
from django.db import models from django.db import models
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.validators import RegexValidator from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator
from django.db.models import Q, signals from django.db.models import Q, signals
from .validators import CallUsernameValidator from .validators import CallUsernameValidator
@ -22,6 +22,11 @@ class Contest(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
@classmethod
def get_current_contest(cls):
return cls.objects.get(id=1)
class Reference(models.Model): class Reference(models.Model):
name = models.CharField(max_length=20, unique=True, db_index=True) name = models.CharField(max_length=20, unique=True, db_index=True)
description = models.TextField() description = models.TextField()
@ -53,7 +58,7 @@ class User(AbstractUser):
location = models.CharField(max_length=128, default="", blank=True) location = models.CharField(max_length=128, default="", blank=True)
opName = 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 # because of cbr parsing bug, we sometimes have users who only have 70cm qsos
# we ignore the band for them when checking QSOs # we ignore the band for them when checking QSOs
@ -141,6 +146,7 @@ class Frequency(models.Model):
return "Channel %s: %s MHz" % (self.channel, self.qrg) return "Channel %s: %s MHz" % (self.channel, self.qrg)
class QSO(models.Model): class QSO(models.Model):
MAX_NO_VALUE = 1000000
reportValidator = RegexValidator("[1-5][1-9]") reportValidator = RegexValidator("[1-5][1-9]")
class Meta: class Meta:
@ -157,8 +163,9 @@ class QSO(models.Model):
reportTX = models.CharField(max_length=7, default=59, verbose_name='RS-S', validators=[reportValidator]) 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]) reportRX = models.CharField(max_length=7, default=59, verbose_name='RS-R', validators=[reportValidator])
ownNo = models.IntegerField(verbose_name='No') ownNo = models.IntegerField(verbose_name='No', validators=[MinValueValidator(1), MaxValueValidator(MAX_NO_VALUE)])
otherNo = models.IntegerField(verbose_name='No-R', null=True, blank=True) 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") refStr = models.CharField(max_length=20, verbose_name="EXC")
ref = models.ForeignKey(Reference, models.SET_NULL, null=True, blank=True) ref = models.ForeignKey(Reference, models.SET_NULL, null=True, blank=True)

View File

@ -25,4 +25,3 @@ class CallLogValidator(validators.RegexValidator):
'e.g. DL7BST, DN1BER-1, DL/OE1FOO, DN1FTW-1/p' 'e.g. DL7BST, DN1BER-1, DL/OE1FOO, DN1FTW-1/p'
) )
flags = re.ASCII if six.PY3 else 0 flags = re.ASCII if six.PY3 else 0

View File

@ -40,11 +40,13 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'crispy_forms', 'crispy_forms',
'rest_framework',
'django_filters',
# local # local
'contest', 'contest',
'api',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -143,3 +145,6 @@ MESSAGE_TAGS = {
messages.ERROR: 'danger', messages.ERROR: 'danger',
} }
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}

View File

@ -34,6 +34,7 @@ urlpatterns = [
url(r'^logout/$', auth_views.logout, {'next_page': '/'}, name='logout'), url(r'^logout/$', auth_views.logout, {'next_page': '/'}, name='logout'),
url(r'^register/$', register, name='register'), url(r'^register/$', register, name='register'),
url(r'^profile/$', profile, name='profile'), url(r'^profile/$', profile, name='profile'),
url(r'^api/', include('api.urls')),
#url(r'^register/$', CreateView.as_view( #url(r'^register/$', CreateView.as_view(
# template_name='registration/register.html', # template_name='registration/register.html',
# form_class=CustomUserCreationForm, # form_class=CustomUserCreationForm,

View File

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

View File

@ -10,7 +10,10 @@
<div class="panel-body"> <div class="panel-body">
<p> <p>
Please register with your (uppercase) Callsign as Usernames. 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> </p>
<form method="POST" action="{% url 'register' %}"> <form method="POST" action="{% url 'register' %}">
{% csrf_token %} {% csrf_token %}