2
0
mirror of https://expo.survex.com/repositories/troggle/.git synced 2024-11-21 23:01:52 +00:00

Removed class Area, use Cave.areacode Cave.subarea

This commit is contained in:
Philip Sargent 2023-09-10 15:42:36 +03:00
parent ad272fab3b
commit ab79a43afa
10 changed files with 75 additions and 159 deletions

View File

@ -2,7 +2,7 @@ from django.contrib import admin
from django.core import serializers
from django.http import HttpResponse
from troggle.core.models.caves import Area, Cave, CaveAndEntrance, Entrance
from troggle.core.models.caves import Cave, CaveAndEntrance, Entrance
from troggle.core.models.logbooks import QM, LogbookEntry, PersonLogEntry, CaveSlug
from troggle.core.models.survex import (
DrawingFile,
@ -135,7 +135,7 @@ class WalletAdmin(TroggleModelAdmin):
admin.site.register(Cave, CaveAdmin)
admin.site.register(Area)
#admin.site.register(Area)
admin.site.register(CaveAndEntrance)
admin.site.register(Entrance, EntranceAdmin)
admin.site.register(CaveSlug)

View File

@ -32,33 +32,15 @@ todo = """
- Can we rewrite things to eliminate the CaveSlug and objects? Surely
foreign keys work fine ?!
- Why do we have CaveAndEntrance objects ? Surely entranceletter belong son the Entrance object?
- Why do we have CaveAndEntrance objects ? Surely entranceletter belongs on the Entrance object?
- move the aliases list from the code and put into an editable file
- Restore constraint: unique_together = (("area", "kataster_number"), ("area", "unofficial_number"))
or replace by a unique 'slug' field, better.
"""
class Area(TroggleModel):
short_name = models.CharField(max_length=100)
name = models.CharField(max_length=200, blank=True, null=True)
description = models.TextField(blank=True, null=True)
super = models.ForeignKey("Area", blank=True, null=True, on_delete=models.SET_NULL)
def __str__(self):
if self.super:
return str(self.super) + " - " + str(self.short_name)
else:
return str(self.short_name)
def kat_area(self):
if self.short_name in ["1623", "1626", "1624", "1627"]:
return self.short_name
elif self.super:
return self.super.kat_area()
class CaveAndEntrance(models.Model):
"""This class is ONLY used to create a FormSet for editing the cave and all its
entrances in one form.
@ -82,7 +64,8 @@ class CaveAndEntrance(models.Model):
class Cave(TroggleModel):
# too much here perhaps,
area = models.ManyToManyField(Area, blank=False)
areacode = models.CharField(max_length=4, blank=True, null=True) # could use models.IntegerChoices
subarea = models.CharField(max_length=10, blank=True, null=True) # 9, 8c etc.
depth = models.CharField(max_length=100, blank=True, null=True)
description_file = models.CharField(max_length=200, blank=True, null=True)
entrances = models.ManyToManyField("Entrance", through="CaveAndEntrance")
@ -97,7 +80,7 @@ class Cave(TroggleModel):
notes = models.TextField(blank=True, null=True)
official_name = models.CharField(max_length=160)
references = models.TextField(blank=True, null=True)
survex_file = models.CharField(max_length=100, blank=True, null=True) # should be a foreign key
survex_file = models.CharField(max_length=100, blank=True, null=True) # should be a foreign key?
survey = models.TextField(blank=True, null=True)
underground_centre_line = models.TextField(blank=True, null=True)
underground_description = models.TextField(blank=True, null=True)
@ -192,12 +175,7 @@ class Cave(TroggleModel):
return qms # a QuerySet
def kat_area(self):
try:
for a in self.area.all():
if a.kat_area():
return a.kat_area()
except:
return ""
return self.areacode
def entrances(self):
return CaveAndEntrance.objects.filter(cave=self)
@ -258,15 +236,7 @@ class Cave(TroggleModel):
return (filepath, content, "utf8")
def getArea(self):
areas = self.area.all()
lowestareas = list(areas)
for area in areas:
if area.super in areas:
try:
lowestareas.remove(area.super)
except:
pass
return lowestareas[0]
return self.areacode
class Entrance(TroggleModel):
MARKING_CHOICES = (

View File

@ -124,10 +124,10 @@ def getnotablecaves():
def caveindex(request):
#Cave.objects.all()
caves1623 = list(Cave.objects.filter(area__short_name="1623"))
caves1626 = list(Cave.objects.filter(area__short_name="1626"))
caves1627 = list(Cave.objects.filter(area__short_name="1627"))
caves1623 = list(Cave.objects.filter(areacode="1623"))
caves1626 = list(Cave.objects.filter(areacode="1626"))
caves1627 = list(Cave.objects.filter(areacode="1627"))
caves1623.sort(key=caveKey)
caves1626.sort(key=caveKey)
caves1627.sort(key=caveKey)

View File

@ -4,7 +4,7 @@ from django.http import HttpResponse
from django.shortcuts import render
import troggle.settings as settings
from troggle.core.models.caves import Area, Cave, Entrance
from troggle.core.models.caves import Cave, Entrance
from troggle.core.views.caves import caveKey
# from pathlib import Path
@ -68,11 +68,11 @@ def prospecting(request):
return render(request, "errors/disabled.html", {"message": message})
areas = []
for key, name in AREANAMES:
a = Area.objects.get(short_name=key) # assumes unique
caves = list(a.cave_set.all())
caves.sort(key=caveKey)
areas.append((name, a, caves))
caves = Cave.objects.all()
for c in caves:
if c.subarea in AREANAMES:
areas.append((AREANAMES[c.subarea], subarea, c))
return render(request, "prospecting.html", {"areas": areas})

View File

@ -574,15 +574,15 @@ def survexcaveslist(request):
subdircaves = []
fnumlist = []
for area in ["1623", "1626", "1624", "1627"]:
cavesdir = get_survexareapath(area)
arealist = sorted([(area, -int(re.match(r"\d*", f).group(0) or "0"), f) for f in os.listdir(cavesdir)])
for areacode in ["1623", "1626", "1624", "1627"]:
cavesdir = get_survexareapath(areacode)
arealist = sorted([(areacode, -int(re.match(r"\d*", f).group(0) or "0"), f) for f in os.listdir(cavesdir)])
fnumlist += arealist
# print(fnumlist)
# go through the list and identify the contents of each cave directory
for area, num, cavedir in fnumlist:
for areacode, num, cavedir in fnumlist:
# these have sub dirs /cucc/ /arge/ /old/ but that is no reason to hide them in this webpage
# so these are now treated the same as 142 and 113 which also have a /cucc/ sub dir
@ -593,18 +593,17 @@ def survexcaveslist(request):
# which usually but not always true. e.g. caves-1623/78/allkaese.svx not caves-1623/78/78.svx
# which is why we now also pass through the cavedir
# Still fails for loutitohoehle etc even though this is set correctly when the pending cave is created
cavesdir = get_survexareapath(area)
cavesdir = get_survexareapath(areacode)
gcavedir = os.path.join(cavesdir, cavedir)
if os.path.isdir(gcavedir) and cavedir[0] != ".":
subdirs, subsvx = identifycavedircontents(gcavedir)
check_cave_registered(
area, cavedir
areacode, cavedir
) # should do this only once per database load or it will be slow
survdirobj = []
for lsubsvx in subsvx:
survdirobj.append(("caves-" + area + "/" + cavedir + "/" + lsubsvx, lsubsvx))
survdirobj.append(("caves-" + areacode + "/" + cavedir + "/" + lsubsvx, lsubsvx))
# caves with subdirectories
if subdirs:
@ -614,7 +613,7 @@ def survexcaveslist(request):
# assert not dsubdirs # handle case of empty sub directory
lsurvdirobj = []
for lsubsvx in dsubsvx:
lsurvdirobj.append(("caves-" + area + "/" + cavedir + "/" + subdir + "/" + lsubsvx, lsubsvx))
lsurvdirobj.append(("caves-" + areacode + "/" + cavedir + "/" + subdir + "/" + lsubsvx, lsubsvx))
if len(dsubsvx) >= 1:
subsurvdirs.append(
(subdir, lsurvdirobj[0], lsurvdirobj[0:])
@ -683,7 +682,7 @@ def survexcavesingle(request, cave_shortname):
else:
return render(request, "errors/svxcaves404.html", {"settings": settings, "cave": cave_shortname})
def check_cave_registered(area, survex_cave):
def check_cave_registered(areacode, survex_cave):
"""Checks whether a cave has been properly registered when it is found in the Loser repo
This should really be called by databaseReset not here in a view
Currently Caves are only registered if they are listed in :expoweb: settings.CAVEDESCRIPTIONS
@ -699,7 +698,7 @@ def check_cave_registered(area, survex_cave):
except MultipleObjectsReturned:
caves = Cave.objects.filter(kataster_number=survex_cave)
for c in caves:
if str(c) == area + "-" + survex_cave:
if str(c) == areacode + "-" + survex_cave:
return str(c) # just get the first that matches
return None # many returned but none in correct area

View File

@ -8,7 +8,7 @@ from pathlib import Path
from django.conf import settings
from django.db import transaction
from troggle.core.models.caves import Area, Cave, CaveAndEntrance, Entrance, GetCaveLookup
from troggle.core.models.caves import Cave, CaveAndEntrance, Entrance, GetCaveLookup
from troggle.core.models.logbooks import CaveSlug
from troggle.core.models.troggle import DataIssue
from troggle.settings import CAVEDESCRIPTIONS, ENTRANCEDESCRIPTIONS, EXPOWEB, SURVEX_DATA
@ -99,36 +99,6 @@ def set_dummy_entrance(id, slug, cave, msg="DUMMY"):
# DataIssue.objects.create(parser="entrances", message=message, url=f"{cave.url}")
# print(message)
def make_areas():
print(" - Creating Areas 1623, 1624, 1627 and 1626")
# This crashes on the server with MariaDB even though a null parent is explicitly allowed.
area_1623 = Area.objects.create(short_name="1623", super=None)
area_1623.save()
area_1624 = Area.objects.create(short_name="1624", super=None)
area_1624.save()
area_1626 = Area.objects.create(short_name="1626", super=None)
area_1626.save()
area_1627 = Area.objects.create(short_name="1627", super=None)
area_1627.save()
def get_area(areanum):
"""Given the number as a string, return the area object
"""
a = Area.objects.all()
if len(a) == 0:
make_areas()
area = Area.objects.get(short_name="1623") # default
if areanum == "1623":
area = Area.objects.get(short_name="1623")
if areanum == "1624":
area = Area.objects.get(short_name="1624")
if areanum == "1626":
area = Area.objects.get(short_name="1626")
if areanum == "1627":
area = Area.objects.get(short_name="1627")
return area
def create_new_cave(svxpath, msg=None):
"""This is called only when a new survex file is edited online which has a path on the
@ -145,29 +115,29 @@ def create_new_cave(svxpath, msg=None):
print(f"parts {parts}, {a}, {caveid}")
# double check
if a[0:3] == "162":
areanum = a[0:4]
url = f"{areanum}/{caveid}.html" # Note we are appending the .html as we are believe in backwards compatability.
#url = f"{areanum}/{a[5:]}.html" # This is original code, but a above is only defined as being 4 characters long, so it did not make sense and produced non unique urls
areacode = a[0:4]
url = f"{areacode}/{caveid}.html" # Note we are appending the .html as we are believe in backwards compatability.
#url = f"{areacode}/{a[5:]}.html" # This is original code, but a above is only defined as being 4 characters long, so it did not make sense and produced non unique urls
else:
print(f"WARNING: parsers/caves/create_new_cave called with svxpath '{svxpath}'. Surely it should start 'caves-162*'? {msg}")
areanum = "1623"
areacode = "1623"
url = f"1623/{caveid}.html"
#url = f"1623/{k}.html" # This is original code, but a above is only defined as being 4 characters long, so it did not make sense and produced non unique urls
k = f"{areanum}-{caveid}"
area = get_area(areanum)
k = f"{areacode}-{caveid}"
caves = Cave.objects.filter(unofficial_number=caveid, area =areanum)
caves = Cave.objects.filter(unofficial_number=caveid, areacode =areacode)
if caves:
message = f" ! Already exists, caveid:{k} in area {areanum} {caves} - {msg}"
message = f" ! Already exists, caveid:{k} in areacode {areacode} {caves} - {msg}"
DataIssue.objects.create(parser="caves", message=message)
print(message)
return caves[0]
try:
cave = do_pending_cave(k, caveid, url, area, msg)
cave = do_pending_cave(k, caveid, url, areacode, msg)
except:
message = f" ! Error. Cannot create pending cave and entrance, pending-id:{k} in area {areanum} - {msg}"
message = f" ! Error. Cannot create pending cave and entrance, pending-id:{k} in area {areacode} - {msg}"
DataIssue.objects.create(parser="caves", message=message)
print(message)
raise
@ -175,14 +145,15 @@ def create_new_cave(svxpath, msg=None):
# we know what the survex file is, we don't need to use the guess.
# But this sets the survex file on he cave from the first one we find, not necessarily the best survex file for this cave
cave.survex_file=survex_file
cave.areacode=areacode
cave.save()
return cave
def do_ARGE_cave(slug, caveid, url, area, svxid):
def do_ARGE_cave(slug, caveid, url, areacode, svxid):
"""Only called by survex parser.
Creates a new Cave object, but with abbreviated data as the survex file (from ARGE) is all we have.
We already know the survex file.
We already know that it doesn't exist.
We already know that it doesn't exist... though there are bugs..
"""
default_note = "This is an ARGE cave where we only have the survex file and no other information"
@ -203,35 +174,31 @@ def do_ARGE_cave(slug, caveid, url, area, svxid):
cave = Cave(
unofficial_number=caveid.upper(),
kataster_number=caveid.upper(), # should only set this if all digits
underground_description="ARGE cave.",
survex_file= f"{svxid}.svx",
url=url,
notes=default_note,
areacode=areacode,
)
if cave:
cave.save() # must save to have id before foreign keys work. This is also a ManyToMany key.
# cave.area.add(area)
# cave.save() # crashes entire transaction with foreign key error.
# The 'caves' list page uses the area__short_name to select for the area, so these ARGE caves do not appear.
# message = f" ! {slug:18} ARGE cave url: {url} "
# DataIssue.objects.create(parser="caves", message=message, url=url)
# print(message)
try: # Now create a cave slug ID
CaveSlug.objects.update_or_create(cave=cave, slug=slug, primary=False)
except:
message = f" ! {slug:11s} ARGE CaveSLUG create failure {caveid=} {url=} {area=} {svxid=}"
message = f" ! {slug:11s} ARGE CaveSLUG create failure {caveid=} {url=} {areacode=} {svxid=}"
DataIssue.objects.create(parser="caves", message=message)
print(message)
else:
message = f" ! {slug:11s} ARGE cave create failure {caveid=} {url=} {area=} {svxid=}"
message = f" ! {slug:11s} ARGE cave create failure {caveid=} {url=} {areacode=} {svxid=}"
DataIssue.objects.create(parser="caves", message=message)
print(message)
return None
return cave
def do_pending_cave(slug, caveid, url, area, msg=None):
def do_pending_cave(slug, caveid, url, areacode, msg=None):
"""
default for a PENDING cave, should be overwritten in the db later if a real cave of the same name exists
in expoweb/cave_data/1623-"k".html
@ -250,16 +217,16 @@ def do_pending_cave(slug, caveid, url, area, msg=None):
else:
id = Path(k)
survex_file = f"caves-{area.short_name}/{id}/{id}.svx"
survex_file = f"caves-{areacode}/{id}/{id}.svx"
if Path(settings.SURVEX_DATA, survex_file).is_file():
return survex_file
else:
survex_file = f"caves-{area.short_name}/{id}.svx"
survex_file = f"caves-{areacode}/{id}.svx"
if Path(settings.SURVEX_DATA, survex_file).is_file():
return survex_file
survex_file = ""
d = Path(settings.SURVEX_DATA, f"caves-{area.short_name}/{id}")
d = Path(settings.SURVEX_DATA, f"caves-{areacode}/{id}")
if d.is_dir():
prime_suspect = ""
dir = d.iterdir()
@ -334,11 +301,10 @@ def do_pending_cave(slug, caveid, url, area, msg=None):
survex_file=survex_file,
url=url,
notes=default_note,
areacode=areacode,
)
if cave:
cave.save() # must save to have id before foreign keys work. This is also a ManyToMany key.
cave.area.add(area)
cave.save()
message = f" ! {slug:18} Pending cave write-up url: {url} - {msg}"
DataIssue.objects.create(parser="caves", message=message, url=url)
print(message)
@ -555,7 +521,7 @@ def read_cave(filename, cave=None):
# print(f"! Entrance {eslug}")
if eslug.endswith('a b'):
message = f' - Entrance has weird name slug:"{eslug}" cave:"{cave}" caveslug:"{slug}" filename:"cave_data/{filename}"'
DataIssue.objects.create(parser="xEntrances", message=message, url=f"{cave.area}/{cave.area}-{cave.url}_cave_edit/")
DataIssue.objects.create(parser="xEntrances", message=message, url=f"{cave.areacode}/{cave.areacode}-{cave.url}_cave_edit/")
# print(message)
letter = getXML(e, "letter", maxItems=1, context=context)[0]
@ -566,7 +532,7 @@ def read_cave(filename, cave=None):
if letter.lower() not in list(string.ascii_lowercase):
letter = "x"
message = f"- Warning - Empty 'letter' field for '{eslug}' in multiple-entrance cave '{cave}', setting to {letter}."
DataIssue.objects.create(parser="entrances", message=message, url=f"{cave.area}/{cave.area}-{cave.url}_cave_edit/")
DataIssue.objects.create(parser="entrances", message=message, url=f"{cave.areacode}/{cave.areacode}-{cave.url}_cave_edit/")
print(message)
if len(entrances) == 1 and not eslug: # may be empty: <entranceslug></entranceslug>
@ -584,13 +550,13 @@ def read_cave(filename, cave=None):
entrances_xslug[eslug] = entrance
except:
message = f"! Fail entrance loading {eslug} /entrance_data/{eslug} file does not exist or loading it failed."
DataIssue.objects.create(parser="entrances", message=message, url=f"{cave.area}/{cave.area}-{cave.url}_cave_edit/")
DataIssue.objects.create(parser="entrances", message=message, url=f"{cave.areacode}/{cave.areacode}-{cave.url}_cave_edit/")
print(message)
return
if eslug != f"{entrance}":
message = f"eslug {eslug} using different entrance {entrance} to set CaveAndEntrance"
DataIssue.objects.create(parser="xEntrances", message=message, url=f"{cave.area}/{cave.area}-{cave.url}_cave_edit/")
DataIssue.objects.create(parser="xEntrances", message=message, url=f"{cave.areacode}/{cave.areacode}-{cave.url}_cave_edit/")
print(message)
try:
# this fails if there is not an unambiguous letter set.
@ -728,27 +694,13 @@ def read_cave(filename, cave=None):
cave.description_file=description_file[0]
cave.url=url[0]
areas = getXML(cavecontents, "area", context=context)
cave.area.clear() # Deletes all links to areas in db
areas = getXML(cavecontents, "area", context=context) # can be multiple <area> tags
for area_slug in areas:
if area_slug in areas_xslug:
newArea = areas_xslug[area_slug]
if area_slug in ["1623", "1624", "1626", "1627"]: # ignore sub areas which are in another <area> tag
cave.areacode = area_slug
else:
areas_new = Area.objects.filter(short_name=area_slug)
if areas_new:
newArea = areas_new[0] # just the first one we find, but we are going to clean up Areas anyway
else:
# Area not seen before. SHould not happen with manual edit
if manual_edit:
message = f" ! Cave edit failure due to unrecognised Area: {area_slug[0]}, skipping this field edit. "
DataIssue.objects.create(parser="caves", message=message)
print(message)
# super value is highly dodgy
newArea = Area(short_name=area_slug, super=Area.objects.get(short_name="1623"))
newArea.save()
areas_xslug[area_slug] = newArea
cave.area.add(newArea)
cave.subarea = area_slug
entrances = getXML(cavecontents, "entrance", context=context)
do_entrances()
# print(f"- {entrances_xslug=}")
@ -834,8 +786,6 @@ def readcaves():
#DataIssue.objects.filter(parser="xEntrances").delete()
with transaction.atomic():
area = get_area("1623")
print(" - Reading Entrances from entrance descriptions xml files")
for filename in next(os.walk(ENTRANCEDESCRIPTIONS))[2]: # Should be a better way of getting a list of files
read_entrance(filename)
@ -860,19 +810,18 @@ def readcaves():
for k in pending:
if k[0:3] == "162":
areanum = k[0:4]
areacode = k[0:4]
number = k[5:]
url = f"{areanum}/{k[5:]}.html" # Note we are appending the .htm to allow for offline websites
url = f"{areacode}/{k[5:]}.html" # Note we are appending the .htm to allow for offline websites
else:
areanum = "1623"
areacode = "1623"
number = k
url = f"1623/{k}"
area = get_area(areanum)
try:
do_pending_cave(k, number, url, area)
do_pending_cave(k, number, url, areacode)
except:
message = f" ! Error. Cannot create pending cave, pending-id:{k} in area {areanum}"
message = f" ! Error. Cannot create pending cave, pending-id:{k} in area {areacode}"
DataIssue.objects.create(parser="caves", message=message)
print(message)
raise

View File

@ -57,9 +57,9 @@ class MapLocations(object):
print(message)
continue # skip this entrance
try:
areaName = k.getArea().short_name
areaName = k.areacode
except:
message = f" ! Failed to get Area on cave '{k}' linked to Entrance:{ent.name} from:{ent.filename} best:{ent.best_station()}"
message = f" ! Failed to get areacode on cave '{k}' linked to Entrance:{ent.name} from:{ent.filename} best:{ent.best_station()}"
stash_data_issue(parser="positions", message=message)
print(message)
store_data_issues()

View File

@ -2169,7 +2169,7 @@ def FindAndLoadSurvex():
# These exceptions WILL be parsed if the are *included by any file which is not excepted
unseensroot = re.sub(r"\.svx$", "", UNSEENS)
excpts = ["surface/terrain", "kataster/kataster-boundaries", "gpx/gpx_publish/essentials", "template", "docs", "deprecated", "subsections", "1623-and-1626-no-schoenberg-hs", "1623-and-1624-and-1626-and-1627", "1623-and-1626",unseensroot]
excpts = ["surface/terrain", "kataster/kataster-boundaries", "gpx/gpx_publish/essentials", "template", "docs", "deprecated", "subsections", "1623-and-1626-no-schoenberg-hs", "1623-and-1624-and-1626-and-1627", "1623-and-1626", "dummy_file", unseensroot]
removals = set()
for x in unseens:
for o in excpts:

View File

@ -43,9 +43,7 @@
<table id="cavepage">
<tr>
<th id="kat_no"><!-- why is this not showing unofficial_number??-->
{% if cave.area.all %}
{{ cave.area.all.0.kat_area }} /
{% endif %}
{{ cave.areacode}} /
{% if cave.kataster_number %}
{{ cave.kataster_number|safe }}
{% if cave.entrancelist %}
@ -200,14 +198,14 @@
<a class="editlink" href="{% if local %}https://expo.survex.com{% endif %}{% url "newentrance" cave.url_parent cave.slug %}">New Entrance</a>
</div>
<h2>Survex File(s)</h2>
All <a href="/survexfile/{{cave.kataster_number}}">survexfiles</a> for this cave <br />
All <a href="/survexfile/{{ cave.areacode }}-{{cave.kataster_number}}">survexfiles</a> for this cave <br />
{% if cave.survex_file %}
Primary <a href="/survexfile/{{cave.survex_file}}">survex file</a> for this cave
<br>
Download .3d file <a href="{% url "cave3d" cave %}">caves-{{ cave.area.all.0.kat_area }}/{{cave.kataster_number}}/{{svx3d}}.3d</a>
Download .3d file <a href="{% url "cave3d" cave %}">caves-{{ cave.areacode }}/{{cave.kataster_number}}/{{svx3d}}.3d</a>
<br>
cave ID '{{cave.reference}}'<br>
cave survex path '{{ cave.area.all.0.kat_area }}/{{cave.kataster_number}}'
cave survex path '{{ cave.areacode }}/{{cave.kataster_number}}'
<div id='scene'></div>
{% endif %}
{% endblock content %}

View File

@ -12,7 +12,7 @@
<tr><th>Cave</th><th>Cave primary</th><th>f.primary</th><th>f.path</th></tr>
{% for f in survexfiles %}
<tr>
<td><a href="/cave/{{f.cave}}">{{f.cave}}</a></td>
<td><a href="/cave/{{f.cave}}">{{f.cave}}</a> {{f.cave.areacode}}{f.cave.subarea}}</td>
<td>{{f.cave.survex_file}}</td>
<td> {{f.primary}}.svx</td>
<td><span {% if f.pathbad %} style="color:red" {% endif %}><a href="/survexfile/{{f.path}}.svx">{{f.path}}.svx</a></span></td>