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" | ||||
|  - link/funktion um qsos erneut gegeneinander checken zu lassen | ||||
|  - listviews mit pagination evaluieren | ||||
|  | @ -25,6 +24,7 @@ | |||
|  - register users still has "reference" labels, make form better | ||||
|  - 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 | ||||
|  - cbr parser zum hochladen | ||||
| 
 | ||||
| Glaube nich, dass ich das mache | ||||
|  - 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 | ||||
| from contest.cbrparser import uploadCBR | ||||
| 
 | ||||
| urlpatterns = [ | ||||
| 	url(r'^$', contest_views.contestIndex, name='index'), | ||||
|  | @ -29,4 +30,5 @@ urlpatterns = [ | |||
| 	url(r'^log/$', contest_views.log, name='log'), | ||||
| 	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'^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> | ||||
| {% 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 request.resolver_match.url_name == "uploadCBR" %} class="active"{%endif%}><a href="{% url "contest:uploadCBR" %}">Upload CBR</a></li> | ||||
| {% endif %} | ||||
| {% else %} | ||||
|             <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