Display QSOs for admins, match QSOs to each other
This commit is contained in:
parent
94739d3508
commit
6a3c9f0bbe
|
@ -117,7 +117,7 @@ class ShadowCallAddForm(forms.ModelForm):
|
||||||
self.helper.field_template = "bootstrap3/layout/inline_field.html"
|
self.helper.field_template = "bootstrap3/layout/inline_field.html"
|
||||||
self.helper.action = reverse("contest:registerRefs")
|
self.helper.action = reverse("contest:registerRefs")
|
||||||
self.helper.add_input(Submit('submit', 'Add shadow'))
|
self.helper.add_input(Submit('submit', 'Add shadow'))
|
||||||
self.helper.layout = Layout(['username'])
|
self.helper.layout = Layout('username')
|
||||||
|
|
||||||
def clean_username(self):
|
def clean_username(self):
|
||||||
data = self.cleaned_data["username"]
|
data = self.cleaned_data["username"]
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,8 +1,11 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from .validators import CallUsernameValidator
|
from .validators import CallUsernameValidator
|
||||||
|
|
||||||
|
@ -35,6 +38,15 @@ class User(AbstractUser):
|
||||||
super(User, self).__init__(*args, **kwargs)
|
super(User, self).__init__(*args, **kwargs)
|
||||||
self._meta.get_field("username").validators = [CallUsernameValidator()]
|
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):
|
class Band(models.Model):
|
||||||
name = models.CharField(max_length=10)
|
name = models.CharField(max_length=10)
|
||||||
contest = models.ForeignKey(Contest)
|
contest = models.ForeignKey(Contest)
|
||||||
|
@ -65,6 +77,7 @@ class QSO(models.Model):
|
||||||
owner = models.ForeignKey(User, db_index=True)
|
owner = models.ForeignKey(User, db_index=True)
|
||||||
time = models.DateTimeField(blank=True)
|
time = models.DateTimeField(blank=True)
|
||||||
call = models.CharField(max_length=20, db_index=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)
|
band = models.ForeignKey(Band)
|
||||||
|
|
||||||
reportTX = models.CharField(max_length=7, default=59, verbose_name='RST-S', validators=[reportValidator])
|
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")
|
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)
|
||||||
|
|
||||||
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):
|
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)
|
||||||
|
|
|
@ -23,6 +23,8 @@ urlpatterns = [
|
||||||
url(r'^regref/$', contest_views.registerRefs, name='registerRefs'),
|
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/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/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'^overview/$', contest_views.overview, name='overview'),
|
||||||
url(r'^log/$', contest_views.log, name='log'),
|
url(r'^log/$', contest_views.log, name='log'),
|
||||||
url(r'^log/edit/(?P<qsoid>\d+)/$', contest_views.logEdit, name='logEdit'),
|
url(r'^log/edit/(?P<qsoid>\d+)/$', contest_views.logEdit, name='logEdit'),
|
||||||
|
|
|
@ -8,6 +8,8 @@ from django.http import HttpResponseRedirect
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import login as auth_login
|
from django.contrib.auth import login as auth_login
|
||||||
|
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||||
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
@ -108,7 +110,7 @@ def registerRefs(request):
|
||||||
allUser = User.objects.all()
|
allUser = User.objects.all()
|
||||||
shadows = ShadowCall.objects.all()
|
shadows = ShadowCall.objects.all()
|
||||||
|
|
||||||
qsos = QSO.objects.all().order_by("-time")
|
qsos = QSO.objects.all().order_by("-time")[0:10]
|
||||||
|
|
||||||
shadowForm = None
|
shadowForm = None
|
||||||
if request.method == 'POST':
|
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})
|
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
|
@staff_member_required
|
||||||
def updateRef(request, shadow, uid):
|
def updateRef(request, shadow, uid):
|
||||||
user = None
|
user = None
|
||||||
|
@ -155,6 +176,14 @@ def updateRef(request, shadow, uid):
|
||||||
|
|
||||||
return render(request, 'contest/updateRef.html', {'userobj': user, 'form': form, "shadow": shadow})
|
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):
|
def overview(request):
|
||||||
# FIXME: Hardcoded for cqtu... everywhere
|
# FIXME: Hardcoded for cqtu... everywhere
|
||||||
c = Contest.objects.get(id=1)
|
c = Contest.objects.get(id=1)
|
||||||
|
|
|
@ -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">«</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">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
|
@ -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 %}
|
|
@ -16,14 +16,18 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th>Call</th>
|
<th>Call</th>
|
||||||
<th>Ref</th>
|
<th>Ref</th>
|
||||||
|
<th># QSO</th>
|
||||||
|
<th># Cfmd</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for u in alluser %}
|
{% for u in alluser %}
|
||||||
<tr class="{% if not u.ref %}danger{% endif %}">
|
<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.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>
|
<td><a href="{% url "contest:updateRef" u.id %}">Update / Create ref</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -67,7 +71,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<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">
|
<div class="panel-body">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -90,7 +94,7 @@
|
||||||
<td>{{ qso.ownNo }}</td>
|
<td>{{ qso.ownNo }}</td>
|
||||||
<td>{{ qso.band }}</td>
|
<td>{{ qso.band }}</td>
|
||||||
<td>{{ qso.time|date:"H:i" }}</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.call }}</td>
|
||||||
<td>{{ qso.reportTX }}</td>
|
<td>{{ qso.reportTX }}</td>
|
||||||
<td>{{ qso.reportRX }}</td>
|
<td>{{ qso.reportRX }}</td>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
Loading…
Reference in New Issue