273 lines
10 KiB
Python
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)
|