From 7345e3a3286ebf2771a7c7800c83632cc9930fa1 Mon Sep 17 00:00:00 2001 From: Philip Sargent Date: Fri, 17 Mar 2023 20:01:52 +0000 Subject: [PATCH] Much QM re-engineering --- core/admin.py | 22 ++--- core/models/caves.py | 18 +++- core/models/logbooks.py | 35 ++------ core/views/caves.py | 14 +-- md5bash.sh | 79 ++++++++++++++++ parsers/QMs.py | 53 ++++++----- parsers/survex.py | 9 +- templates/cave_qms.html | 43 ++++----- templates/core/QMs_json_list.html | 12 --- templates/qm.html | 145 +++++++++++++++--------------- 10 files changed, 252 insertions(+), 178 deletions(-) create mode 100644 md5bash.sh diff --git a/core/admin.py b/core/admin.py index bfbd727..b7c0d2b 100644 --- a/core/admin.py +++ b/core/admin.py @@ -47,11 +47,11 @@ class SurvexBlockAdmin(TroggleModelAdmin): inlines = (RoleInline,) -class QMsFoundInline(admin.TabularInline): - model = QM - fk_name = "found_by" - fields = ("number", "grade", "location_description", "comment") # need to add foreignkey to cave part - extra = 1 +# class QMsFoundInline(admin.TabularInline): + # model = QM + # fk_name = "found_by" + # fields = ("number", "grade", "location_description", "comment") # need to add foreignkey to cave part + # extra = 1 class PersonLogEntryInline(admin.TabularInline): @@ -64,7 +64,7 @@ class LogbookEntryAdmin(TroggleModelAdmin): prepopulated_fields = {"slug": ("title",)} search_fields = ("title", "expedition__year") date_heirarchy = "date" - inlines = (PersonLogEntryInline, QMsFoundInline) + # inlines = (PersonLogEntryInline, QMsFoundInline) class Media: css = {"all": ("css/troggleadmin.css",)} # this does not exist @@ -91,12 +91,12 @@ class PersonAdmin(TroggleModelAdmin): class QMAdmin(TroggleModelAdmin): - search_fields = ("found_by__cave__kataster_number", "number", "found_by__date") - list_display = ("__str__", "grade", "found_by", "ticked_off_by") + search_fields = ("number", "expoyear") + list_display = ("__str__", "grade") list_display_links = ("__str__",) - list_editable = ("found_by", "ticked_off_by", "grade") - list_per_page = 20 - raw_id_fields = ("found_by", "ticked_off_by") + # list_editable = ("comment", "page_ref", "grade") + # list_per_page = 20 + # raw_id_fields = ("found_by", "ticked_off_by") class PersonExpeditionAdmin(TroggleModelAdmin): diff --git a/core/models/caves.py b/core/models/caves.py index 6492698..8b23ceb 100644 --- a/core/models/caves.py +++ b/core/models/caves.py @@ -1,4 +1,5 @@ import os +import os import re from collections import defaultdict from datetime import datetime, timezone @@ -161,14 +162,27 @@ class Cave(TroggleModel): def __str__(self, sep=": "): return str(self.slug()) - def get_QMs(self): + def get_open_QMs(self): """Searches for all QMs that reference this cave.""" # qms = self.qm_set.all().order_by('expoyear', 'block__date') qms = QM.objects.filter(cave=self).order_by( "expoyear", "block__date" ) # a QuerySet, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#order-by - return qms # a QuerySet + qmsopen = qms.filter(ticked=False) + return qmsopen # a QuerySet + def get_ticked_QMs(self): + """Searches for all QMs that reference this cave.""" + qms = QM.objects.filter(cave=self).order_by( + "expoyear", "block__date" + ) + qmticked = qms.filter(ticked=True) + return qmticked # a QuerySet + + def get_QMs(self): + qms = self.get_open_QMs() | self.get_ticked_QMs() # set union operation + return qms # a QuerySet + def kat_area(self): for a in self.area.all(): if a.kat_area(): diff --git a/core/models/logbooks.py b/core/models/logbooks.py index 226f8e5..b594945 100644 --- a/core/models/logbooks.py +++ b/core/models/logbooks.py @@ -144,38 +144,19 @@ class QM(TroggleModel): number = models.IntegerField( help_text="this is the sequential number in the year, only unique for CSV imports", ) + grade = models.CharField(max_length=1, blank=True, null=True, help_text="A/B/C/D/X") cave = models.ForeignKey("Cave", related_name="QMs", blank=True, null=True, on_delete=models.SET_NULL) block = models.ForeignKey("SurvexBlock", null=True, on_delete=models.SET_NULL) # only for QMs from survex files blockname = models.TextField(blank=True, null=True) # NB truncated copy of survexblock name with last char added - expoyear = models.CharField( - max_length=4, blank=True, null=True - ) # could change to datetime if logbooks similarly chnaged - found_by = models.ForeignKey( - LogbookEntry, related_name="QMs_found", blank=True, null=True, on_delete=models.SET_NULL - ) - ticked = models.BooleanField( - default=False - ) # for ticked QMs not attached to a logbook entry, should imply completion_description has text - ticked_off_by = models.ForeignKey( - LogbookEntry, related_name="QMs_ticked_off", blank=True, null=True, on_delete=models.SET_NULL - ) # unused, ever?! - - GRADE_CHOICES = ( - ("A", "A: Large obvious lead"), - ("B", "B: Average lead"), - ("C", "C: Tight unpromising lead"), - ("D", "D: Dig"), - ("X", "X: Unclimbable or horrid"), - ("V", "V: Vertical"), - ) # also seen "?" in imported data - see urls.py - grade = models.CharField(max_length=1, choices=GRADE_CHOICES) + expoyear = models.CharField(max_length=4, blank=True, null=True) + ticked = models.BooleanField(default=False) location_description = models.TextField(blank=True) - nearest_station_description = models.CharField(max_length=400, blank=True, null=True) + completion_description = models.TextField(blank=True) + completion_date = models.DateField(blank=True, null=True) nearest_station_name = models.CharField(max_length=200, blank=True, null=True) resolution_station_name = models.CharField(max_length=200, blank=True, null=True) - nearest_station = models.ForeignKey("SurvexStation", blank=True, null=True, on_delete=models.SET_NULL) area = models.CharField(max_length=100, blank=True, null=True) - completion_description = models.TextField(blank=True, null=True) + page_ref = models.TextField(blank=True, null=True) comment = models.TextField(blank=True, null=True) def __str__(self): @@ -228,8 +209,8 @@ class QM(TroggleModel): ), ) - def get_next_by_id(self): + def get_next_by_id(self): # called in template return QM.objects.get(id=self.id + 1) - def get_previous_by_id(self): + def get_previous_by_id(self): # called in template return QM.objects.get(id=self.id - 1) diff --git a/core/views/caves.py b/core/views/caves.py index fa5e7ef..c866718 100644 --- a/core/views/caves.py +++ b/core/views/caves.py @@ -536,9 +536,11 @@ def caveQMs(request, slug): def qm(request, cave_id, qm_id, year, grade=None, blockname=None): """Reports on one specific QM Fixed and working July 2022, for both CSV imported QMs - needs refactoring though. + + Needs refactoring though! Uses extremely baroque way of getting the QMs instead of querying for QM objects + directly, presumably as a result of a baroque history. - 290 has several QMS with the same number, grade, year (2108) and first 8 chars of the survexblock. This crashes things. + Many caves have several QMS with the same number, grade, year (2018) and first 8 chars of the survexblock. This crashes things, so the terminal char of the survexblock name was added """ year = int(year) @@ -547,7 +549,7 @@ def qm(request, cave_id, qm_id, year, grade=None, blockname=None): # CSV import QMs, use old technique try: c = getCave(cave_id) - manyqms = c.get_QMs() + manyqms = c.get_open_QMs() | c.get_ticked_QMs() # set union operation qm = manyqms.get(number=qm_id, expoyear=year) return render(request, "qm.html", {"qm": qm}) except QM.DoesNotExist: @@ -565,12 +567,12 @@ def qm(request, cave_id, qm_id, year, grade=None, blockname=None): qmslug = f"{cave_id}-{year}-{blockname=}{qm_id}{grade}" print(f"{qmslug=}") c = getCave(cave_id) - manyqms = c.get_QMs() + manyqms = c.get_open_QMs() | c.get_ticked_QMs() # set union operation qmqs = manyqms.filter(expoyear=year, blockname=blockname, number=qm_id, grade=grade) if len(qmqs) > 1: for q in qmqs: print(qmqs) - message = f"Multiple QMs with the same cave, year, number, grade AND first 8 chars of the survexblock name. (Could be caused by incomplete databasereset). Fix this in the survex file(s). {cave_id=} {year=} {qm_id=} {blockname=}" + message = f"Multiple QMs with the same cave, year, number, grade AND first-several+terminal chars of the survexblock name. (Could be caused by incomplete databasereset). Fix this in the survex file(s). {cave_id=} {year=} {qm_id=} {blockname=}" return render(request, "errors/generic.html", {"message": message}) else: qm = qmqs.get(expoyear=year, blockname=blockname, number=qm_id, grade=grade) @@ -588,7 +590,7 @@ def qm(request, cave_id, qm_id, year, grade=None, blockname=None): {"badslug": f"Failed get {cave_id=} {year=} {qm_id=} {grade=} {blockname=}"}, ) except MultipleObjectsReturned: - message = f"Multiple QMs with the same cave, year, number, grade AND first 8 chars of the survexblock name. (Could be caused by incomplete databasereset). Fix this in the survex file(s). {cave_id=} {year=} {qm_id=} {blockname=}" + message = f"Multiple QMs with the same cave, year, number, grade AND first-several+terminal chars of the survexblock name. (Could be caused by incomplete databasereset). Fix this in the survex file(s). {cave_id=} {year=} {qm_id=} {blockname=}" return render(request, "errors/generic.html", {"message": message}) except QM.DoesNotExist: # raise diff --git a/md5bash.sh b/md5bash.sh new file mode 100644 index 0000000..9a2ed8f --- /dev/null +++ b/md5bash.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# read in the input string from command line arguments +input=$1 + +# pad the input string with a single "1" bit +padded_input="$input"$(echo -ne '\x80') + +# pad the input string to a multiple of 512 bits (64 bytes) +while (( $(echo -n "$padded_input" | wc -c) % 64 != 56 )) +do + padded_input="$padded_input"$(echo -ne '\x00') +done + +# append the length of the input string (in bits) as a 64-bit little-endian integer +length=$(echo -n "$input" | wc -c) +length=$(echo "obase=16; $length * 8" | bc | xxd -p -c 16 | tac | tr -d '\n') +while (( $(echo -n "$length" | wc -c) < 16 )) +do + length="$length"0 +done +padded_input="$padded_input"$(echo -ne "$length" | xxd -r -p) + +# initialize the buffer (A, B, C, D) +A="67452301" +B="efcdab89" +C="98badcfe" +D="10325476" + +# process the input in 512-bit (64-byte) chunks +for (( i=0; i<$(echo -n "$padded_input" | wc -c)/64; i++ )) +do + chunk=$(echo -n "$padded_input" | dd bs=64 skip=$i count=1 2>/dev/null | xxd -p -c 64) + + # initialize the message schedule (M) + M=() + for (( j=0; j<16; j++ )) + do + word=$(echo -ne "${chunk:$j*8:8}" | xxd -r -p | od -An -tu4 -v) + M+=($word) + done + for (( j=16; j<64; j++ )) + do + word1=${M[j-15]} + s0=$(( (word1>>7 | word1<<25) ^ (word1>>18 | word1<<14) ^ (word1>>3) )) + + word2=${M[j-2]} + s1=$(( (word2>>17 | word2<<15) ^ (word2>>19 | word2<<13) ^ (word2>>10) )) + + M+=($((M[j-16] + s0 + M[j-7] + s1))) + done + + # initialize the working variables + AA=$A + BB=$B + CC=$C + DD=$D + + # round 1 + for (( j=0; j<16; j++ )) + do + F=$(( (B & C) | (~B & D) )) + g=$j + dTemp=$((D)) + D=$((C)) + C=$((B)) + B=$((B + ((A + F + M[g] + 0xd76aa478) & 0xffffffff))) + A=$((dTemp)) + done + + # round 2 + for (( j=16; j<32; j++ )) + do + F=$(( (D & B) | (~D & C) )) + g=$(( (5*j + 1) % 16 )) + dTemp=$((D)) + D=$((C)) + C=$((B)) + diff --git a/parsers/QMs.py b/parsers/QMs.py index 7a553be..f3044ff 100644 --- a/parsers/QMs.py +++ b/parsers/QMs.py @@ -65,7 +65,6 @@ def parseCaveQMs(cave, inputFile, ticked=False): nqms = parse_KH_QMs(kh, inputFile=inputFile, ticked=ticked) return nqms - # qmPath = settings.EXPOWEB+inputFile qmPath = Path(settings.EXPOWEB, inputFile) with open(qmPath, "r") as qmCSVContents: @@ -76,15 +75,17 @@ def parseCaveQMs(cave, inputFile, ticked=False): n = 0 nqms = 0 for line in qmReader: + #"Number","Grade","Area","Description","Page reference","Nearest survey station","Completion description","Comment" try: n += 1 year = int(line[0][1:5]) f"PH_{int(year)}_{int(n):02d}" QMnum = re.match(r".*?-\d*?-X?(?P\d*)", line[0]).group("numb") - newQM = QM() + newQM = QM() # creates python object, does not touch db yet # newQM.found_by=placeholder newQM.number = QMnum newQM.cave = caveid + newQM.expoyear = year newQM.blockname = "" if line[1] == "Dig": newQM.grade = "D" @@ -92,32 +93,28 @@ def parseCaveQMs(cave, inputFile, ticked=False): newQM.grade = line[1] newQM.area = line[2] newQM.location_description = line[3] - + newQM.page_ref = line[4] # In the table, completion is indicated by the presence of a completion discription. - newQM.completion_description = line[4] - newQM.nearest_station_description = line[5] + newQM.nearest_station_name = line[5] + newQM.completion_description = line[6] if newQM.completion_description: newQM.ticked = True else: newQM.ticked = False - newQM.comment = line[6] + newQM.comment = line[7] try: # year and number are unique for a cave in CSV imports preexistingQM = QM.objects.get( - number=QMnum, found_by__date__year=year - ) # if we don't have this one in the DB, save it - if ( - preexistingQM.new_since_parsing is False - ): # if the pre-existing QM has not been modified, overwrite it - VERY OLD THING + number=QMnum, expoyear=year, cave=caveid, + ) + if preexistingQM: + message = f" ! - {qmPath} PRE-EXISTING QM - should not exist ! {str(line)} " + print(message) + DataIssue.objects.create(parser="QMs", message=message) preexistingQM.delete() - newQM.expoyear = year - newQM.save() - else: # otherwise, print that it was ignored - print((" - preserving " + str(preexistingQM) + ", which was edited in admin \r")) - + newQM.save() except QM.DoesNotExist: # if there is no pre-existing QM, save the new one - newQM.expoyear = year newQM.save() nqms += 1 except KeyError: # check on this one @@ -130,6 +127,12 @@ def parseCaveQMs(cave, inputFile, ticked=False): print(message) DataIssue.objects.create(parser="QMs", message=message) continue + except: + message = f" ! - {qmPath} UNKNOWN error {str(line)} " + print(message) + DataIssue.objects.create(parser="QMs", message=message) + raise + continue return nqms @@ -139,8 +142,9 @@ def parse_KH_QMs(kh, inputFile, ticked): khQMs = khQMfile.readlines() nqms = 0 for line in khQMs: + #
C1997-161-27 A
Sib: pitch at end of Fuzzy Logic [Paradox Rift - continues] [sep.fuzzy.13] res = re.search( - r"name=\"[CB](?P\d*)-(?P\d*)-(?P\d*).* (?P[ABDCV])
(?P.*)\[(?P.*)\]", + r"name=\"[CB](?P\d*)-(?P\d*)-(?P\d*).* (?P[ABCDX])
(?P.*)\[(?P.*)\]", line, ) if res: @@ -157,8 +161,10 @@ def parse_KH_QMs(kh, inputFile, ticked): } nonLookupAttribs = { "ticked": ticked, - "nearest_station_name": res["nearest_station"], - "location_description": res["description"], + "page_ref": "", + "completion_description": "", + "nearest_station_name": res["nearest_station_name"], + "location_description": res["location_description"], } # Create new. We know it doesn't exist as we deleted evrything when we started. instance = QM.objects.create(**nonLookupAttribs, **lookupAttribs) @@ -169,8 +175,11 @@ def parse_KH_QMs(kh, inputFile, ticked): def Load_QMs(): deleteQMs() - n204 = parseCaveQMs(cave="204-steinBH", inputFile=r"1623/204/qm.csv") - n234 = parseCaveQMs(cave="234-Hauch", inputFile=r"1623/234/qm.csv") + #Number Grade Area Description Page reference Nearest station Completion description Comment + n204 = parseCaveQMs(cave="204-steinBH", inputFile=r"1623/204/qm-204.csv") # TAB separated values + + #"Number","Grade","Area","Description","Page reference","Nearest survey station","Completion description","Comment" + n234 = parseCaveQMs(cave="234-Hauch", inputFile=r"1623/234/qm-234.csv") # COMMA separated values, with quotes. n161 = parseCaveQMs(cave="161-KH", inputFile="1623/161/qmtodo.htm", ticked=False) t161 = parseCaveQMs(cave="161-KH", inputFile="1623/161/qmdone.htm", ticked=True) # parseCaveQMs(cave='balkonhoehle',inputFile=r"1623/264/qm.csv") diff --git a/parsers/survex.py b/parsers/survex.py index d8eda23..79e90d1 100644 --- a/parsers/survex.py +++ b/parsers/survex.py @@ -1309,7 +1309,10 @@ class LoadingSurvex: insp = self.insp # create a short, hopefully-unique name for this block to be used in the QM id - blockname = survexblock.name[:6] + survexblock.name[-1:] + if len(survexblock.name) < 7: + blockname = survexblock.name + else: + blockname = survexblock.name[:6] + survexblock.name[-1:] # logslug = f'D{int(qmyear)}_{blockname}_{int(qm_no):03d}' qm_ticked = False # default @@ -1319,7 +1322,7 @@ class LoadingSurvex: self.TickSurvexQM(survexblock, qmline) return - if qm_grade not in ["A", "B", "C", "D", "X", "V", "?"]: + if qm_grade not in ["A", "B", "C", "D", "X"]: # "V", "?" not allowed in survex file QMs message = f" ! QM{qm_no} INVALID code '{qm_grade}' [{blockname}] '{survexblock.survexfile.path}'" print(insp + message) stash_data_issue( @@ -1336,7 +1339,7 @@ class LoadingSurvex: pass else: qm_ticked = True - print(f"{survexblock.survexfile.cave} {survexblock}:{qm_no}{qm_grade} {qmline.group(4)}", file=sys.stderr) + # print(f"{survexblock.survexfile.cave} {survexblock}:{qm_no}{qm_grade} {qmline.group(4)}", file=sys.stderr) if resolution_station_name: qm_ticked = True # if qmline.group(6) and qmline.group(6) != "-": diff --git a/templates/cave_qms.html b/templates/cave_qms.html index 8b042d3..f55ce9b 100644 --- a/templates/cave_qms.html +++ b/templates/cave_qms.html @@ -20,39 +20,42 @@
  • 1626-359 QMs Homecoming +

    For how to set up your own QMs, see Adding QMs .

    For full explanation of the current status of the QM system(s), see scriptsqms page. {% endblock %} {% block content %} -

    Extant

    -

    {% if cave.get_QMs %} -

      - {% for QM in cave.get_QMs %} - {% if QM.ticked %} - {% else %} +

      Open leads

      +
        {% for QM in cave.get_open_QMs %}
      • {{QM}} - {% if QM.nearest_station_description %}⋮{{QM.nearest_station_description}}⋮{% endif %} {{QM.location_description}} {{QM.grade}} + {% if QM.nearest_station_name %}§{{QM.nearest_station_name}}§{% endif %} +{{QM.location_description}} {{QM.grade}} {% if QM.block %} {{QM.block}}.svx {{QM.block.date}} {% endif %}
      • - {% endif %} + {% empty %} +
      • No open leads. {% endfor %}
      + +§ QM.nearest_station_name
      +☆ QM.resolution_station_name

      Ticked off

      -
        - {% for QM in cave.get_QMs %} - {% if QM.ticked %} +
          {% for QM in cave.get_ticked_QMs %}
        • {{QM}} - {% if QM.nearest_station %}⋮{{QM.nearest_station}}⋮{% endif %} - {% if QM.nearest_station_name %}⋮{{QM.nearest_station_name}}⋮{% endif %} - {% if QM.nearest_station_description %}⋮{{QM.nearest_station_description}}⋮{% endif %} {{QM.location_description}} {{QM.grade}} + {% if QM.nearest_station_name %}§{{QM.nearest_station_name}}§{% endif %} + {% if QM.resolution_station_name %}☆{{QM.resolution_station_name}}☆{% endif %} + {{QM.location_description}} {{QM.grade}} {% if QM.block %} {{QM.block}}.svx {{QM.block.date}} {% endif %} {% if QM.completion_description %} Completion page: {{QM.completion_description}} - {% endif %} - {% endif %} - - - + {% endif %} + {% if QM.comment %} + Comment: {{QM.comment}} + {% endif %} + {% empty %} +
        • No ticked leads. {% endfor %}
        -{% endif %}

        +

        +§ QM.nearest_station_name
        +☆ QM.resolution_station_name
        {% endblock %} \ No newline at end of file diff --git a/templates/core/QMs_json_list.html b/templates/core/QMs_json_list.html index 70ed97b..6a52105 100644 --- a/templates/core/QMs_json_list.html +++ b/templates/core/QMs_json_list.html @@ -18,16 +18,4 @@ Hacked up list of caves with grade "?" {% endfor %} }

        ------------------------------------------------------
        -Hacked up list of caves with grade "V" -

        - -{% for qm in object_list %} -{% if qm.grade == "V" %}
        -"{{qm.id}}": ["{{qm.expoyear}}","{{qm.cave}}",{% if qm.ticked %}"TICKed",{% else %}"OPEN",{% endif %} -"{{qm.blockname}}","QM{{qm.number}}","{{qm.grade}}","{{qm.nearest_station_name}}","{{qm.resolution_station_name}}", -"{{qm.get_absolute_url}}" -]{% if not forloop.last %},{% endif %} -{% endif %} -{% endfor %} } -{% endblock %} diff --git a/templates/qm.html b/templates/qm.html index 69ada72..1afaa15 100644 --- a/templates/qm.html +++ b/templates/qm.html @@ -2,6 +2,75 @@ {% load link %} {% block title %} QM: {{qm|safe}} {% endblock %} {% block contentheader %} + + + +{% endblock %} + +{% block related %} +{% endblock %} + +{% block content %} + + + + + + +
        Previous QMQM   {{qm|safe}}Next QM
        + +{% if qm.ticked %} +

        This QM is TICKED

        + + +{% if qm.resolution_station_name %} +The survey station which record the continuation of the cave past this QM is {{qm.resolution_station_name}}.
        +{%endif %} +Tick off comment: '{{qm.comment}}'
        +Tick off completion description: '{{qm.completion_description}}'
        +Tick off url: {{qm.get_completion_url}} WRONG
        +Tick off date: '{{qm.completion_date}}' [For survex file ticks. not implemented yet]
        +{% endif %} + +{% if qm.cave %} +

        Cave: {{qm.cave|safe }}

        +All QMs on this cave {{ qm.cave|safe }}
        + Cave description {{ qm.cave|safe }} +{% else %} +

        This QM is OPEN

        +Comment: '{{qm.comment}}'
        +{% endif %} + +QM page_ref: '{{qm.page_ref}}'
        + +

        QM Number: {{qm.number}} Grade: {{qm.grade}}

        + +

        Explanation of Grade letters: Handbook - QM grades + +{% if qm.block %} +

        Survexfile

        +{{qm.block.date}} {{qm.block}}.svx +{% endif %} + + + + + +

        QM original closest survey station

        +{% if qm.block %} +Survex block: {{qm.blockname}} +
        +{% endif %} +Nearest station: '{{qm.nearest_station_name}}' {{nearest_station_description}} +

        QM original description

        +{{qm.location_description}} + +{% if qm.found_by %} +

        Creation

        +Found by {{qm.found_by}} on {{qm.found_by.date}}. +{% endif %} + +

        QMs available for these caves from CSV import

        For full explanation of the current status of the QM system(s), see scriptsqms page. - - - - - - - -
        Previous QMQM   {{qm|safe}}Next QM
        -{% endblock %} - -{% block related %} -{% endblock %} - -{% block content %} -

        Cave

        -{% if qm.cave %} -{{ qm.cave|safe }} QMs
        -{% else %} - {% if qm.cave %} - {{ qm.cave|safe }} QMs
        - {% endif %} -{% endif %} -{{ qm.cave|safe }} cave description - -

        QM Number

        -{{qm.number}} - -

        Grade

        -{{qm.grade}} -

        Explanation of Grade letters: Handbook - QM grades - -{% if qm.block %} -

        Survexfile

        -{{qm.block.date}} {{qm.block}}.svx -{% endif %} - -{% if qm.ticked %} -

        Ticked

        -This QM is TICKED -{% endif %} - -{% if qm.resolution_station_name %} -

        resolution_station_name

        -{{qm.resolution_station_name}} - -{% endif %} - -

        Location

        -{% if qm.block %} -Survex block: {{qm.blockname}} -
        -{% endif %} -Nearest station: '{{qm.nearest_station_name}}' {{nearest_station_description}} -

        QM Description

        -{{qm.location_description}} - -{% if qm.found_by %} -

        Creation

        -Found by {{qm.found_by}} on {{qm.found_by.date}}. -{% endif %} - -

        Completion

        -{% if qm.ticked %} -Ticked off log entry: {{qm.ticked_off_by}}
        -Ticked off date: {{qm.get_completion_url}} WRONG
        -Tick off date: [For survex file ticks. not implemented yet, needs model change]
        -Tick off completion description: {{qm.completion_description}} -{% else %} -No completion description yet- STILL EXTANT. - -{% endif %} - -

        Comment

        -{{qm.comment}} - +
        {% endblock %}