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)