diff --git a/contest/forms.py b/contest/forms.py index 5f3c1f7..eeb45a7 100644 --- a/contest/forms.py +++ b/contest/forms.py @@ -117,7 +117,7 @@ class ShadowCallAddForm(forms.ModelForm): self.helper.field_template = "bootstrap3/layout/inline_field.html" self.helper.action = reverse("contest:registerRefs") self.helper.add_input(Submit('submit', 'Add shadow')) - self.helper.layout = Layout(['username']) + self.helper.layout = Layout('username') def clean_username(self): data = self.cleaned_data["username"] diff --git a/contest/migrations/0011_auto_20170125_0126.py b/contest/migrations/0011_auto_20170125_0126.py new file mode 100644 index 0000000..94ee33f --- /dev/null +++ b/contest/migrations/0011_auto_20170125_0126.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-01-25 01:26 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0010_shadowcall'), + ] + + operations = [ + migrations.AddField( + model_name='qso', + name='callRef', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='qsoref', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='qso', + name='cfmdQSO', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contest.QSO'), + ), + migrations.AlterField( + model_name='qso', + name='remarks', + field=models.CharField(blank=True, default=None, max_length=50), + ), + ] diff --git a/contest/models.py b/contest/models.py index 0036988..da663f7 100644 --- a/contest/models.py +++ b/contest/models.py @@ -1,8 +1,11 @@ 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.db.models import Q from .validators import CallUsernameValidator @@ -35,6 +38,15 @@ class User(AbstractUser): super(User, self).__init__(*args, **kwargs) self._meta.get_field("username").validators = [CallUsernameValidator()] + def getQSOCount(self): + return self.qso_set.count() + + def getCfmdQSOCount(self): + return self.qso_set.filter(~Q(cfmdQSO=None)).count() + + def getCfmdRefCount(self): + return len(set(map(lambda _x: _x["refStr"], self.qso_set.filter(ref__isnull=False).values("ref", "refStr")))) + class Band(models.Model): name = models.CharField(max_length=10) contest = models.ForeignKey(Contest) @@ -65,6 +77,7 @@ class QSO(models.Model): owner = models.ForeignKey(User, 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) reportTX = models.CharField(max_length=7, default=59, verbose_name='RST-S', validators=[reportValidator]) @@ -76,7 +89,78 @@ class QSO(models.Model): refStr = models.CharField(max_length=20, verbose_name="EXC") ref = models.ForeignKey(Reference, models.SET_NULL, null=True, blank=True) - remarks = models.CharField(max_length=50, 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) + + CFMD_SEC = 300 + + def checkQSOData(self): + """ Match strdata to log rows. Only call, if you intent to save this object if we return True! """ + # find reference + changed = False + if self.refStr: + # Old reference exists? + if self.ref and self.ref.name != self.refStr: + self.ref = None + changed = True + + if not self.ref: + # find matching ref + try: + self.ref = Reference.objects.get(name=self.refStr) + changed = True + except Reference.DoesNotExist: + pass + + # find call + if not self.callRef or self.callRef.username != self.call: + try: + self.callRef = User.objects.get(username=self.call) + changed = True + except User.DoesNotExist: + if self.callRef: + changed = True + self.callRef = None + + # find matching qso + if self.cfmdQSO: + # check if this still checks out + q = self.cfmdQSO + if (self.time - q.time).total_seconds() <= self.CFMD_SEC and \ + self.ref and self.owner.ref and \ + self.ref == q.owner.ref and self.owner.ref == q.ref and \ + self.band == q.band: + # checks out + pass + else: + changed = True + self.cfmdQSO.cfmdQSO = None + self.cfmdQSO = None + + self.cfmdQSO = None + 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__gte=self.time + datetime.timedelta(0, self.CFMD_SEC)) | Q(time__gte=self.time - datetime.timedelta(0, self.CFMD_SEC))), + owner__ref=self.ref, + ref=self.owner.ref, + band=self.band) + + if q.count() == 1: + changed = True + q[0].cfmdQSO = self + q[0].save(checkQSO=False) + self.cfmdQSO = q[0] + + return changed + + def save(self, checkQSO=True, *args, **kwargs): + if checkQSO: + self.checkQSOData() + + print(" ==> ", self, " ==> ", self.cfmdQSO) + super(QSO, self).save(*args, **kwargs) def __str__(self): - return "QSO no %s at %s with %s@%s %s/%s" % (self.ownNo, self.time.strftime("%H:%M"), self.call, self.refStr, self.reportTX, self.reportRX) + return "QSO no %s at from %s %s with %s@%s %s/%s" % (self.ownNo, self.time.strftime("%H:%M"), self.owner.username, self.call, self.refStr, self.reportTX, self.reportRX) diff --git a/contest/urls.py b/contest/urls.py index 48c1103..84f6c87 100644 --- a/contest/urls.py +++ b/contest/urls.py @@ -23,6 +23,8 @@ urlpatterns = [ 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'), diff --git a/contest/views.py b/contest/views.py index ae4aae0..b4483ef 100644 --- a/contest/views.py +++ b/contest/views.py @@ -8,6 +8,8 @@ from django.http import HttpResponseRedirect from django.contrib import messages from django.urls import reverse from django.contrib.auth import login as auth_login +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger + import datetime @@ -108,7 +110,7 @@ def registerRefs(request): allUser = User.objects.all() shadows = ShadowCall.objects.all() - qsos = QSO.objects.all().order_by("-time") + qsos = QSO.objects.all().order_by("-time")[0:10] shadowForm = None if request.method == 'POST': @@ -122,6 +124,25 @@ def registerRefs(request): return render(request, 'contest/registerRefs.html', {'alluser': allUser, "qsos": qsos, "shadowForm": shadowForm, "shadows": shadows}) +def getPage(paginator, pageNo): + try: + return paginator.page(pageNo) + except PageNotAnInteger: + return paginator.page(1) + except EmptyPage: + return paginator.page(paginator.num_pages) + +@staff_member_required +def viewUserQSOs(request, uid, page=1): + user = get_object_or_404(User, id=uid) + qsos = QSO.objects.filter(owner=user).order_by("-time") + qsoPager = Paginator(qsos, 50) + qsoPage = getPage(qsoPager, request.GET.get('page')) + + 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, 'qsoPager': qsoPager, 'qsoPage': qsoPage, 'userRefs': userRefs}) + @staff_member_required def updateRef(request, shadow, uid): user = None @@ -155,6 +176,14 @@ 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") + qsoPager = Paginator(qsos, 10) + qsoPage = getPage(qsoPager, request.GET.get('page')) + + return render(request, 'contest/viewAllQSOs.html', {'qsoPager': qsoPager, 'qsoPage': qsoPage}) + def overview(request): # FIXME: Hardcoded for cqtu... everywhere c = Contest.objects.get(id=1) diff --git a/templates/contest/paginationNav.html b/templates/contest/paginationNav.html new file mode 100644 index 0000000..288d7fc --- /dev/null +++ b/templates/contest/paginationNav.html @@ -0,0 +1,17 @@ + diff --git a/templates/contest/qsoAdminTable.html b/templates/contest/qsoAdminTable.html new file mode 100644 index 0000000..fa9d83a --- /dev/null +++ b/templates/contest/qsoAdminTable.html @@ -0,0 +1,39 @@ +{% include "contest/paginationNav.html" with page=qsoPage pages=qsoPager %} + + + + + + + + + + + + + + + + + + +{% for qso in qsoPage %} + + + + + + + + + + + + + + + +{% endfor %} + +
Nr-SNr-RBandUTCCall ACall BEXCCfmd
{{ qso.ownNo }}{{ qso.otherNo }}{{ qso.band }}{{ qso.time|date:"H:i" }}{{ qso.owner }}{% if qso.callRef %}{% endif %}{{ qso.call }}{% if qso.callRef %} {% endif %}{{ qso.refStr }}{% if qso.ref %} {% endif %}{{ qso.cfmdQSO|default:"" }}
+{% include "contest/paginationNav.html" with page=qsoPage pages=qsoPager %} diff --git a/templates/contest/registerRefs.html b/templates/contest/registerRefs.html index 8c92236..a9271b3 100644 --- a/templates/contest/registerRefs.html +++ b/templates/contest/registerRefs.html @@ -16,14 +16,18 @@ Call Ref + # QSO + # Cfmd {% for u in alluser %} - {{ u.username }} + {{ u.username }} {{ u.ref|default:"unknown / unset" }} + {{ u.getQSOCount}} + {{ u.getCfmdQSOCount}} Update / Create ref {% endfor %} @@ -67,7 +71,7 @@
-
Current QSOs
+
Last {{ qsos.count }} QSOs Show all
@@ -90,7 +94,7 @@ - + diff --git a/templates/contest/viewAllQSOs.html b/templates/contest/viewAllQSOs.html new file mode 100644 index 0000000..fe24536 --- /dev/null +++ b/templates/contest/viewAllQSOs.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% load crispy_forms_tags %} + +{% block content %} +
+
+
+
All QSOs
+
+ {% include "contest/qsoAdminTable.html" with qsoPage=qsoPage qsoPager=qsoPager %} +
+
+
+
+ +{% endblock %} + diff --git a/templates/contest/viewUserQSOs.html b/templates/contest/viewUserQSOs.html new file mode 100644 index 0000000..80a873b --- /dev/null +++ b/templates/contest/viewUserQSOs.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% load crispy_forms_tags %} + +{% block content %} +
+
+
+
QSOs by {{ owner }}
+
+

+ {{ owner }} has {{ owner.getQSOCount }} QSO{{ owner.getQSOCount|pluralize }}, {{ owner.getCfmdQSOCount }} confirmed QSO{{ owner.getCfmdQSOCount|pluralize }} and worked the following {{ userRefs|length }} exchange{{ userRefs|length|pluralize }}: {{ userRefs|join:", " }}. +

+ {% include "contest/qsoAdminTable.html" with qsoPage=qsoPage qsoPager=qsoPager %} +
+
+
+
+ +{% endblock %} +
{{ qso.ownNo }} {{ qso.band }} {{ qso.time|date:"H:i" }}{{ qso.owner.username }}{{ qso.owner.username }} {{ qso.call }} {{ qso.reportTX }} {{ qso.reportRX }}