diff --git a/core/models/logbooks.py b/core/models/logbooks.py
index 3152a49a4..4d35d6e13 100644
--- a/core/models/logbooks.py
+++ b/core/models/logbooks.py
@@ -107,7 +107,31 @@ class LogbookEntry(TroggleModel):
index = index % mx
return index
-def writelogbook(year, filename):
+READ_THIS="""
+"""
+def writelogbook(year, filename, generate_on_view=False):
"""Writes all the database logbook entries into a new logbook.html file
"""
current_expedition = Expedition.objects.get(year=year)
@@ -140,6 +164,9 @@ def writelogbook(year, filename):
except:
print(" ! Very Bad Error opening " + endpath)
raise
+ if generate_on_view:
+ endmatter += READ_THIS
+
except:
print(" ! FAIL endpath " + endpath)
raise
diff --git a/core/utils.py b/core/utils.py
index 408198db0..0954338f5 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -405,8 +405,11 @@ def git_commit(cwd, message, editor, commands=[]):
print(f"..{message=}\n..{editor=}")
cmd_commit = [git, "commit", "-m", message, "--author", f"{editor}"]
commands.append(cmd_commit)
-
- cp_commit = subprocess.run(cmd_commit, cwd=cwd, capture_output=True, text=True)
+ print(commands)
+ try:
+ cp_commit = subprocess.run(cmd_commit, cwd=cwd, capture_output=True, text=True)
+ except Exception as e:
+ print(e)
# This produces return code = 1 if it commits OK, but when the local repo still needs to be pushed to origin/repo
# which will be the case when running a test troggle system on a development machine
diff --git a/core/views/logbook_edit.py b/core/views/logbook_edit.py
index 457fdbc0c..18e3c6981 100644
--- a/core/views/logbook_edit.py
+++ b/core/views/logbook_edit.py
@@ -25,6 +25,7 @@ from troggle.core.utils import (
write_and_commit,
wrap_text,
)
+from troggle.core.views.logbooks import write_entries_json
from troggle.parsers.people import GetPersonExpeditionNameLookup, known_foreigner
# from databaseReset import reinit_db # don't do this. databaseRest runs code *at import time*
@@ -301,70 +302,76 @@ def logbookedit(request, year=None, slug=None):
return render(request, "errors/generic.html", {"message": message})
store_edited_entry_into_database(date, place, title, entry, others, author, tu, slug)
+ # Successful POST so save to fiesystem
+ json_entries_dir = settings.EXPOWEB / "years" / year / settings.JSON_LOG_ENTRIES
+ if json_entries_dir.is_dir(): # only 2025 currently, or the current expo
+ print(f"- Rewriting JUST this edited logbook entry to a JSON file. ")
+ this_entry = LogbookEntry.objects.get(slug=slug)
+ write_entries_json([this_entry], year, editor)
+ else:
+ print(f"- Rewriting the entire {year} logbook to disc ")
+ filename= "logbook.html"
+ try:
+ print(f" - Logbook for {year} to be exported and written out.")
+ writelogbook(year, filename) # uses a template, not the code fragment below which is just a visible hint to logged on user
+ except:
+ message = f'! - Logbook saving failed - \n!! Permissions failure ?! on attempting to save file "logbook.html"'
+ print(message)
+ return render(request, "errors/generic.html", {"message": message})
+
+
+ # So save to database and then write out whole new logbook.html file
- print(f"- Rewriting the entire {year} logbook to disc ")
- filename= "logbook.html"
- try:
- print(f" - Logbook for {year} to be exported and written out.")
- writelogbook(year, filename) # uses a template, not the code fragment below which is just a visible hint to logged on user
- except:
- message = f'! - Logbook saving failed - \n!! Permissions failure ?! on attempting to save file "logbook.html"'
- print(message)
- return render(request, "errors/generic.html", {"message": message})
-
- # Code fragment illustration - not actually what gets saved to database
- output = f'''
+ # We do author validation on the form as displayed by GET, not at the moment of POST.
+ # If we had JS validation then we could be more timely.
+ dirpath = Path(settings.EXPOWEB) / "years" / str(year)
+ contents_path = dirpath / filename
+ commit_msg = f"Online edit of logbookentry {slug}"
+ add_commit(contents_path, commit_msg, editor)
+
+ # This does not change the URL in the browser, so despite a new slug being created,
+ # the next time this code is run it thinks a new slug needs to be created. So we should
+ # actually redirect to a new URL (an edit not a create) not simply return a render object.
+ # logbookedit/2022-08-21a
+
+ # HOWEVER by doing a redirect rather than returning a rendered page, we lose all the
+ # error settings e.g dateflag and authroflag so the user gets no feedback about bad data entered.
+ # so we need to pass the flags explicitly in the url and then extract them from the request in the GET bit. sigh.
+ response = HttpResponseRedirect(f"/logbookedit/{slug}?dateflag={dateflag}&authorflag={authorflag}")
+ response.set_cookie('editor_id', editor, max_age=get_cookie_max_age(request)) # cookie expires after get_cookie_max_age(request) seconds
+ return response
+
+ # Do the redirect instead of this:
+
+ # Code fragment illustration - not actually what gets saved to database
+ # output = f'''
-
{date}
-{author}, {others}
-{place} - {title}
+# {date}
+# {author}, {others}
+# {place} - {title}
-{entry}
+# {entry}
-T/U {tu} hrs
-
+# T/U {tu} hrs
+#
-'''
- # Successful POST
- # So save to database and then write out whole new logbook.html file
-
- # We do author validation on the form as displayed by GET, not at the moment of POST.
- # If we had JS validation then we could be more timely.
- dirpath = Path(settings.EXPOWEB) / "years" / str(year)
- contents_path = dirpath / filename
- commit_msg = f"Online edit of logbookentry {slug}"
- add_commit(contents_path, commit_msg, editor)
-
- # This does not change the URL in the browser, so despite a new slug being created,
- # the next time this code is run it thinks a new slug needs to be created. So we should
- # actually redirect to a new URL (an edit not a create) not simply return a render object.
- # logbookedit/2022-08-21a
-
- # HOWEVER by doing a redirect rather than returning a rendered page, we lose all the
- # error settings e.g dateflag and authroflag so the user gets no feedback about bad data entered.
- # so we need to pass the flags explicitly in the url and then extract them from the request in the GET bit. sigh.
- response = HttpResponseRedirect(f"/logbookedit/{slug}?dateflag={dateflag}&authorflag={authorflag}")
- response.set_cookie('editor_id', editor, max_age=get_cookie_max_age(request)) # cookie expires after get_cookie_max_age(request) seconds
- return response
-
- # Do the redirect instead of this:
- # return render(
- # request,
- # "logbookform.html",
- # {
- # "form": form,
- # "year": year,
- # "date": date, "dateflag": dateflag,
- # "author": author, "authorflag": authorflag,
- # "others": others,
- # "place": place,
- # "title": title,
- # "tu": tu,
- # "entry": entry,
- # "output": output,
- # "slug": slug,
- # },
- # )
+# ''' # return render(
+ # request,
+ # "logbookform.html",
+ # {
+ # "form": form,
+ # "year": year,
+ # "date": date, "dateflag": dateflag,
+ # "author": author, "authorflag": authorflag,
+ # "others": others,
+ # "place": place,
+ # "title": title,
+ # "tu": tu,
+ # "entry": entry,
+ # "output": output,
+ # "slug": slug,
+ # },
+ # )
# GET here. Does not fall-through from the POST section.
else:
diff --git a/core/views/logbooks.py b/core/views/logbooks.py
index 5c85206ce..8aed855d3 100644
--- a/core/views/logbooks.py
+++ b/core/views/logbooks.py
@@ -30,8 +30,6 @@ todo = """- Fix the get_person_chronology() display bug.
- Fix id= value preservation on editing
"""
-LOGBOOK_ENTRIES = "log_entries" # directory name
-
def notablepersons(request):
def notabilitykey(person):
return person.notability()
@@ -247,7 +245,7 @@ def logentrydelete(request, year):
"""This only gets called by a POST from the logreport page
This function is dedicated to James Waite who managed to make so many duplicate logbook entries
- that we needed a sopecial mechanism to delete them.
+ that we needed a special mechanism to delete them.
"""
for i in request.POST:
print(f" - '{i}' {request.POST[i]}")
@@ -261,6 +259,7 @@ def logentrydelete(request, year):
filename= "logbook.html"
try:
writelogbook(year, filename) # uses a template
+
except:
message = f'! - Logbook saving failed - \n!! Permissions failure ?! on attempting to save file "logbook.html"'
print(message)
@@ -368,6 +367,32 @@ def logbookentry(request, date, slug):
print(msg)
return render(request, "errors/generic.html", {"message": msg})
+def logbookfile(request, year):
+ """This was just a link to logbook.html, an ordinary HTML file handbook-style, but in Nov.2025 we
+ changed to have individual JSON files per entry. So this "whole logbook" is now only
+ re-generated when anyone want to see it.
+ """
+ json_entries_dir = settings.EXPOWEB / "years" / year / settings.JSON_LOG_ENTRIES
+ exp = Expedition.objects.get(year=year)
+ if json_entries_dir.is_dir(): # only 2025 currently, or the current expo
+ # Re-generate the logbook.html because it won't have been done by
+ # indovidual entry edits for this year
+
+ contents_path = settings.EXPOWEB / "years" / year / exp.logbookfile
+ try:
+ print(f" - Logbook for {year} to be exported and written out.")
+ writelogbook(year, exp.logbookfile, generate_on_view=True) # uses a template
+ commit_msg = f"Request to see whole logbook triggers re-exporting."
+ editor = get_editor(request)
+ add_commit(contents_path, commit_msg, editor)
+ except:
+ message = f'! - Logbook saving to file from database failed - \n!! Permissions failure ?! on attempting to save file {contents_path}'
+ print(message)
+ raise # got a one-off failure here, we need to know why..
+ return render(request, "errors/generic.html", {"message": message})
+
+ return redirect(f"/years/{year}/{exp.logbookfile}")
+
def get_people(request, expeditionslug):
exp = Expedition.objects.get(year=expeditionslug)
@@ -385,11 +410,11 @@ def logbook_entries_export(request, year):
entries = get_entries(year)
editor = get_editor(request)
- write_entries(entries, year, editor)
+ write_entries_json(entries, year, editor)
return redirect(f"/logreport/{year}")
-def write_entries(entries, year, editor):
+def write_entries_json(entries, year, editor):
"""Exports logentries from the live database to JSON files.
entries - a list, use a list of one member if writing a single entry
@@ -438,13 +463,16 @@ def write_entries(entries, year, editor):
expedition_data = model_to_dict(le.expedition, fields=['year', 'name'])
- entrydict = model_to_dict(le, fields=('slug', 'date', 'title', 'cave', 'place', 'other_people', 'time_underground', 'text'))
+ entrydict = model_to_dict(le, fields=('slug', 'date', 'title', 'place', 'other_people', 'time_underground', 'text'))
entrydict['author'] = author_data
entrydict['trippersons'] = participants
entrydict['expedition'] = expedition_data
+ if le.cave:
+ cave_data = model_to_dict(le.cave, fields=['areacode', 'kataster_number', 'unofficial_number'])
+ entrydict['cave'] = cave_data
return entrydict
- dirpath = settings.EXPOWEB / "years" / year / LOGBOOK_ENTRIES
+ dirpath = settings.EXPOWEB / "years" / year / settings.JSON_LOG_ENTRIES
for le in entries:
# filename = f"{le.slug}-{le.pk:03}.json"
diff --git a/parsers/imports.py b/parsers/imports.py
index d553941e4..57a73d59b 100644
--- a/parsers/imports.py
+++ b/parsers/imports.py
@@ -43,7 +43,7 @@ def import_logbooks():
with transaction.atomic():
troggle.parsers.logbooks.LoadLogbooks()
-def import_logbook(year=2024):
+def import_logbook(year=2025):
print(f"-- Importing Logbook {year}")
with transaction.atomic():
troggle.parsers.logbooks.LoadLogbook(year)
diff --git a/parsers/logbooks.py b/parsers/logbooks.py
index e959767b8..ba98db7d3 100644
--- a/parsers/logbooks.py
+++ b/parsers/logbooks.py
@@ -14,7 +14,7 @@ from django.template.defaultfilters import slugify
from parsers.people import GetPersonExpeditionNameLookup, known_foreigner, load_people_expos
from typing import Any, List, Tuple
-from troggle.core.models.caves import GetCaveLookup
+from troggle.core.models.caves import GetCaveLookup, Cave
from troggle.core.models.logbooks import LogbookEntry, PersonLogEntry
from troggle.core.models.troggle import DataIssue, Expedition, Person, PersonExpedition
from troggle.core.utils import alphabet_suffix, get_process_memory, unique_slug
@@ -248,7 +248,7 @@ def tidy_trip_persons(trippeople, title, expedition, logtime_underground, tid):
return trippersons, author, guests
def tidy_trip_cave(place):
- # GetCaveLookup() need to work better. None of this data is *used* though?
+ # GetCaveLookup() need to work better. Used in parsing logbooks: place=>cave
# 'tripcave' is converted to a string doing this, which renders as the cave slug.
lplace = place.lower()
@@ -526,8 +526,6 @@ def parser_html(year, expedition, txt, seq=""):
entry = LogbookEntryData(ldate, place, tripcave, triptitle, tripcontent, trippersons, author, guests, expedition, tu, tid)
logentries.append(entry)
- if str(ldate) == "2025-07-08":
- print(f"PARSED from html\n",entry,"\n")
return logentries
@@ -629,6 +627,7 @@ def parser_blog(year, expedition, txt, sq=""):
logtime_underground = 0
trippersons, author, guests = tidy_trip_persons(trippeople, triptitle, expedition, logtime_underground, tid)
# print(f" - author: {author}")
+
tripcave = tidy_trip_cave(place)
tripcontent = tidy_trip_image_urls(tripcontent, year)
tid = tidy_tid(tid, triptitle, datestamp)
@@ -691,7 +690,7 @@ def parse_logbook_for_expedition(expedition, blog=False):
"""
ldate = datetime.fromisoformat(entrydict["date"]).date()
place = entrydict["place"]
- tripcave = entrydict["cave"]
+ tripcave=None
triptitle = entrydict["title"]
tripcontent = entrydict["text"]
@@ -699,20 +698,31 @@ def parse_logbook_for_expedition(expedition, blog=False):
expedition = Expedition.objects.get(name=entrydict["expedition"]["name"])
tu = entrydict["time_underground"]
tid = entrydict["slug"]
-
+
_author_person = Person.objects.get(slug=entrydict["author"]["slug"])
- _author_nickname = entrydict["author"]["nickname"]
- _author_tu = entrydict["author"]["tu"]
+ # author does not have tu or nickname, that info is on the same person in the participants list
author = PersonExpedition.objects.get(person=_author_person, expedition=expedition) # not a tuple
trippersons = []
for tp in entrydict["trippersons"]:
_person = Person.objects.get(slug=tp["slug"])
_personexpo = PersonExpedition.objects.get(person=_person, expedition=expedition)
+ # if "nickname" not in tp:
+ # tp["nickname"] = ""
+ # if "tu" not in tp:
+ # tp["tu"] = ""
trippersons.append((_personexpo,tp["nickname"],tp["tu"]))
-
+
+ tripcave = tidy_trip_cave(place)
+ if "cave" in entrydict:
+ _cave = Cave.objects.get(areacode=entrydict["cave"]["areacode"],
+ unofficial_number=entrydict["cave"]["unofficial_number"],
+ kataster_number=entrydict["cave"]["kataster_number"])
+ if tripcave != _cave:
+ message = f"! MISMATCH between place and Cave: {tripcave=} {_cave=}"
+ print(message)
+ DataIssue.objects.update_or_create(parser="logbooks", message=message, url=jsonurl)
+
logentry = LogbookEntryData(ldate, place, tripcave, triptitle, tripcontent, trippersons, author, guests, expedition, tu, tid)
- if entrydict["date"] == "2025-07-08":
- print(f"PARSED from JSON\n",logentry,"\n")
return logentry
@@ -734,15 +744,9 @@ def parse_logbook_for_expedition(expedition, blog=False):
expect = ENTRIES[year]
# print(" - Logbook for: " + year)
- json_entries_dir = settings.EXPOWEB / "years" / year / "log_entries"
+ json_entries_dir = settings.EXPOWEB / "years" / year / settings.JSON_LOG_ENTRIES
+
- if json_entries_dir.is_dir():
- print(f" # WARNING year {year} has JSON-encoded logbook entries. Using these instead of the archive .html file.")
- logentries = load_from_json()
-
- logentries = [] # but don't actually use these.
- # check_number_of_entries(logentries)
- # return logentries
if year in LOGBOOK_PARSER_SETTINGS:
@@ -764,6 +768,15 @@ def parse_logbook_for_expedition(expedition, blog=False):
yearfile, parsefunc = BLOG_PARSER_SETTINGS[year]
print(f" - BLOG file {yearfile} using parser {parsefunc}")
else:
+ if json_entries_dir.is_dir():
+ print(f" # WARNING year {year} has JSON-encoded logbook entries. Using these instead of the archive .html file.")
+ logentries = load_from_json()
+
+ check_number_of_entries(logentries)
+ # we know this is being called for a non-blog from the blog=False setting
+ # so we can just skip the rest and return.
+ return logentries
+
lb = Path(expologbase, year, logbookpath.stem + logbookpath.suffix)
if not (lb.is_file()):
message = f" ! Logbook file does not exist (yet): '{lb}'"
diff --git a/settings.py b/settings.py
index 01119d5bd..ef196f9c9 100644
--- a/settings.py
+++ b/settings.py
@@ -84,6 +84,8 @@ ROOT_URLCONF = "troggle.urls" # i.e. troggle/urls.py
LOGOUT_REDIRECT_URL = "/statistics" # see troggle/core/views/auth.py
LOGIN_REDIRECT_URL = "/controlpanel" # see troggle/core/views/auth.py
+JSON_LOG_ENTRIES="log_entries"
+
PASSWORD_RESET_TIMEOUT = 3*60*60 # password reset sends an email. The response is valid for 3 hours
#ACCOUNT_ACTIVATION_DAYS = 3 # this is only if we are using django-registration package
diff --git a/templates/expedition.html b/templates/expedition.html
index 9e48850e4..1fe3420bc 100644
--- a/templates/expedition.html
+++ b/templates/expedition.html
@@ -23,7 +23,7 @@
documentation index for this Expo
wallet completion status for this Expo
alias names for this Expo
- full logbook for this Expo
+ full logbook for this Expo
logbook report for this Expo
new logbook entry for this Expo
diff --git a/templates/logbook2005style.html b/templates/logbook2005style.html
index 971c3e47f..f022d856f 100644
--- a/templates/logbook2005style.html
+++ b/templates/logbook2005style.html
@@ -7,7 +7,11 @@
-
{%for logbook_entry in logbook_entries%}
diff --git a/templates/logbookentry.html b/templates/logbookentry.html
index 4b0b074df..2f7bfb383 100644
--- a/templates/logbookentry.html
+++ b/templates/logbookentry.html
@@ -9,7 +9,7 @@
{{logbookentry.expedition.year}} calendar page
- full logbook
+ full logbook
logbook report
diff --git a/templates/logbookform.html b/templates/logbookform.html
index 28a28c882..0179cb3ba 100644
--- a/templates/logbookform.html
+++ b/templates/logbookform.html
@@ -116,7 +116,7 @@
{% if date %}
Link to this entry {% endif %}
- Full logbook: Logbook {{year}}
+ Full logbook: Logbook {{year}}
{{year}} Logbook report
@@ -128,7 +128,8 @@
{% if output %}
-Click this triangle to see the HTML which has been put into logbook.html, and below that is the rendered logbook entry.
+Click this triangle to see the HTML which has been saved (either put into logbook.html or, for the current expo, put into
+an individual JSON file), and below that is the rendered logbook entry.
{{output}}
diff --git a/templates/logreport.html b/templates/logreport.html
index c13d94131..87b027c62 100644
--- a/templates/logreport.html
+++ b/templates/logreport.html
@@ -21,7 +21,7 @@
(Hover mouse over the date to see the slug for the entry.)
{% if logged_in %}Logged in as expoadmin
Export logbook entries as individual files
-
+This will also export any BLOG entries which have been parsed from, e.g. BLOG_PARSER_SETTINGS
{% endif %}
@@ -83,9 +83,9 @@
documentation index for this Expo
wallet completion status for this Expo
alias names for this Expo
- full logbook for this Expo
+ full logbook for this Expo
new logbook entry for this Expo
-{% if logged_in %}export logbook entries as individual files in /years/{{expedition.year}}/entries/
+{% if logged_in %}export logbook entries as individual files in /years/{{expedition.year}}/entries/. This will also export any BLOG entries which have been parsed from, e.g. BLOG_PARSER_SETTINGS
{% endif %}
diff --git a/urls.py b/urls.py
index d5c287326..2ad4c1e4f 100644
--- a/urls.py
+++ b/urls.py
@@ -55,6 +55,7 @@ from troggle.core.views.logbooks import (
logbookentry,
logentrydelete,
logreport,
+ logbookfile,
notablepersons,
people_ids,
person,
@@ -244,6 +245,7 @@ trogglepatterns = [
path('logbook_entries/', logbook_entries_export, name='logbook_entries_export'),
path('logreport/', logreport, name='logreport'),
path('logentrydelete/', logentrydelete, name='logentrydelete'),
+ path('logbookfile/', logbookfile, name='logbookfile'),
# Internal. editfile.html template uses these internally
re_path(r'^getPeople/(?P.*)', get_people, name = "get_people"),