diff options
-rw-r--r-- | core/forms.py | 12 | ||||
-rw-r--r-- | core/models/caves.py | 46 | ||||
-rw-r--r-- | core/utils.py | 23 | ||||
-rw-r--r-- | core/views/caves.py | 81 | ||||
-rw-r--r-- | templates/cave.html | 4 | ||||
-rw-r--r-- | templates/editentrance.html | 6 | ||||
-rw-r--r-- | templates/menu.html | 2 | ||||
-rw-r--r-- | urls.py | 18 |
8 files changed, 124 insertions, 68 deletions
diff --git a/core/forms.py b/core/forms.py index 9dd01db..6c83a47 100644 --- a/core/forms.py +++ b/core/forms.py @@ -116,11 +116,21 @@ class EntranceForm(ModelForm): CaveAndEntranceFormSet = modelformset_factory(CaveAndEntrance, exclude=('cave',)) class EntranceLetterForm(ModelForm): - '''Can't see what this does at all. called twice from views.caves + '''Form to link entrances to caves, along with an entrance number. + + Nb. The relationship between caves and entrances has historically been a many to many relationship. + With entrances gaining new caves and letters when caves are joined. ''' class Meta: model = CaveAndEntrance exclude = ('cave', 'entrance') + + def full_clean(self): + super(EntranceLetterForm, self).full_clean() + try: + self.instance.validate_unique() + except forms.ValidationError as e: + self._update_errors(e) diff --git a/core/models/caves.py b/core/models/caves.py index f5ae320..9336f5b 100644 --- a/core/models/caves.py +++ b/core/models/caves.py @@ -70,6 +70,10 @@ class CaveAndEntrance(models.Model): cave = models.ForeignKey('Cave',on_delete=models.CASCADE) entrance = models.ForeignKey('Entrance',on_delete=models.CASCADE) entrance_letter = models.CharField(max_length=20,blank=True, null=True) + class Meta: + unique_together = [['cave', 'entrance'], ['cave', 'entrance_letter']] + ordering = ['entrance_letter'] + def __str__(self): return str(self.cave) + str(self.entrance_letter) @@ -121,7 +125,7 @@ class Cave(TroggleModel): def hassurveydata(self): if not self.underground_centre_line: return "No" - if self.survex_file: + if self.survex_filcavee: return "Yes" return "Missing" @@ -153,6 +157,9 @@ class Cave(TroggleModel): #return settings.URL_ROOT + '/cave/' + href + '/' #return urljoin(settings.URL_ROOT, reverse('cave',kwargs={'cave_id':href,})) # WRONG. This produces /cave/161 and should be /1623/161 return Path(settings.URL_ROOT) / self.url # not good Django style.. NEEDS actual URL + + def url_parent(self): + return self.url.rsplit("/", 1)[0] def __str__(self, sep = ": "): return str(self.slug()) @@ -172,7 +179,7 @@ class Cave(TroggleModel): # undated.append(q) # sortedqms = sorted(dated, key=operator.attrgetter('block.date')) # sort by date of survexblock the QM was defined in # orderedqms = sorted(undated, key=operator.attrgetter('expoyear')) # sort by date of expoyear - # return orderedqms + sortedqms # a list, NOT a QuerySet + # return orderedqmcaves + sortedqms # a list, NOT a QuerySet # def new_QM_number(self, year=datetime.date.today().year): @@ -181,7 +188,7 @@ class Cave(TroggleModel): # res=QM.objects.filter(found_by__date__year=year, found_by__cave_slug=self.slug).order_by('-number')[0] # except IndexError: # return 1 - # return res.number+1 + # return res.number+1CaveAndEntrance def kat_area(self): for a in self.area.all(): @@ -233,7 +240,15 @@ class Cave(TroggleModel): u = t.render(c) writetrogglefile(filepath, u) return + + def file_output(self): + filepath = Path(os.path.join(settings.CAVEDESCRIPTIONS, self.filename)) + t = loader.get_template('dataformat/cave.xml') + #c = Context({'cave': self}) + c = dict({'cave': self}) + content = t.render(c) + return (filepath, content, "utf8") def getArea(self): areas = self.area.all() @@ -292,6 +307,9 @@ class Entrance(TroggleModel): url = models.CharField(max_length=200,blank=True, null=True) filename = models.CharField(max_length=200) cached_primary_slug = models.CharField(max_length=200,blank=True, null=True) + + class Meta: + ordering = ['caveandentrance__entrance_letter'] def __str__(self): return str(self.slug()) @@ -370,7 +388,7 @@ class Entrance(TroggleModel): # if ancestor_titles: # res = '/'.join((self.get_root().cave.get_absolute_url(), ancestor_titles, self.title)) # else: - # res = '/'.join((self.get_root().cave.get_absolute_url(), self.title)) + # res = '/'.jocavein((self.get_root().cave.get_absolute_url(), self.title)) # return res res = '/'.join((self.get_root().cave.get_absolute_url(), self.title)) return res @@ -399,6 +417,14 @@ class Entrance(TroggleModel): def get_file_path(self): return Path(settings.ENTRANCEDESCRIPTIONS, self.filename) + + def file_output(self): + filepath = Path(os.path.join(settings.ENTRANCEDESCRIPTIONS, self.filename)) + + t = loader.get_template('dataformat/entrance.xml') + c = dict({'entrance': self}) + content = t.render(c) + return (filepath, content, "utf8") def writeDataFile(self): filepath = os.path.join(settings.ENTRANCEDESCRIPTIONS, self.filename) @@ -408,7 +434,17 @@ class Entrance(TroggleModel): u = t.render(c) writetrogglefile(filepath, u) return - + + def url_parent(self): + if self.url: + return self.url.rsplit("/", 1)[0] + else: + cavelist = self.cavelist() + if len(self.cavelist()) == 1: + return cavelist[0].url_parent() + else: + return "" + class LogbookEntry(TroggleModel): """Single parsed entry from Logbook diff --git a/core/utils.py b/core/utils.py index 5b696dd..228a6ad 100644 --- a/core/utils.py +++ b/core/utils.py @@ -133,7 +133,7 @@ def write_and_commit(files, message): kwargs = {"encoding": encoding} else: mode = "wb" - kwargs = {} + kwargs = {} try: with open(filepath, mode, **kwargs) as f: print(f'WRITING{cwd}---{filename} ') @@ -143,18 +143,21 @@ def write_and_commit(files, message): except PermissionError: raise WriteAndCommitError(f'CANNOT save this file.\nPERMISSIONS incorrectly set on server for this file {filename}. Ask a nerd to fix this.') - cp_add = subprocess.run([git, "add", filename], cwd=cwd, capture_output=True, text=True) - if cp_add.returncode != 0: - msgdata = 'Ask a nerd to fix this.\n\n' + cp_add.stderr + '\n\n' + cp_add.stdout + '\n\nreturn code: ' + str(cp_add.returncode) - raise WriteAndCommitError(f'CANNOT git on server for this file {filename}. Edits saved but not added to git.\n\n' + msgdata) - + cp_diff = subprocess.run([git, "diff", filename], cwd=cwd, capture_output=True, text=True) + if cp_diff.returncode == 0: + cp_add = subprocess.run([git, "add", filename], cwd=cwd, capture_output=True, text=True) + if cp_add.returncode != 0: + msgdata = 'Ask a nerd to fix this.\n\n' + cp_add.stderr + '\n\n' + cp_add.stdout + '\n\nreturn code: ' + str(cp_add.returncode) + raise WriteAndCommitError(f'CANNOT git on server for this file {filename}. Edits saved but not added to git.\n\n' + msgdata) + else: + print("No change %s" % filepah) cp_commit = subprocess.run([git, "commit", "-m", message], cwd=cwd, capture_output=True, text=True) + cp_status = subprocess.run([git, "status"], cwd=cwd, capture_output=True, text=True) # This produces return code = 1 if it commits OK, but when the repo still needs to be pushed to origin/expoweb - if cp_commit.returncode != 0 and cp_commit.stdout != 'nothing to commit, working tree clean': - msgdata = 'Ask a nerd to fix this.\n\n' + cp_commit.stderr + '\n\n' + cp_commit.stdout + '\n\nreturn code: ' + str(cp_commit.returncode) - print(msgdata) + if cp_status.stdout.split("\n")[-2] != 'nothing to commit, working tree clean': + print("FOO: ", cp_status.stdout.split("\n")[-2]) + msgdata = 'Ask a nerd to fix this.\n\n' + cp_status.stderr + '\n\n' + cp_status.stdout + '\n\nreturn code: ' + str(cp_status.returncode) raise WriteAndCommitError(f'Error code with git on server for this file {filename}. Edits saved, added to git, but NOT committed.\n\n' + msgdata) - except subprocess.SubprocessError: raise WriteAndCommitError(f'CANNOT git on server for this file {filename}. Subprocess error. Edits not saved.\nAsk a nerd to fix this.') diff --git a/core/views/caves.py b/core/views/caves.py index 44dbb80..26ac87f 100644 --- a/core/views/caves.py +++ b/core/views/caves.py @@ -19,6 +19,7 @@ from troggle.core.views import expo from troggle.core.models.troggle import Expedition, DataIssue from troggle.core.models.caves import CaveSlug, Cave, CaveAndEntrance, QM, EntranceSlug, Entrance, Area, SurvexStation, GetCaveLookup from troggle.core.forms import CaveForm, CaveAndEntranceFormSet, EntranceForm, EntranceLetterForm +from troggle.core.utils import writetrogglefile, write_and_commit from .auth import login_required_if_public '''Manages the complex procedures to assemble a cave description out of the compnoents @@ -187,7 +188,7 @@ def file3d(request, cave, cave_id): # These if statements need refactoring more cleanly if cave.survex_file: #print(" - cave.survex_file '{}'".format(cave.survex_file)) - if threedpath.is_file(): + if threedpath.Pathis_file(): #print(" - threedpath '{}'".format(threedpath)) # possible error here as several .svx files of same names in different directories will overwrite in /3d/ if survexpath.is_file(): @@ -276,8 +277,11 @@ def cavepage(request, karea, subpath): r = rendercave(request, cave, cave.slug()) return r except NoReverseMatch: - message = f'Failed to render cave: {kpath} (it does exist and is unique) because of a Django URL resolution error. Check urls.py.' - return render(request,'errors/generic.html', {'message': message}) + if settings.DEBUG: + raise + else: + message = f'Failed to render cave: {kpath} (it does exist and is unique) because of a Django URL resolution error. Check urls.py.' + return render(request,'errors/generic.html', {'message': message}) except: # anything else is a new problem. Add in specific error messages here as we discover new types of error raise @@ -294,7 +298,7 @@ def caveEntrance(request, slug): return render(request,'cave_entrances.html', {'cave': cave}) @login_required_if_public -def edit_cave(request, slug=None): +def edit_cave(request, path = "", slug=None): '''This is the form that edits all the cave data and writes out an XML file in the :expoweb: repo folder The format for the file being saved is in templates/dataformat/cave.xml @@ -339,7 +343,8 @@ def edit_cave(request, slug=None): ceinst.cave = cave ceinst.save() try: - cave.writeDataFile() + cave_file = cave.file_output() + write_and_commit([cave_file], "Online edit of %s" % cave) # leave other exceptions unhandled so that they bubble up to user interface except PermissionError: message = f'CANNOT save this file.\nPERMISSIONS incorrectly set on server for this file {cave.filename}. Ask a nerd to fix this.' @@ -365,34 +370,35 @@ def edit_cave(request, slug=None): }) @login_required_if_public -def edit_entrance(request, caveslug=None, slug=None): +def edit_entrance(request, path = "", caveslug=None, slug=None): '''This is the form that edits the entrance data for a single entrance and writes out an XML file in the :expoweb: repo folder The format for the file being saved is in templates/dataformat/entrance.xml It does save the data into into the database directly, not by parsing the file. ''' - message = "" - if caveslug is not None: - try: - cave = Cave.objects.get(caveslug__slug = caveslug) - except: - return render(request,'errors/badslug.html', {'badslug': caveslug}) - else: - cave = Cave() - if slug is not None: + + try: + cave = Cave.objects.get(caveslug__slug = caveslug) + except: + return render(request,'errors/badslug.html', {'badslug': caveslug}) + + if slug: entrance = Entrance.objects.get(entranceslug__slug = slug) + caveAndEntrance = CaveAndEntrance.objects.get(entrance = entrance, cave = cave) + entlettereditable = False else: entrance = Entrance() + caveAndEntrance = CaveAndEntrance(cave = cave, entrance = entrance) + entlettereditable = True + if request.POST: form = EntranceForm(request.POST, instance = entrance) + entletter = EntranceLetterForm(request.POST, instance = caveAndEntrance) #versionControlForm = VersionControlCommentForm(request.POST) - if slug is None: - entletter = EntranceLetterForm(request.POST) - else: - entletter = None - if form.is_valid() and (slug is not None or entletter.is_valid()): + if form.is_valid() and entletter.is_valid(): entrance = form.save(commit = False) + entrance_letter = entletter.save(commit = False) if slug is None: if entletter.cleaned_data["entrance_letter"]: slugname = cave.slug() + entletter.cleaned_data["entrance_letter"] @@ -404,36 +410,29 @@ def edit_entrance(request, caveslug=None, slug=None): if slug is None: es = EntranceSlug(entrance = entrance, slug = slugname, primary = True) es.save() - el = entletter.save(commit = False) - el.cave = cave - el.entrance = entrance - el.save() - try: - entrance.writeDataFile() - # leave other exceptions unhandled so that they bubble up to user interface - except PermissionError: - message = f'CANNOT save this file.\nPERMISSIONS incorrectly set on server for this file {entrance.filename}. Ask a nerd to fix this.' - return render(request,'errors/generic.html', {'message': message}) - except subprocess.SubprocessError: - message = f'CANNOT git on server for this file {entrance.filename}. Edits may not be committed.\nAsk a nerd to fix this.' - return render(request,'errors/generic.html', {'message': message}) - + entrance_file = entrance.file_output() + cave_file = cave.file_output() + write_and_commit([entrance_file, cave_file], "Online edit of %s%s" % (cave, entletter)) + entrance.save() + if slug is None: + entrance_letter.save() return HttpResponseRedirect("/" + cave.url) - else: - message = f'! POST data is INVALID {cave}' - print(message) else: form = EntranceForm(instance = entrance) #versionControlForm = VersionControlCommentForm() if slug is None: - entletter = EntranceLetterForm(request.POST) + entletter = EntranceLetterForm() else: - entletter = None + entletter = caveAndEntrance.entrance_letter + return render(request, 'editentrance.html', - {'form': form, 'cave': cave, 'message': message, + {'form': form, + + 'cave': cave, #'versionControlForm': versionControlForm, - 'entletter': entletter + 'entletter': entletter, + 'entlettereditable': entlettereditable }) def ent(request, cave_id, ent_letter): diff --git a/templates/cave.html b/templates/cave.html index 416a450..7cbeef2 100644 --- a/templates/cave.html +++ b/templates/cave.html @@ -541,7 +541,7 @@ div#scene { {{ ent.entrance_letter|safe }} {% if ent.entrance.name %} {{ ent.entrance.name|safe }} - {% endif %}<a href="{% url "editentrance" cave.slug ent.entrance.slug %}">Edit</a> + {% endif %}<a href="{% url "editentrance" ent.entrance.url_parent cave.slug ent.entrance.slug %}">Edit</a> <dl> {% if ent.entrance.marking %} <dt>Marking</dt><dd>{{ ent.entrance.marking_val|safe }}</dd> @@ -601,6 +601,6 @@ div#scene { </ul> {% endif %}</p> -<a href="{% url "newentrance" cave.slug %}">New Entrance</a> +<a href="{% url "newentrance" cave.url_parent cave.slug %}">New Entrance</a> </div> {% endblock content %} diff --git a/templates/editentrance.html b/templates/editentrance.html index 83273ff..138abeb 100644 --- a/templates/editentrance.html +++ b/templates/editentrance.html @@ -8,8 +8,12 @@ <h1>Edit Entrance - at cave {{cave.official_name|safe}} - {{cave.kataster_number}}</h1> {% include 'html_editor_pop_ups.html' %} <h2>{{message}}</h2> -<p>{{entletter}} <form action="" method="post">{% csrf_token %} + {% if entlettereditable %} + <table>{{ entletter }}</table> + {% else %} + <table><tr><th>Entrance Letter</th><td>{{ entletter }}</td></table> + {% endif %} <table>{{ form }}</table> {{ versionControlForm }} <p><input type="submit" value="Submit" /></p> diff --git a/templates/menu.html b/templates/menu.html index 2dd44f6..8953eed 100644 --- a/templates/menu.html +++ b/templates/menu.html @@ -43,7 +43,7 @@ <input id="omega-autofocus" type=search name=P size=8 autofocus> <input type=submit value="Search"></form></li> {% if editable %}<li><a href="{% url "editexpopage" path %}" class="editlink"><strong>Edit this page</strong></a></li>{% endif %} -{% if cave_editable %}<li><a href="{% url "edit_cave" cave.slug %}" class="editlink"><strong>Edit this cave</strong></a></li>{% endif %} +{% if cave_editable %}<li><a href="{% url "edit_cave" cave.url_parent cave.slug %}" class="editlink"><strong>Edit this cave</strong></a></li>{% endif %} </ul> </div> {% endif %} @@ -60,7 +60,7 @@ else: path('<path:filepath>', expofilessingle, name="single"), # local copy of EXPOFILES ] -# see https://docs.djangoproject.com/en/dev/topics/auth/default/ +# see https://docs.djangoproject.com/en/dev/topics/auth/default/tiny # The URLs provided by include('django.contrib.auth.urls') are: # # accounts/login/ [name='login'] @@ -130,15 +130,19 @@ trogglepatterns = [ #re_path(r'^cave/description/([^/]+)/?$', caves.caveDescription), #!!!BAD, local links fail.. #re_path(r'^cave/(?P<cave_id>[^/]+)/?$', caves.cave, name="cave"), # used only in testing !? XXXXXXXXXXXXXXXXXXXXXXXXXX #re_path(r'^cave/(?P<cave_id>[^/]+)/?(?P<ent_letter>[^/])$', ent), #!!!BAD, local links fail..# view_caves.ent - re_path(r'^(?P<slug>[^/]+)_cave_edit/$', edit_cave, name="edit_cave"), # edit_cave needed by cave.html template for url matching + +# Edit caves and entrances + re_path(r'^(?P<path>.*)/(?P<slug>[^/]+)_cave_edit/$', edit_cave, name="edit_cave"), # edit_cave needed by cave.html template for url matching + re_path(r'^(?P<path>.*)/(?P<caveslug>[^/]+):(?P<slug>[^:]+)_entrance_edit', edit_entrance, name = "editentrance"), #edit existing entrance + re_path(r'^(?P<path>.*)/(?P<caveslug>[^/]+)_entrance_new$', edit_entrance, name = "newentrance"), # new entrance for a cave + re_path(r'^(.*)_edit$', editexpopage, name="editexpopage"), re_path(r'^(?P<karea>\d\d\d\d)(?P<subpath>.*)$', cavepage, name="cavepage"), # shorthand /1623/264 or 1623/161/top.htm # Note that urls eg '/1623/161/l/rl89a.htm' are handled by cavepage which redirects them to 'expopage' # Note that _edit$ for a cave description page in a subfolder e.g. /1623/204/204.html_edit gets caught here and breaks with 404 # Entrances re_path(r'^cave/entrance/([^/]+)/?$', caveEntrance), # lists all entrances !!!BAD, local links fail - re_path(r'^entrance/(?P<caveslug>[^/]+)/(?P<slug>[^/]+)/edit/', edit_entrance, name = "editentrance"), #edit existing entrance - re_path(r'^entrance/new/(?P<caveslug>[^/]+)$', edit_entrance, name = "newentrance"), # new entrance for a cave + # System admin and monitoring path('statistics', statistics.stats, name="stats"), @@ -177,11 +181,11 @@ trogglepatterns = [ # The tunnel and therion drawings files pageswalletslistcave path('dwgfiles', dwgallfiles, name="dwgallfiles"), - path('dwgfiles/', dwgallfiles, name="dwgallfiles"), + path('dwgfiles/', dwgallfiles, name="dwgallfiles"), path('dwgdataraw/<path:path>', dwgfilesingle, name="dwgfilesingle"), # QMs pages - must precede other /caves pages? - re_path(r'^cave/qms/([^/]+)/?$', caveQMs, name="caveQMs"), + re_path(r'^cave/qms/([^/]+)/?$', caveQMs, name="caveQMs"), re_path(r'^cave/qms/(?P<cave_id>[^/]+)/(?P<year>\d\d\d\d)-(?P<qm_id>\d*)(?P<grade>[ABCDXV\?]?)-?(?P<blockname>[a-zA-Z]+.*)?$', qm, name="qm"), # Dogs breakfast # the resolution of a QM uses several fields together, there is no clean slug field. Artefact of history. @@ -203,7 +207,7 @@ trogglepatterns = [ re_path(r'^new_image_form/(?P<path>.*)', new_image_form, name = 'new_image_form'), -# Final catchall which also serves expoweb handbook pages and images +# Final catchall which also serves expoweb handbook pages and imagestiny re_path(r'^(.*)$', expopage, name="expopage"), # CATCHALL assumed relative to EXPOWEB ] |