cqtu/contest/models.py

273 lines
10 KiB
Python

import datetime
from django.contrib.auth.models import AbstractUser
from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator
from django.db import models
from django.db.models import Q, signals
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)
contestNo = models.IntegerField(validators=[MinValueValidator(1)],
help_text="Running number of contest (for vanity reasons)")
rulesetLink = models.TextField(help_text="URL to the ruleset pdf for this contest")
callQrg = models.ForeignKey("Frequency", on_delete=models.SET_NULL, null=True, blank=True)
deadline = models.DateTimeField()
qsoStartTime = models.DateTimeField()
qsoEndTime = models.DateTimeField()
def __str__(self):
return self.name
@classmethod
def get_current_contest(cls):
# Currently the contest with the latest deadline is the active one
# This definitely has potential for improvement, but it's better than a hardcoded contest
contests = cls.objects.order_by("-deadline")
if len(contests) > 0:
return contests[0]
return None
class Reference(models.Model):
name = models.CharField(max_length=20, unique=True, db_index=True)
description = models.TextField()
def __str__(self):
return self.name
class EntryCategory(models.Model):
name = models.CharField(max_length=64, unique=True)
description = models.TextField(blank=True)
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)
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)
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, 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
ignoreBand = models.BooleanField(default=False)
# extra profile stuff so DL7BST can sleep well without his doodles
editedProfile = models.BooleanField(default=False)
dncall = models.CharField(max_length=16, default='', blank=True,
verbose_name="DN-Call",
help_text="If you have a DN call that you will offer to SWLs please enter it here")
qrv2m = models.BooleanField(default=False,
verbose_name="QRV on 2m",
help_text="Will you be QRV on 2m during the contest?")
qrv70cm = models.BooleanField(default=False,
verbose_name="QRV on 70cm",
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?")
def __init__(self, *args, **kwargs):
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"))))
def calcClaimedPoints(self):
return self.calcPoints(cfmd=False)
def calcCfmdPoints(self):
return self.calcPoints(cfmd=True)
def calcPoints(self, cfmd):
contest = Contest.objects.get(id=1)
result = {"refCount": 0, "qsoCount": 0}
for band in contest.band_set.all():
result[band.name] = self.calcBandPoints(band, cfmd)
result["refCount"] += result[band.name]["refCount"]
result["qsoCount"] += result[band.name]["qsoCount"]
result["points"] = result["qsoCount"] * result["refCount"]
return result
def calcBandPoints(self, band, cfmd=False):
contest = band.contest
qsos = self.qso_set.filter(band=band, time__gte=contest.qsoStartTime, time__lte=contest.qsoEndTime)
if cfmd:
qsos = qsos.filter(cfmdQSO__isnull=False)
refs = set(map(lambda _x: _x["refStr"], qsos.values("refStr")))
return {
"band": band,
"refs": refs,
"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, 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, 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:
index_together = [
["owner", "call"],
]
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, 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', 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, on_delete=models.SET_NULL, null=True, blank=True)
remarks = models.CharField(max_length=50, blank=True, default=None)
cfmdQSO = models.ForeignKey("QSO", on_delete=models.SET_NULL, null=True, blank=True, default=None)
CFMD_SEC = 5 * 60
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:
refName = self.refStr.replace("-", "")
if refName == "GX":
refName = "DX"
# Old reference exists?
if self.ref and self.ref.name != refName:
self.ref = None
changed = True
if not self.ref:
# find matching ref
try:
self.ref = Reference.objects.get(name=refName)
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 abs((self.time - q.time).total_seconds()) <= self.CFMD_SEC and \
self.ref and self.owner.ref and self.callRef and q.callRef and \
q.owner == self.callRef and q.callRef == self.owner 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.save(checkQSO=False)
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__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,
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()
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)