Display QSOs for admins, match QSOs to each other

This commit is contained in:
Sebastian Lohff 2017-01-26 19:14:58 +01:00
parent 94739d3508
commit 6a3c9f0bbe
10 changed files with 253 additions and 7 deletions

View File

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

View File

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

View File

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

View File

@ -23,6 +23,8 @@ urlpatterns = [
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'),

View File

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

View File

@ -0,0 +1,17 @@
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="{% if not page.has_previous %}disabled{% endif %}">
<a{% if page.has_previous %} href="?page={{ page.previous_page_number }}"{% endif%} aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% for pageNo in pages.page_range %}
<li{% if pageNo == page.number %} class="active"{% endif %}><a href="?page={{ pageNo }}" pageNo %}">{{ pageNo }}</a></li>
{% endfor %}
<li class="{% if not page.has_next %}disabled{% endif %}">
<a{% if page.has_next %} href="?page={{ page.next_page_number }}"{% endif %} aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>

View File

@ -0,0 +1,39 @@
{% include "contest/paginationNav.html" with page=qsoPage pages=qsoPager %}
<table class="table">
<thead>
<tr>
<th>Nr-S</th>
<th>Nr-R</th>
<th>Band</th>
<th>UTC</th>
<th>Call A</th>
<th>Call B</th>
<th class="hidden-xs">RS-S</th>
<th class="hidden-xs">RS-R</th>
<th class="hidden-xs">Nr-R</th>
<th>EXC</th>
<th class="hidden-xs">Remarks</th>
<th class="">Cfmd</th>
</tr>
</thead>
<tbody>
{% for qso in qsoPage %}
<tr id="qso-row-{{ forloop.counter0 }}" class="{% if qso.cfmdQSO %}success{% endif %}">
<td>{{ qso.ownNo }}</td>
<td>{{ qso.otherNo }}</td>
<td>{{ qso.band }}</td>
<td>{{ qso.time|date:"H:i" }}</td>
<td><a href="{% url "contest:viewUserQSOs" qso.owner.id %}">{{ qso.owner }}</a></td>
<td>{% if qso.callRef %}<a href="{% url "contest:viewUserQSOs" qso.callRef.id %}">{% endif %}{{ qso.call }}{% if qso.callRef %}</a> <span class="glyphicon glyphicon-ok-sign text-success"></span>{% endif %}</td>
<td class="hidden-xs">{{ qso.reportTX }}</td>
<td class="hidden-xs">{{ qso.reportRX }}</td>
<td class="hidden-xs">{{ qso.otherNo }}</td>
<td>{{ qso.refStr }}{% if qso.ref %} <span class="glyphicon glyphicon-ok-sign text-success"></span>{% endif %}</td>
<td class="hidden-xs">{{ qso.remarks }}</td>
<!-- <td><a href="{% url "contest:logEdit" qso.id %}"><span class="glyphicon glyphicon-pencil"></span></a> <a href="{% url "contest:logDelete" qso.id %}"><span class="glyphicon glyphicon-trash"></span></a></td> -->
<td>{{ qso.cfmdQSO|default:"" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include "contest/paginationNav.html" with page=qsoPage pages=qsoPager %}

View File

@ -16,14 +16,18 @@
<tr>
<th>Call</th>
<th>Ref</th>
<th># QSO</th>
<th># Cfmd</th>
<th></th>
</tr>
</thead>
<tbody>
{% for u in alluser %}
<tr class="{% if not u.ref %}danger{% endif %}">
<td>{{ u.username }}</td>
<td><a href="{% url "contest:viewUserQSOs" u.id %}">{{ u.username }}</a></td>
<td>{{ u.ref|default:"unknown / unset" }}</td>
<td>{{ u.getQSOCount}}</td>
<td>{{ u.getCfmdQSOCount}}</td>
<td><a href="{% url "contest:updateRef" u.id %}">Update / Create ref</a></td>
</tr>
{% endfor %}
@ -67,7 +71,7 @@
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">Current QSOs</div>
<div class="panel-heading">Last {{ qsos.count }} QSOs <small><a href="{% url "contest:viewAllQSOs" %}">Show all</a></div>
<div class="panel-body">
<table class="table">
<thead>
@ -90,7 +94,7 @@
<td>{{ qso.ownNo }}</td>
<td>{{ qso.band }}</td>
<td>{{ qso.time|date:"H:i" }}</td>
<td>{{ qso.owner.username }}</td>
<td><a href="{% url "contest:viewUserQSOs" qso.owner.id %}">{{ qso.owner.username }}</td>
<td>{{ qso.call }}</td>
<td>{{ qso.reportTX }}</td>
<td>{{ qso.reportRX }}</td>

View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">All QSOs</div>
<div class="panel-body">
{% include "contest/qsoAdminTable.html" with qsoPage=qsoPage qsoPager=qsoPager %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">QSOs by {{ owner }}</div>
<div class="panel-body">
<p>
{{ 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:", " }}.
</p>
{% include "contest/qsoAdminTable.html" with qsoPage=qsoPage qsoPager=qsoPager %}
</div>
</div>
</div>
</div>
{% endblock %}