From de22b071b0749fef13ea3c53faaed1adb3191356 Mon Sep 17 00:00:00 2001 From: Sam Wenham Date: Fri, 19 Jul 2019 01:04:18 +0100 Subject: [PATCH] Improve README Make new style QMs from survexfiles work --- README.txt | 8 +- core/admin.py | 6 +- core/models.py | 44 +++++--- core/models_survex.py | 9 +- core/views_caves.py | 8 +- docker/requirements.txt.dj-1.10 | 4 +- parsers/logbooks.py | 10 ++ parsers/survex.py | 182 +++++++++++++++++++------------- templates/qm.html | 4 +- templates/survey.html | 4 +- templates/svxfile.html | 2 +- templates/tasks.html | 4 +- urls.py | 6 +- 13 files changed, 179 insertions(+), 112 deletions(-) diff --git a/README.txt b/README.txt index 81c2b14f1..79d8770fe 100644 --- a/README.txt +++ b/README.txt @@ -7,7 +7,7 @@ Troggle setup Python, Django, and Database setup ----------------------------------- -Troggle requires Django 1.4 or greater, and any version of Python that works with it. +Troggle requires Django 1.10, and Python 2.7. Install Django with the following command: apt-get install python-django (on debian/ubuntu) @@ -20,10 +20,14 @@ Troggle itself ------------- Choose a directory where you will keep troggle, and git clone Troggle into it using the following command: -git clone git://expo.survex.com/troggle +git clone git://expo.survex.com/~/troggle or more reliably git clone ssh://expo@expo.survex.com/home/expo/troggle +Running in development +---------------------- +The simplest way to run Troggle in development is through the docker-compose setup +See the docker folder in the repo for details If you want to work on the source code and be able to commit, your account will need to be added to the troggle project members list. Contact wookey at wookware dot org to get this set up. diff --git a/core/admin.py b/core/admin.py index ca38cfbb3..392e346e7 100644 --- a/core/admin.py +++ b/core/admin.py @@ -95,11 +95,11 @@ class PersonAdmin(TroggleModelAdmin): class QMAdmin(TroggleModelAdmin): search_fields = ('found_by__cave__kataster_number','number','found_by__date') - list_display = ('__unicode__','grade','found_by','ticked_off_by') + list_display = ('__unicode__','grade','found_by','ticked_off_by','nearest_station') list_display_links = ('__unicode__',) - list_editable = ('found_by','ticked_off_by','grade') + list_editable = ('found_by','ticked_off_by','grade','nearest_station') list_per_page = 20 - raw_id_fields=('found_by','ticked_off_by') + raw_id_fields=('found_by','ticked_off_by','nearest_station') class PersonExpeditionAdmin(TroggleModelAdmin): diff --git a/core/models.py b/core/models.py index 9529582eb..46a86da14 100644 --- a/core/models.py +++ b/core/models.py @@ -10,14 +10,13 @@ from django.db.models import Min, Max from django.conf import settings from decimal import Decimal, getcontext from django.core.urlresolvers import reverse -from imagekit.models import ImageModel +from imagekit.models import ProcessedImageField #ImageModel from django.template import Context, loader import settings getcontext().prec=2 #use 2 significant figures for decimal calculations from troggle.core.models_survex import * - def get_related_by_wikilinks(wiki_text): found=re.findall(settings.QM_PATTERN,wiki_text) res=[] @@ -98,11 +97,11 @@ class Expedition(TroggleModel): return res def day_min(self): - res = self.expeditionday_set.all() + res = self.Expeditionday_set.all() return res and res[0] or None def day_max(self): - res = self.expeditionday_set.all() + res = self.Expeditionday_set.all() return res and res[len(res) - 1] or None @@ -114,9 +113,12 @@ class ExpeditionDay(TroggleModel): ordering = ('date',) def GetPersonTrip(self, personexpedition): - personexpeditions = self.persontrip_set.filter(expeditionday=self) + personexpeditions = self.Persontrip_set.filter(expeditionday=self) return personexpeditions and personexpeditions[0] or None + def __unicode__(self): + return str(self.expedition) + ' ' + str(self.date) + # # single Person, can go on many years # @@ -462,7 +464,7 @@ class Cave(TroggleModel): return unicode(self.slug()) def get_QMs(self): - return QM.objects.filter(found_by__cave_slug=self.caveslug_set.all()) + return QM.objects.filter(nearest_station__block__cave__caveslug=self.caveslug_set.all()) def new_QM_number(self, year=datetime.date.today().year): """Given a cave and the current year, returns the next QM number.""" @@ -739,20 +741,20 @@ class NewSubCave(TroggleModel): return unicode(self.name) class QM(TroggleModel): - #based on qm.csv in trunk/expoweb/1623/204 which has the fields: - #"Number","Grade","Area","Description","Page reference","Nearest station","Completion description","Comment" + # based on qm.csv in trunk/expoweb/1623/204 which has the fields: + # "Number","Grade","Area","Description","Page reference","Nearest station","Completion description","Comment" found_by = models.ForeignKey(LogbookEntry, related_name='QMs_found',blank=True, null=True ) ticked_off_by = models.ForeignKey(LogbookEntry, related_name='QMs_ticked_off',null=True,blank=True) - #cave = models.ForeignKey(Cave) - #expedition = models.ForeignKey(Expedition) + # cave = models.ForeignKey(Cave) + # expedition = models.ForeignKey(Expedition) number = models.IntegerField(help_text="this is the sequential number in the year", ) GRADE_CHOICES=( - ('A', 'A: Large obvious lead'), - ('B', 'B: Average lead'), - ('C', 'C: Tight unpromising lead'), - ('D', 'D: Dig'), - ('X', 'X: Unclimbable aven') + ('A', 'A: Large obvious lead'), + ('B', 'B: Average lead'), + ('C', 'C: Tight unpromising lead'), + ('D', 'D: Dig'), + ('X', 'X: Unclimbable aven') ) grade = models.CharField(max_length=1, choices=GRADE_CHOICES) location_description = models.TextField(blank=True) @@ -767,11 +769,19 @@ class QM(TroggleModel): return u"%s %s" % (self.code(), self.grade) def code(self): - return u"%s-%s-%s" % (unicode(self.found_by.cave)[6:], self.found_by.date.year, self.number) + if self.found_by: + # Old style QMs where found_by is a logbook entry + return u"%s-%s-%s" % (unicode(self.found_by.cave)[6:], self.found_by.date.year, self.number) + elif self.nearest_station: + # New style QMs where QMs are stored in SVX files and nearest station is a forigin key + return u"%s-%s-%s" % (self.nearest_station.block.name, self.nearest_station.name, self.number) + else: + # Just give up!! + return u"%s" % (self.number) def get_absolute_url(self): #return settings.URL_ROOT + '/cave/' + self.found_by.cave.kataster_number + '/' + str(self.found_by.date.year) + '-' + '%02d' %self.number - return urlparse.urljoin(settings.URL_ROOT, reverse('qm',kwargs={'cave_id':self.found_by.cave.kataster_number,'year':self.found_by.date.year,'qm_id':self.number,'grade':self.grade})) + return urlparse.urljoin(settings.URL_ROOT, reverse('qm',kwargs={'qm_id':self.id})) def get_next_by_id(self): return QM.objects.get(id=self.id+1) diff --git a/core/models_survex.py b/core/models_survex.py index e78cd48e0..3becffbe5 100644 --- a/core/models_survex.py +++ b/core/models_survex.py @@ -47,6 +47,8 @@ class SurvexFile(models.Model): def SetDirectory(self): dirpath = os.path.split(self.path)[0] survexdirectorylist = SurvexDirectory.objects.filter(cave=self.cave, path=dirpath) + # if self.cave is '' or self.cave is None: + # print('No cave set for survex dir %s' % self.path) if survexdirectorylist: self.survexdirectory = survexdirectorylist[0] else: @@ -74,8 +76,11 @@ class SurvexStation(models.Model): z = models.FloatField(blank=True, null=True) def __unicode__(self): - return self.block.cave.slug() + '/' + self.block.name + '/' + self.name or 'No station name' - + if self.block.cave: + # If we haven't got a cave we can't have a slug, saves a nonetype return + return self.block.cave.slug() + '/' + self.block.name + '/' + self.name or 'No station name' + else: + return str(self.block.cave) + '/' + self.block.name + '/' + self.name or 'No station name' def path(self): r = self.name b = self.block diff --git a/core/views_caves.py b/core/views_caves.py index 7b0555ff2..6dab34a88 100644 --- a/core/views_caves.py +++ b/core/views_caves.py @@ -50,7 +50,7 @@ def caveCmp(x, y): return numericalcmp(x.unofficial_number, y.unofficial_number) def caveindex(request): - caves = Cave.objects.all() + #caves = Cave.objects.all() notablecavehrefs = settings.NOTABLECAVESHREFS notablecaves = [Cave.objects.get(kataster_number=kataster_number) for kataster_number in notablecavehrefs ] caves1623 = list(Cave.objects.filter(area__short_name = "1623")) @@ -106,6 +106,7 @@ def caveQMs(request, slug): return render(request,'nonpublic.html', {'instance': cave}) else: return render(request,'cave_qms.html', {'cave': cave}) + def caveLogbook(request, slug): cave = Cave.objects.get(caveslug__slug = slug) if cave.non_public and settings.PUBLIC_SITE and not request.user.is_authenticated(): @@ -210,10 +211,9 @@ def editEntrance(request, caveslug, slug=None): 'entletter': entletter }) -def qm(request,cave_id,qm_id,year,grade=None): - year=int(year) +def qm(request,qm_id): try: - qm=getCave(cave_id).get_QMs().get(number=qm_id,found_by__date__year=year) + qm=QM.objects.get(id=qm_id) return render(request,'qm.html',locals()) except QM.DoesNotExist: diff --git a/docker/requirements.txt.dj-1.10 b/docker/requirements.txt.dj-1.10 index 603aa254a..622653629 100644 --- a/docker/requirements.txt.dj-1.10 +++ b/docker/requirements.txt.dj-1.10 @@ -8,4 +8,6 @@ smartencoding fuzzywuzzy GitPython unidecode -django-extensions \ No newline at end of file +django-extensions +pygraphviz +python-Levenshtein diff --git a/parsers/logbooks.py b/parsers/logbooks.py index 224b2cd0e..012d771c1 100644 --- a/parsers/logbooks.py +++ b/parsers/logbooks.py @@ -14,6 +14,7 @@ import csv import re import datetime import os +from fuzzywuzzy import fuzz from utils import save_carefully @@ -42,6 +43,15 @@ def GetTripPersons(trippeople, expedition, logtime_underground): print(" - No name match for: '%s'" % tripperson) message = "No name match for: '%s' in year '%s'" % (tripperson, expedition.year) models.DataIssue.objects.create(parser='logbooks', message=message) + print(' - Lets try something fuzzy') + fuzzy_matches = {} + for person in GetPersonExpeditionNameLookup(expedition): + fuzz_num = fuzz.ratio(tripperson.lower(), person) + if fuzz_num > 50: + #print(" - %s -> %s = %d" % (tripperson.lower(), person, fuzz_num)) + fuzzy_matches[person] = fuzz_num + for i in sorted(fuzzy_matches.items(), key = lambda kv:(kv[1]), reverse=True): + print(' - %s -> %s' % (i[0], i[1])) res.append((personyear, logtime_underground)) if mul: author = personyear diff --git a/parsers/survex.py b/parsers/survex.py index f80f9f276..d542c094c 100644 --- a/parsers/survex.py +++ b/parsers/survex.py @@ -14,7 +14,8 @@ from datetime import datetime line_leg_regex = re.compile(r"[\d\-+.]+$") -def LoadSurvexLineLeg(survexblock, stardata, sline, comment): +def LoadSurvexLineLeg(survexblock, stardata, sline, comment, cave): + # The try catches here need replacing as they are relativly expensive ls = sline.lower().split() ssfrom = survexblock.MakeSurvexStation(ls[stardata["from"]]) ssto = survexblock.MakeSurvexStation(ls[stardata["to"]]) @@ -62,7 +63,10 @@ def LoadSurvexLineLeg(survexblock, stardata, sline, comment): assert line_leg_regex.match(lclino) and lclino != "-", ls survexleg.compass = float(lcompass) survexleg.clino = float(lclino) - + + if cave: + survexleg.cave = cave + # only save proper legs survexleg.save() @@ -94,23 +98,41 @@ regex_ref = re.compile(r'.*?ref.*?(\d+)\s*#\s*(\d+)') regex_star = re.compile(r'\s*\*[\s,]*(\w+)\s*(.*?)\s*(?:;.*)?$') regex_team = re.compile(r"(Insts|Notes|Tape|Dog|Useless|Pics|Helper|Disto|Consultant)\s+(.*)$(?i)") regex_team_member = re.compile(r" and | / |, | & | \+ |^both$|^none$(?i)") +regex_qm = re.compile(r'^\s*QM(\d)\s+?([a-dA-DxX])\s+([\w\-]+)\.(\d+)\s+(([\w\-]+)\.(\d+)|\-)\s+(.+)$') def RecursiveLoad(survexblock, survexfile, fin, textlines): iblankbegins = 0 text = [ ] stardata = stardatadefault teammembers = [ ] - -# uncomment to print out all files during parsing + + # uncomment to print out all files during parsing print(" - Reading file: " + survexblock.survexfile.path) stamp = datetime.now() lineno = 0 - while True: - svxline = fin.readline().decode("latin1") - if not svxline: - print(' - Not survex') - return - textlines.append(svxline) + + # Try to find the cave in the DB if not use the string as before + path_match = re.search(r"caves-(\d\d\d\d)/(\d+|\d\d\d\d-?\w+-\d+)/", survexblock.survexfile.path) + if path_match: + pos_cave = '%s-%s' % (path_match.group(1), path_match.group(2)) + # print('Match') + # print(pos_cave) + cave = models.getCaveByReference(pos_cave) + if cave: + survexfile.cave = cave + svxlines = '' + svxlines = fin.read().splitlines() + # print('Cave - preloop ' + str(survexfile.cave)) + # print(survexblock) + for svxline in svxlines: + + # print(survexblock) + + # print(svxline) + # if not svxline: + # print(' - Not survex') + # return + # textlines.append(svxline) lineno += 1 @@ -118,7 +140,6 @@ def RecursiveLoad(survexblock, survexfile, fin, textlines): # break the line at the comment sline, comment = regex_comment.match(svxline.strip()).groups() - # detect ref line pointing to the scans directory mref = comment and regex_ref.match(comment) if mref: @@ -129,7 +150,52 @@ def RecursiveLoad(survexblock, survexfile, fin, textlines): #survexblock.refscandir = "%s/%s%%23%s" % (mref.group(1), mref.group(1), mref.group(2)) survexblock.save() continue - + + # This whole section should be moved if we can have *QM become a proper survex command + # Spec of QM in SVX files, currently commented out need to add to survex + # needs to match regex_qm + # ;Serial number grade(A/B/C/D/X) nearest-station resolution-station description + # ;QM1 a hobnob_hallway_2.42 hobnob-hallway_3.42 junction of keyhole passage + # ;QM1 a hobnob_hallway_2.42 - junction of keyhole passage + qmline = comment and regex_qm.match(comment) + if qmline: + print(qmline.groups()) + #(u'1', u'B', u'miraclemaze', u'1.17', u'-', None, u'\tcontinuation of rift') + qm_no = qmline.group(1) + qm_grade = qmline.group(2) + qm_from_section = qmline.group(3) + qm_from_station = qmline.group(4) + qm_resolve_section = qmline.group(6) + qm_resolve_station = qmline.group(7) + qm_notes = qmline.group(8) + + print('Cave - %s' % survexfile.cave) + print('QM no %d' % int(qm_no)) + print('QM grade %s' % qm_grade) + print('QM section %s' % qm_from_section) + print('QM station %s' % qm_from_station) + print('QM res section %s' % qm_resolve_section) + print('QM res station %s' % qm_resolve_station) + print('QM notes %s' % qm_notes) + + # If the QM isn't resolved (has a resolving station) thn load it + if not qm_resolve_section or qm_resolve_section is not '-' or qm_resolve_section is not 'None': + from_section = models.SurvexBlock.objects.filter(name=qm_from_section) + # If we can find a section (survex note chunck, named) + if len(from_section) > 0: + print(from_section[0]) + from_station = models.SurvexStation.objects.filter(block=from_section[0], name=qm_from_station) + # If we can find a from station then we have the nearest station and can import it + if len(from_station) > 0: + print(from_station[0]) + qm = models.QM.objects.create(number=qm_no, + nearest_station=from_station[0], + grade=qm_grade.upper(), + location_description=qm_notes) + else: + print('QM found but resolved') + + #print('Cave -sline ' + str(cave)) if not sline: continue @@ -137,9 +203,12 @@ def RecursiveLoad(survexblock, survexfile, fin, textlines): mstar = regex_star.match(sline) if not mstar: if "from" in stardata: - LoadSurvexLineLeg(survexblock, stardata, sline, comment) + # print('Cave ' + str(survexfile.cave)) + # print(survexblock) + LoadSurvexLineLeg(survexblock, stardata, sline, comment, survexfile.cave) # print(' - From: ') #print(stardata) + pass elif stardata["type"] == "passage": LoadSurvexLinePassage(survexblock, stardata, sline, comment) # print(' - Passage: ') @@ -156,26 +225,42 @@ def RecursiveLoad(survexblock, survexfile, fin, textlines): path_match = re.search(r"caves-(\d\d\d\d)/(\d+|\d\d\d\d-?\w+-\d+)/", includepath) if path_match: pos_cave = '%s-%s' % (path_match.group(1), path_match.group(2)) - print(pos_cave) + # print(pos_cave) cave = models.getCaveByReference(pos_cave) - if not cave: - cave = survexfile.cave + if cave: + survexfile.cave = cave else: print('No match for %s' % includepath) - cave = survexfile.cave - includesurvexfile = models.SurvexFile(path=includepath, cave=cave) + includesurvexfile = models.SurvexFile(path=includepath) includesurvexfile.save() includesurvexfile.SetDirectory() if includesurvexfile.exists(): + survexblock.save() fininclude = includesurvexfile.OpenFile() RecursiveLoad(survexblock, includesurvexfile, fininclude, textlines) elif re.match("begin$(?i)", cmd): - if line: + if line: + newsvxpath = os.path.join(os.path.split(survexfile.path)[0], re.sub(r"\.svx$", "", line)) + # Try to find the cave in the DB if not use the string as before + path_match = re.search(r"caves-(\d\d\d\d)/(\d+|\d\d\d\d-?\w+-\d+)/", newsvxpath) + if path_match: + pos_cave = '%s-%s' % (path_match.group(1), path_match.group(2)) + print(pos_cave) + cave = models.getCaveByReference(pos_cave) + if cave: + survexfile.cave = cave + else: + print('No match for %s' % newsvxpath) + name = line.lower() - #print(' - Begin found for: ' + name) - survexblockdown = models.SurvexBlock(name=name, begin_char=fin.tell(), parent=survexblock, survexpath=survexblock.survexpath+"."+name, cave=survexblock.cave, survexfile=survexfile, totalleglength=0.0) + print(' - Begin found for: ' + name) + # print('Block cave: ' + str(survexfile.cave)) + survexblockdown = models.SurvexBlock(name=name, begin_char=fin.tell(), parent=survexblock, survexpath=survexblock.survexpath+"."+name, cave=survexfile.cave, survexfile=survexfile, totalleglength=0.0) survexblockdown.save() + survexblock.save() + survexblock = survexblockdown + # print(survexblockdown) textlinesdown = [ ] RecursiveLoad(survexblockdown, survexfile, fin, textlinesdown) else: @@ -222,8 +307,9 @@ def RecursiveLoad(survexblock, survexfile, fin, textlines): elif cmd == "title": #print(' - Title found: ') - survextitle = models.SurvexTitle(survexblock=survexblock, title=line.strip('"'), cave=survexblock.cave) + survextitle = models.SurvexTitle(survexblock=survexblock, title=line.strip('"'), cave=survexfile.cave) survextitle.save() + pass elif cmd == "require": # should we check survex version available for processing? @@ -261,37 +347,6 @@ def RecursiveLoad(survexblock, survexfile, fin, textlines): timetaken = endstamp - stamp # print(' - Time to process: ' + str(timetaken)) - -def ReloadSurvexCave(survex_cave, area): - print(' - Area: ' + str(area) + ' Cave: ' + str(survex_cave)) - filt_cave = models.Cave.objects.filter(kataster_number=survex_cave, area__short_name=area)[:1] - - if len(filt_cave) < 1: - filt_cave = models.Cave.objects.filter(unofficial_number=survex_cave, area__short_name=area)[:1] - - cave = filt_cave[0] - print(' - ' + str(cave)) - cave.survexblock_set.all().delete() - cave.survexfile_set.all().delete() - cave.survexdirectory_set.all().delete() - - file_stamp_start = datetime.now() - survexfile = models.SurvexFile(path="caves-" + cave.kat_area() + "/" + survex_cave + "/" + survex_cave, cave=cave) - survexfile.save() - survexfile.SetDirectory() - - survexblockroot = models.SurvexBlock(name="root", survexpath="caves-" + cave.kat_area(), begin_char=0, cave=cave, survexfile=survexfile, totalleglength=0.0) - survexblockroot.save() - fin = survexfile.OpenFile() - file_stamp_end = datetime.now() - file_time = file_stamp_end - file_stamp_start - print(' - Files time to process: ' + str(file_time)) - textlines = [ ] - RecursiveLoad(survexblockroot, survexfile, fin, textlines) - survexblockroot.text = "".join(textlines) - survexblockroot.save() - - def LoadAllSurvexBlocks(): print('Loading All Survex Blocks...') @@ -316,30 +371,13 @@ def LoadAllSurvexBlocks(): survexblockroot.save() fin = survexfile.OpenFile() textlines = [ ] + # The real work starts here RecursiveLoad(survexblockroot, survexfile, fin, textlines) + fin.close() survexblockroot.text = "".join(textlines) survexblockroot.save() - #Load each cave, - #FIXME this should be dealt with load all above - print(" - Reloading all caves") - caves = models.Cave.objects.all() - for cave in caves: - rec_stamp_start = datetime.now() - if cave.kataster_number and os.path.isdir(os.path.join(settings.SURVEX_DATA, "caves-" + cave.kat_area(), cave.kataster_number)): - if cave.kataster_number not in ['40']: - print(" - Loading " + str(cave) + " " + cave.kat_area()) - #ReloadSurvexCave(cave.kataster_number, cave.kat_area()) - rec_stamp_end = datetime.now() - if cave.unofficial_number and os.path.isdir(os.path.join(settings.SURVEX_DATA, "caves-" + cave.kat_area(), cave.unofficial_number)): - print(" - Loading " + str(cave) + " " + cave.kat_area()) - #ReloadSurvexCave(cave.unofficial_number, cave.kat_area()) - timetaken = rec_stamp_end - rec_stamp_start - # print(' - Time to process: ' + str(timetaken)) - # print('--------') - - poslineregex = re.compile(r"^\(\s*([+-]?\d*\.\d*),\s*([+-]?\d*\.\d*),\s*([+-]?\d*\.\d*)\s*\)\s*([^\s]+)$") diff --git a/templates/qm.html b/templates/qm.html index f96019776..0e5815634 100644 --- a/templates/qm.html +++ b/templates/qm.html @@ -4,9 +4,7 @@ {% block title %} QM: {{qm|wiki_to_html_short}} {% endblock %} -{% block editLink %}| Edit QM {{qm|wiki_to_html_short}}{% endblock %} - - +{% block editLink %}| Edit QM {{qm|wiki_to_html_short}}{% endblock %} {% block contentheader %} diff --git a/templates/survey.html b/templates/survey.html index 13a8800d7..f83aa9441 100644 --- a/templates/survey.html +++ b/templates/survey.html @@ -5,7 +5,7 @@ {% block title %}CUCC Virtual Survey Binder: {{ current_expedition }}{{ current_survey }}{%endblock%} {% block head %} - + + diff --git a/templates/tasks.html b/templates/tasks.html index 36d35f415..f741bdf64 100644 --- a/templates/tasks.html +++ b/templates/tasks.html @@ -34,6 +34,6 @@ add wikilinks {% endblock content %} {% block margins %} - - + + {% endblock margins %} diff --git a/urls.py b/urls.py index 911b8723b..3ad52e528 100644 --- a/urls.py +++ b/urls.py @@ -75,9 +75,9 @@ actualurlpatterns = [ url(r'^cave/(?P[^/]+)/edit/$', views_caves.edit_cave, name="edit_cave"), #(r'^cavesearch', caveSearch), - - url(r'^cave/(?P[^/]+)/(?P\d\d\d\d)-(?P\d*)(?P[ABCDX]?)?$', views_caves.qm, name="qm"), - + # url(r'^cave/(?P[^/]+)/(?P\d\d\d\d)-(?P\d*)(?P[ABCDX]?)?$', views_caves.qm, name="qm"), + url(r'^cave/qm/(?P[^/]+)?$', views_caves.qm, name="qm"), + url(r'^prospecting_guide/$', views_caves.prospecting), url(r'^logbooksearch/(.*)/?$', views_logbooks.logbookSearch),