CBR parser + uploader
This commit is contained in:
parent
c97cda3c49
commit
8aa7910775
2
TODO
2
TODO
|
@ -1,4 +1,3 @@
|
||||||
- cbr parser zum hochladen
|
|
||||||
- calculate "claimed points"
|
- calculate "claimed points"
|
||||||
- link/funktion um qsos erneut gegeneinander checken zu lassen
|
- link/funktion um qsos erneut gegeneinander checken zu lassen
|
||||||
- listviews mit pagination evaluieren
|
- listviews mit pagination evaluieren
|
||||||
|
@ -25,6 +24,7 @@
|
||||||
- register users still has "reference" labels, make form better
|
- register users still has "reference" labels, make form better
|
||||||
- ensure QSOs are logged in UTC (they currently are, but why?)
|
- ensure QSOs are logged in UTC (they currently are, but why?)
|
||||||
- rearrange login index: contest content rüberziehen; exchange registration nach rechts in der nav
|
- rearrange login index: contest content rüberziehen; exchange registration nach rechts in der nav
|
||||||
|
- cbr parser zum hochladen
|
||||||
|
|
||||||
Glaube nich, dass ich das mache
|
Glaube nich, dass ich das mache
|
||||||
- call dupe validation könnte ins model wandern
|
- call dupe validation könnte ins model wandern
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from .models import Contest, Band
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.contrib import messages
|
||||||
|
from django import forms
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
|
||||||
|
|
||||||
|
from .forms import QSOFormWithTime
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
def parseCBR(raw):
|
||||||
|
""" Parse a CBR file for the CQTU
|
||||||
|
|
||||||
|
Yes, this could be used for other tools, BUT you'd have to take
|
||||||
|
look at the regex and parsingfoo, as there is some cqtu specific foo
|
||||||
|
inside them.
|
||||||
|
"""
|
||||||
|
kvlinere = re.compile(r"^(?P<key>[A-Z-]+):(?: (?P<value>.*))?$")
|
||||||
|
qsore = re.compile(r"^(?P<band>144|432)\s+(?P<mode>[A-Z]{2})\s+(?P<datetime>\d{4}-\d{2}-\d{2} \d{4}) (?P<call_s>[A-Z0-9/-]+)\s+(?P<rst_s>\d{2,3})\s+(?P<exc_s>[A-Z0-9-]+)\s+(?P<call_r>[A-Z0-9/-]+)\s+(?P<rst_r>\d{2,3})\s+(?P<exc_r>[A-Z0-9-]+)\s+0$")
|
||||||
|
|
||||||
|
|
||||||
|
qsoNo = 1
|
||||||
|
info = {
|
||||||
|
"call": None,
|
||||||
|
"location": None,
|
||||||
|
"qsos": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, line in enumerate(raw.split("\n"), 1):
|
||||||
|
line = line.strip()
|
||||||
|
# ignore empty lines
|
||||||
|
if line == "":
|
||||||
|
continue
|
||||||
|
|
||||||
|
m = kvlinere.match(line)
|
||||||
|
if m:
|
||||||
|
k = m.group("key")
|
||||||
|
if k == "CALLSIGN":
|
||||||
|
info["call"] = m.group("value").strip().upper()
|
||||||
|
elif k == "LOCATION":
|
||||||
|
info["location"] = m.group("value").strip().upper()
|
||||||
|
elif k == "QSO":
|
||||||
|
q = m.group("value").strip()
|
||||||
|
# no-s FM date UTC-HHMM call rst-s exch-s call-r rst-r exch-r
|
||||||
|
# no-s / date UTC-HH-MM call rst-s
|
||||||
|
qm = qsore.search(q)
|
||||||
|
if qm:
|
||||||
|
qsoData = qm.groupdict()
|
||||||
|
qsoData["band"] = "2m" if qsoData["band"] == 144 else "70cm"
|
||||||
|
qsoTime = timezone.datetime.strptime(qsoData["datetime"], "%Y-%m-%d %H%M")
|
||||||
|
qsoData["datetime"] = timezone.get_current_timezone().localize(qsoTime)
|
||||||
|
qsoData["no_s"] = qsoNo
|
||||||
|
info["qsos"].append(qsoData)
|
||||||
|
|
||||||
|
if info["call"] != qsoData["call_s"]:
|
||||||
|
raise forms.ValidationError("Error in line %d: qso was not made by you? (callsigns do not match)" % n)
|
||||||
|
if info["location"] != qsoData["exc_s"]:
|
||||||
|
raise forms.ValidationError("Error in line %d: exchange does not match your location? (callsigns do not match)" % n)
|
||||||
|
else:
|
||||||
|
raise forms.ValidationError("Error in line %d: qso was broken, regex did not match" % n)
|
||||||
|
|
||||||
|
qsoNo += 1
|
||||||
|
elif k == "X-QSO":
|
||||||
|
qsoNo += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise forms.ValidationError("Error in line %d: could not parse \"KEY: value\" pair" % n)
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
class CBRForm(forms.Form):
|
||||||
|
data = forms.CharField(widget=forms.Textarea, label="Cabrillo data", help_text="Paste your cabrillo file contents here")
|
||||||
|
|
||||||
|
def clean_data(self):
|
||||||
|
rawData = self.cleaned_data["data"]
|
||||||
|
parsedData = parseCBR(rawData)
|
||||||
|
|
||||||
|
return parsedData
|
||||||
|
|
||||||
|
def checkCBRConsistency(contest, user, info):
|
||||||
|
errors = []
|
||||||
|
qsos = []
|
||||||
|
if user.username != info["call"]:
|
||||||
|
errors.append("You are not the owner of this logfile! (%s != %s)" % (user.username, info["call"]))
|
||||||
|
|
||||||
|
if user.ref.name != info["location"]:
|
||||||
|
errors.append("Location of logfile and registered exchange do not match! (%s != %s)" % (user.ref.name, info["location"]))
|
||||||
|
|
||||||
|
for n, qsoData in enumerate(info["qsos"], 1):
|
||||||
|
qsoFormData = {
|
||||||
|
#"owner": user,
|
||||||
|
"time": qsoData["datetime"],
|
||||||
|
"call": qsoData["call_r"],
|
||||||
|
"band": Band.objects.get(contest=contest, name=qsoData["band"]).id,
|
||||||
|
"reportTX": qsoData["rst_s"],
|
||||||
|
"reportRX": qsoData["rst_r"],
|
||||||
|
"ownNo": qsoData["no_s"],
|
||||||
|
"otherNo": None,
|
||||||
|
"refStr": qsoData["exc_r"],
|
||||||
|
"remarks": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
qsoForm = QSOFormWithTime(user, data=qsoFormData)
|
||||||
|
qsoForm.is_valid()
|
||||||
|
qsoForm.instance.owner = user
|
||||||
|
print(qsoForm.errors)
|
||||||
|
qsos.append((qsoForm.instance, qsoForm))
|
||||||
|
|
||||||
|
return qsos, errors
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def uploadCBR(request):
|
||||||
|
if not request.user.ref:
|
||||||
|
return HttpResponseRedirect(reverse("contest:index"))
|
||||||
|
|
||||||
|
contest = Contest.objects.get(id=1)
|
||||||
|
deadline = False
|
||||||
|
form = None
|
||||||
|
verifyData = []
|
||||||
|
verifyErrors = []
|
||||||
|
save = saved = False
|
||||||
|
|
||||||
|
if timezone.now() < contest.deadline:
|
||||||
|
if request.method == "POST":
|
||||||
|
form = CBRForm(data=request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
verifyData, verifyErrors = checkCBRConsistency(contest, request.user, form.cleaned_data["data"])
|
||||||
|
|
||||||
|
if request.POST.get("action") == "save":
|
||||||
|
save = True
|
||||||
|
if not verifyErrors:
|
||||||
|
cnt = 0
|
||||||
|
for qso, qsoForm in verifyData:
|
||||||
|
if qsoForm.is_valid():
|
||||||
|
qso.save()
|
||||||
|
cnt += 1
|
||||||
|
|
||||||
|
if cnt > 0:
|
||||||
|
messages.success(request, "%d QSOs have been saved from the cbr file" % cnt)
|
||||||
|
else:
|
||||||
|
messages.warnnig(request, "CBR file was parsed, but no QSOs could be saved, as all cointained errors.")
|
||||||
|
|
||||||
|
return HttpResponseRedirect(reverse("contest:uploadCBR"))
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
form = CBRForm()
|
||||||
|
else:
|
||||||
|
deadline = True
|
||||||
|
|
||||||
|
return render(request, "contest/uploadCBR.html", {"deadline": deadline, 'form': form, 'verifyData': verifyData, 'verifyErrors': verifyErrors, 'save': save, 'saved': saved})
|
|
@ -17,6 +17,7 @@ from django.conf.urls import url
|
||||||
|
|
||||||
|
|
||||||
import contest.views as contest_views
|
import contest.views as contest_views
|
||||||
|
from contest.cbrparser import uploadCBR
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', contest_views.contestIndex, name='index'),
|
url(r'^$', contest_views.contestIndex, name='index'),
|
||||||
|
@ -29,4 +30,5 @@ urlpatterns = [
|
||||||
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'),
|
||||||
url(r'^log/delete/(?P<qsoid>\d+)/$', contest_views.logDelete, name='logDelete'),
|
url(r'^log/delete/(?P<qsoid>\d+)/$', contest_views.logDelete, name='logDelete'),
|
||||||
|
url(r'^uploadcbr/$', uploadCBR, name='uploadCBR'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
<li{% if request.resolver_match.url_name == 'index' %} class="active"{%endif%}><a href="{% url "contest:index" %}">Contest Overview</a></li>
|
<li{% if request.resolver_match.url_name == 'index' %} class="active"{%endif%}><a href="{% url "contest:index" %}">Contest Overview</a></li>
|
||||||
{% if user.ref %}
|
{% if user.ref %}
|
||||||
<li{% if "log" in request.resolver_match.url_name %} class="active"{%endif%}><a href="{% url "contest:log" %}">Log</a></li>
|
<li{% if "log" in request.resolver_match.url_name %} class="active"{%endif%}><a href="{% url "contest:log" %}">Log</a></li>
|
||||||
|
<li{% if request.resolver_match.url_name == "uploadCBR" %} class="active"{%endif%}><a href="{% url "contest:uploadCBR" %}">Upload CBR</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<li{% if request.resolver_match.url_name == 'index' %} class="active"{%endif%}><a href="{% url "index" %}">Home</a></li>
|
<li{% if request.resolver_match.url_name == 'index' %} class="active"{%endif%}><a href="{% url "index" %}">Home</a></li>
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if not deadline %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">Upload cabrillo data</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{# {% crispy form %} #}
|
||||||
|
<form method="post" action="{% url 'contest:uploadCBR' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form | crispy }}
|
||||||
|
<div class="form-action">
|
||||||
|
<button class="btn btn-danger" type="submit" name="action" value="save">Save</button>
|
||||||
|
<button class="btn btn-success" type="submit" name="action" value="verify">Verify</button>
|
||||||
|
<p>
|
||||||
|
<strong>Note:</strong> On clicking save your QSOs will be saved to the database and you will have
|
||||||
|
to use the log form to verify and edit your data!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Note:</strong> QSOs marked red in the verify phase will not be saved.
|
||||||
|
</p>
|
||||||
|
{% if save and not saved %}
|
||||||
|
<p class="danger">
|
||||||
|
Could not save, as your data still contains errors!
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% if verifyErrors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<ul>
|
||||||
|
{% for error in verifyErrors %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>QSO</th>
|
||||||
|
<th>Error</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for qso, qsoForm in verifyData %}
|
||||||
|
<tr class="{% if qsoForm.errors %}danger{% else %}success{% endif %}">
|
||||||
|
<td>{{ qso }}</td>
|
||||||
|
<td>{{ qsoForm.errors }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6 col-sm-offset-3">
|
||||||
|
<div class="panel panel-danger">
|
||||||
|
<div class="panel-heading">Upload closed</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>
|
||||||
|
The deadline for this contest has passed.
|
||||||
|
Uploading cabrillo files is not possible anymore.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
Loading…
Reference in New Issue