diff --git a/core/forms.py b/core/forms.py index 5cbbe95..1c2c299 100644 --- a/core/forms.py +++ b/core/forms.py @@ -210,7 +210,7 @@ class EntranceLetterForm(ModelForm): """ # This only needs to be required=True for the second and subsequent entrances, not the first. Tricky. - entranceletter = forms.CharField(required=True, widget=forms.TextInput(attrs={"size": "2"})) + entranceletter = forms.CharField(required=False, widget=forms.TextInput(attrs={"size": "2"})) class Meta: model = CaveAndEntrance diff --git a/core/models/caves.py b/core/models/caves.py index ab8414b..01ec4a3 100644 --- a/core/models/caves.py +++ b/core/models/caves.py @@ -198,22 +198,13 @@ class Cave(TroggleModel): res += "–" + prevR return res - def writeDataFile(self): - """Seems to be a duplicate of file_output() ?! - REFACTOR""" - filepath = os.path.join(settings.CAVEDESCRIPTIONS, self.filename) - - t = loader.get_template("dataformat/cave.xml") - now = datetime.now(timezone.utc) - print(now) - c = dict({"cave": self, "date": now}) - u = t.render(c) - writetrogglefile(filepath, u) - return - def file_output(self): """This produces the content which wll be re-saved as the cave_data html file. """ + if not self.filename: + self.filename = self.slug() + ".html" + self.save() + filepath = Path(settings.CAVEDESCRIPTIONS, self.filename) t = loader.get_template("dataformat/cave.xml") @@ -221,6 +212,11 @@ class Cave(TroggleModel): c = dict({"cave": self, "date": now}) content = t.render(c) return (filepath, content, "utf8") + + def writeDataFile(self): + filepath, content, coding = self.file_output() + writetrogglefile(filepath, content) + return class Entrance(TroggleModel): MARKING_CHOICES = ( @@ -388,6 +384,9 @@ class Entrance(TroggleModel): return Path(settings.ENTRANCEDESCRIPTIONS, self.filename) def file_output(self): + if not self.filename: + self.filename = self.slug + ".html" + self.save() filepath = Path(os.path.join(settings.ENTRANCEDESCRIPTIONS, self.filename)) t = loader.get_template("dataformat/entrance.xml") @@ -397,13 +396,8 @@ class Entrance(TroggleModel): return (filepath, content, "utf8") def writeDataFile(self): - filepath = os.path.join(settings.ENTRANCEDESCRIPTIONS, self.filename) - - t = loader.get_template("dataformat/entrance.xml") - now = datetime.now(timezone.utc) - c = dict({"entrance": self, "date": now}) - u = t.render(c) - writetrogglefile(filepath, u) + filepath, content, coding = self.file_output() + writetrogglefile(filepath, content) return def url_parent(self): diff --git a/core/utils.py b/core/utils.py index 0e0dfcc..7111c0c 100644 --- a/core/utils.py +++ b/core/utils.py @@ -152,6 +152,8 @@ def write_and_commit(files, message): # GIT see also core/views/uploads.py dwgupload() # GIT see also core/views/expo.py editexpopage() os.makedirs(os.path.dirname(filepath), exist_ok = True) + if filepath.is_dir(): + return False if encoding: mode = "w" kwargs = {"encoding": encoding} @@ -160,7 +162,7 @@ def write_and_commit(files, message): kwargs = {} try: with open(filepath, mode, **kwargs) as f: - print(f"WRITING {cwd}---{filename} ") + print(f"WRITING {cwd}/{filename} ") f.write(content) except PermissionError: raise WriteAndCommitError( @@ -226,6 +228,7 @@ def write_and_commit(files, message): raise WriteAndCommitError( f"CANNOT git on server for this file {filename}. Subprocess error. Edits not saved.\nAsk a nerd to fix this." ) + return True class WriteAndCommitError(Exception): diff --git a/core/views/caves.py b/core/views/caves.py index adb3398..9d2512d 100644 --- a/core/views/caves.py +++ b/core/views/caves.py @@ -475,6 +475,30 @@ def edit_entrance(request, path="", caveslug=None, entslug=None): GET RID of all this entranceletter stuff. Far too overcomplexified. We don't need it. Just the entrance slug is fine, then check uniqueness. """ + def check_new_slugname_ok(slug, letter): + """In Nov.2023 it is possible to create a 2nd entrance and not set an entrance letter, + which leads to a constraint uniqueness crash. FIX THIS. + The letter may be set to an existing letter, OR it may be unset, but there may + be an existing unlettered single entrance. Both of these will crash unless fixed. + """ + slugname = f"{slug}{letter}" + nents = Entrance.objects.filter(slug=slugname).count() + print(f"NUM ents {slugname=} => {nents}") + if nents == 0: + # looks good, but we need to check the CaveaAndEntrance object too + e = entrance #Entrance.objects.get(slug=slugname) # does not exist yet! + gcl = GetCaveLookup() + c = gcl[slug] + nce = CaveAndEntrance.objects.filter(cave=c, entrance=e).count() + if nce == 0 : + return slugname, letter + + # That entrance already exists, or the CE does, OK.. do recursive call, starting at "b" + if letter =="": + return check_new_slugname_ok(slug, "b") + else: + nextletter = chr(ord(letter)+1) + return check_new_slugname_ok(slug, nextletter) try: cave = Cave.objects.get(caveslug__slug=caveslug) @@ -495,42 +519,60 @@ def edit_entrance(request, path="", caveslug=None, entslug=None): caveAndEntrance = CaveAndEntrance.objects.get(entrance=entrance, cave=cave) entlettereditable = False else: - caveAndEntrance = CaveAndEntrance(cave=cave, entrance=Entrance()) + caveAndEntrance = CaveAndEntrance(cave=cave, entrance=Entrance()) # creates a new Entrance object as well as a new CE object entlettereditable = True - if caveAndEntrance.entranceletter == "" and cave.entrances().count() > 0 : + ce = caveAndEntrance + if ce.entranceletter == "" and cave.entrances().count() > 0 : # this should not be blank on a multiple-entrance cave # but it doesn't trigger the entrance letter form unless entletter has a value - entlettereditable = True + entlettereditable = True # but the user has to remember to actually set it... print(f"{entlettereditable=}") - # if the entletter is no editable, then the entletterform does not appear and so is always invalid. + # if the entletter is not editable, then the entletterform does not appear and so is always invalid. if request.POST: print(f"POST Online edit of entrance: '{entrance}' where {cave=}") entform = EntranceForm(request.POST, instance=entrance) + if not entlettereditable: - entranceletter = caveAndEntrance.entranceletter - ce = caveAndEntrance + entranceletter = ce.entranceletter else: - entletterform = EntranceLetterForm(request.POST, instance=caveAndEntrance) + entletterform = EntranceLetterForm(request.POST, instance=ce) if entletterform.is_valid(): ce = entletterform.save(commit=False) entranceletter = entletterform.cleaned_data["entranceletter"] + message = f"- POST valid {caveslug=} {entslug=} {path=} entletterform valid \n {entletterform=}." + print(message) else: - print(f"- POST INVALID {caveslug=} {entslug=} {path=} entletterform invalid.") - return render(request, "errors/badslug.html", {"badslug": "entletter problem in edit_entrances()"}) - # if entform.is_valid() and entletterform.is_valid(): - if entform.is_valid(): + # maybe this doesn't matter? It just means entranceletter unset ? + # probably because 'Cave and entrance with this Cave and Entranceletter already exists.' + message = f"- POST INVALID {caveslug=} {entslug=} {path=} entletterform invalid \n{entletterform.errors=}\n{entletterform=}." + print(message) + # if entletterform.errors: + # for field in entletterform: + # for error in field.errors: + # print(f"ERR {field=} {error=}") + # return render(request, "errors/generic.html", {"message": message}) + entranceletter="" + + if not entform.is_valid(): + message = f"- POST INVALID {caveslug=} {entslug=} {path=} entform valid:{entform.is_valid()} entletterform valid:{entletterform.is_valid()}" entrance = entform.save(commit=False) + print(message) + return render(request, "errors/generic.html", {"message": message}) + else: print(f"- POST {caveslug=} {entslug=} {entranceletter=} {path=}") if entslug is None: + # we are creating a new entrance if entranceletter: - slugname = cave.slug() + entranceletter - print(f"- POST letter {entranceletter=}") + slugname, letter = check_new_slugname_ok(cave.slug(), entranceletter) else: - slugname = cave.slug() + slugname, letter = check_new_slugname_ok(cave.slug(), "") + ce.entranceletter = letter + entrance = ce.entrance # the one we created earlier + entrance.slug = slugname entrance.cached_primary_slug = slugname entrance.filename = slugname + ".html" @@ -541,41 +583,48 @@ def edit_entrance(request, path="", caveslug=None, entslug=None): try: entrance.save() except: - # fails with uniqueness constraint failure. Which is on CaveAndEntrance, not just on entrance, which is bizarre. + # fails with uniqueness constraint failure. Which is on CaveAndEntrance, not just on entrance, + # which is confusing to a user who is just editing an Entrance. + # Can happen when user specifies an existing letter! (or none, when they should set one) print(f"SAVE EXCEPTION FAIL {entrance=}") print(f"CAVE {cave}") for ce in cave.entrances(): print(f"CAVE:{ce.cave} - ENT:{ce.entrance} - LETTER:'{ce.entranceletter}'") raise ce.entrance = entrance + # try not to do this: + # UNIQUE constraint failed: core_caveandentrance.cave_id, core_caveandentrance.entranceletter ce.save() entrance_file = entrance.file_output() cave_file = cave.file_output() - print(f"- POST WRITE letter: '{ce}' {entrance=}") - write_and_commit([entrance_file, cave_file], f"Online edit of entrance {entrance.slug}") - return HttpResponseRedirect("/" + cave.url) - else: # one of the forms is not valid - print(f"- POST INVALID {caveslug=} {entslug=} {path=} entform valid:{entform.is_valid()} entletterform valid:{entletterform.is_valid()}") + if write_and_commit([entrance_file, cave_file], f"Online edit of entrance {entrance.slug}"): + return HttpResponseRedirect("/" + cave.url) + else: + efilepath, econtent, eencoding = entrance_file + cfilepath, ccontent, cencoding = cave_file + message = f"- FAIL write_and_commit \n entr:'{efilepath}'\n cave:'{cfilepath}'" + print(message) + return render(request, "errors/generic.html", {"message": message}) else: # GET the page, not POST, or if either of the forms were invalid when POSTed entletterform = None entletter = "" - print(f"ENTRANCE in db: entranceletter = '{caveAndEntrance.entranceletter}'") + print(f"ENTRANCE in db: entranceletter = '{ce.entranceletter}'") if entrance: # re-read entrance data from file. filename = str(entrance.slug +".html") ent = read_entrance(filename, ent=entrance) - print(f"ENTRANCE from file: entranceletter = '{caveAndEntrance.entranceletter}'") + print(f"ENTRANCE from file: entranceletter = '{ce.entranceletter}'") entform = EntranceForm(instance=entrance) if entslug is None: entletterform = EntranceLetterForm() # print(f" Getting entletter from EntranceLetterForm") else: - entletter = caveAndEntrance.entranceletter + entletter = ce.entranceletter if entletter == "": entletterform = EntranceLetterForm() print(f" Blank value: getting entletter from EntranceLetterForm") @@ -619,7 +668,14 @@ def cave_debug(request): {"ents": ents}, ) - +def caveslist(request): + caves = Cave.objects.all() + print("CAVESLIST") + return render( + request, + "caveslist.html", + {"caves": caves}, + ) def get_entrances(request, caveslug): try: cave = Cave.objects.get(caveslug__slug=caveslug) diff --git a/templates/caveindex.html b/templates/caveindex.html index 135fcc8..a07350e 100644 --- a/templates/caveindex.html +++ b/templates/caveindex.html @@ -46,8 +46,8 @@ Black triangle against a name indicat {% if cave.no_location %}{% endif %} {% else %} {% endif %} - {% else %} - {% endif %} + {% else %} + {% endif %} {% if cave.survex_file %}{% else %}*{% endif %} diff --git a/templates/caveslist.html b/templates/caveslist.html new file mode 100644 index 0000000..d950fe4 --- /dev/null +++ b/templates/caveslist.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block title %}Caves List page + +{% endblock %} + +{% block content %} + +

Caves

+

Debug.. gets edited to whatever is needed by programmer.. + + + + + + + +{% for c in caves %} + + + +{% endfor %} +
Cavecave filename
+ {{c}} + + {{c.filename}} +
+ + +{% endblock %} diff --git a/templates/errors/generic.html b/templates/errors/generic.html index 9f345c0..272a2ee 100644 --- a/templates/errors/generic.html +++ b/templates/errors/generic.html @@ -7,11 +7,11 @@

Troggle Error

- +
-

There has been an error. This is either bad data or a bug in the software.

+

There has been an error. This is either bad data troggle can't cope with (yet), or a bug in the software.

diff --git a/urls.py b/urls.py index 7d02627..db3a8a8 100644 --- a/urls.py +++ b/urls.py @@ -6,7 +6,7 @@ from django.conf.urls.static import static from troggle.core.views import statistics, survex from troggle.core.views.auth import expologin, expologout -from troggle.core.views.caves import (cave3d, caveindex, entranceindex, +from troggle.core.views.caves import (cave3d, caveindex, entranceindex, caveslist, cavepage, caveslugfwd, cavepagefwd, caveQMs, edit_cave, cave_debug, edit_entrance, get_entrances, qm, expo_kml, expo_kmz) from troggle.core.views.drawings import dwgallfiles, dwgfilesingle @@ -94,8 +94,9 @@ trogglepatterns = [ re_path(r'^caves$', caveindex, name="caveindex"), re_path(r'^indxal.htm$', caveindex, name="caveindex"), # ~420 hrefs to this url in expoweb files re_path(r'^people/?$', notablepersons, name="notablepersons"), - - re_path(r'^entrances$', entranceindex, name="entranceindex"), + path('caveslist', caveslist, name="caveslist"), + + path('entrances', entranceindex, name="entranceindex"), re_path(r'^admin/doc/', include('django.contrib.admindocs.urls')), # needs docutils Python module (http://docutils.sf.net/). re_path(r'^admin/', admin.site.urls), # includes admin login & logout urls & /admin/jsi18n/