summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/views/editor_helpers.py111
-rw-r--r--core/views/expo.py2
-rw-r--r--lib/version_control.py43
-rw-r--r--templates/editexpopage.html137
-rw-r--r--templates/image_page_template.html24
-rw-r--r--templates/image_selector.html3
-rw-r--r--templates/linked_image_template.html1
-rw-r--r--templates/new_image_form.html5
-rw-r--r--urls.py6
9 files changed, 313 insertions, 19 deletions
diff --git a/core/views/editor_helpers.py b/core/views/editor_helpers.py
new file mode 100644
index 0000000..e0ff5cd
--- /dev/null
+++ b/core/views/editor_helpers.py
@@ -0,0 +1,111 @@
+from django.shortcuts import render, redirect
+from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonResponse
+
+from django.urls import reverse, resolve
+from django.template import Context, loader
+import re, io
+from PIL import Image
+from pathlib import Path
+import django.forms as forms
+import troggle.settings as settings
+
+from troggle.lib import version_control
+
+MAX_IMAGE_WIDTH = 1000
+MAX_IMAGE_HEIGTH = 800
+
+THUMBNAIL_WIDTH = 200
+THUMBNAIL_HEIGTH = 200
+
+def image_selector(request, path):
+ '''Returns available images'''
+ directory = path.rsplit('/', 1)[0]
+ thumbnailspath = Path(settings.EXPOWEB) / directory / "t"
+ thumbnails = []
+ for f in thumbnailspath.iterdir():
+ if f.is_file():
+ thumbnail_url = reverse('expopage', args=["%s/t/%s" % (directory, f.name)])
+ name_base = f.name.rsplit('.', 1)[0]
+ page_path_base = Path(settings.EXPOWEB) / directory / "l"
+ if ((page_path_base / ("%s.htm" % name_base)).is_file()):
+ page_url = reverse('expopage', args=["%s/l/%s.htm" % (directory, name_base)])
+ else:
+ page_url = reverse('expopage', args=["%s/l/%s.html" % (directory, name_base)])
+
+ thumbnails.append({"thumbnail_url": thumbnail_url, "page_url": page_url})
+
+ return render(request, 'image_selector.html', {'thumbnails': thumbnails})
+
+def new_image_form(request, path):
+ '''Manages a form to upload new images'''
+ directory = path.rsplit('/', 1)[0]
+ if request.method == 'POST':
+ form = NewWebImageForm(request.POST, request.FILES, directory = directory)
+ if form.is_valid():
+ f = request.FILES["file_"]
+ binary_data = io.BytesIO()
+ for chunk in f.chunks():
+ binary_data.write(chunk)
+ i = Image.open(binary_data)
+ width, height = i.size
+ if width > MAX_IMAGE_WIDTH or height > MAX_IMAGE_HEIGTH:
+ scale = max(width / MAX_IMAGE_WIDTH, height / MAX_IMAGE_HEIGTH)
+ i = i.resize((int(width / scale), int(height / scale)), Image.ANTIALIAS)
+ tscale = max(width / THUMBNAIL_WIDTH, height / THUMBNAIL_HEIGTH)
+ thumbnail = i.resize((int(width / tscale), int(height / tscale)), Image.ANTIALIAS)
+ ib = io.BytesIO()
+ i.save(ib, format="png")
+ tb = io.BytesIO()
+ thumbnail.save(tb, format="png")
+ image_rel_path, thumb_rel_path, desc_rel_path = form.get_rel_paths()
+ image_page_template = loader.get_template('image_page_template.html')
+ image_page = image_page_template.render({'header': form.cleaned_data["header"], 'description': form.cleaned_data["description"],
+ 'photographer': form.cleaned_data["photographer"], 'year': form.cleaned_data["year"],
+ 'filepath': f'/{image_rel_path}'
+ })
+ image_path, thumb_path, desc_path = form.get_full_paths()
+ try:
+ change_message = form.cleaned_data["change_message"]
+ version_control.write_and_commit([(desc_path, image_page, "utf-8"),
+ (image_path, ib.getbuffer(), False),
+ (thumb_path, tb.getbuffer(), False)],
+ f'{change_message} - online adding of an image')
+ except version_control.WriteAndCommitError as e:
+ return JsonResponse({"error": e.message})
+ linked_image_template = loader.get_template('linked_image_template.html')
+ html_snippet = linked_image_template.render({'thumbnail_url': f'/{thumb_rel_path}', 'page_url': f'/{desc_rel_path}'}, request)
+ return JsonResponse({"html": html_snippet})
+ else:
+ form = NewWebImageForm(directory = directory)
+ template = loader.get_template('new_image_form.html')
+ htmlform = template.render({'form': form, 'path': path}, request)
+ return JsonResponse({"form": htmlform})
+
+class NewWebImageForm(forms.Form):
+ '''The form used by the editexpopage function
+ '''
+ header = forms.CharField(widget=forms.TextInput(attrs={'size':'60', 'placeholder': "Enter title (displayed as a header and in the tab)"}))
+ file_ = forms.FileField()
+ description = forms.CharField(widget=forms.Textarea(attrs={"cols":80, "rows":20, 'placeholder': "Describe the photo (using HTML)"}))
+ photographer = forms.CharField(widget=forms.TextInput(attrs={'size':'60', 'placeholder': "Photographers name"}), required = False)
+ year = forms.CharField(widget=forms.TextInput(attrs={'size':'60', 'placeholder': "Year photo was taken"}), required = False)
+ change_message = forms.CharField(widget=forms.Textarea(attrs={"cols":80, "rows":3, 'placeholder': "Descibe the change made (for git)"}))
+
+ def __init__(self, *args, **kwargs):
+ self.directory = Path(kwargs.pop('directory'))
+ super(forms.Form, self).__init__(*args, **kwargs)
+
+ def get_rel_paths(self):
+ f = self.cleaned_data['file_']
+ return [self.directory / "i" / (f.name.rsplit('.', 1)[0] + ".png"),
+ self.directory / "t" / (f.name.rsplit('.', 1)[0] + ".png"),
+ self.directory / "l" / (f.name.rsplit('.', 1)[0] + ".html")]
+
+ def get_full_paths(self):
+ return [Path(settings.EXPOWEB) / x for x in self.get_rel_paths()]
+
+ def clean_file_(self):
+ for rel_path, full_path in zip(self.get_rel_paths(), self.get_full_paths()):
+ if full_path.exists():
+ raise forms.ValidationError("File already exists in %s" % rel_path)
+ return self.cleaned_data['file_']
diff --git a/core/views/expo.py b/core/views/expo.py
index 00cb6dc..91919a5 100644
--- a/core/views/expo.py
+++ b/core/views/expo.py
@@ -359,7 +359,7 @@ def editexpopage(request, path):
if result != html: # Check if content changed
try:
change_message = pageform.cleaned_data["change_message"]
- version_control.write_and_commit(filepath, result, f'{change_message} - online edit of {path}')
+ version_control.write_and_commit([(filepath, result, "utf-8")], f'{change_message} - online edit of {path}')
except version_control.WriteAndCommitError as e:
return render(request,'errors/generic.html', {'message': e.message})
diff --git a/lib/version_control.py b/lib/version_control.py
index 6343914..01bd8ba 100644
--- a/lib/version_control.py
+++ b/lib/version_control.py
@@ -1,28 +1,35 @@
import troggle.settings as settings
import subprocess
-def write_and_commit(filepath, content, message):
+def write_and_commit(files, message):
"""Writes the content to the filepath and adds and commits the file to git. If this fails, a WriteAndCommitError is raised."""
- cwd = filepath.parent
- filename = filepath.name
git = settings.GIT
- # GIT see also core/models/cave.py writetrogglefile()
- # GIT see also core/views/uploads.py dwgupload()
-
try:
- with open(filepath, "w", encoding="utf8") as f:
- print(f'WRITING{cwd}---{filename} ')
- # as the wsgi process www-data, we have group write-access but are not owner, so cannot chmod.
- # os.chmod(filepath, 0o664) # set file permissions to rw-rw-r--
- f.write(content)
- except PermissionError:
- raise WriteAndCommitError(f'CANNOT save this file.\nPERMISSIONS incorrectly set on server for this file {filename}. Ask a nerd to fix this.')
+ for filepath, content, encoding in files:
+ cwd = filepath.parent
+ filename = filepath.name
+ # GIT see also core/models/cave.py writetrogglefile()
+ # GIT see also core/views/uploads.py dwgupload()
+
+ if encoding:
+ mode = "w"
+ kwargs = {"encoding": encoding}
+ else:
+ mode = "wb"
+ kwargs = {}
+ try:
+ with open(filepath, mode, **kwargs) as f:
+ print(f'WRITING{cwd}---{filename} ')
+ # as the wsgi process www-data, we have group write-access but are not owner, so cannot chmod.
+ # os.chmod(filepath, 0o664) # set file permissions to rw-rw-r--
+ f.write(content)
+ except PermissionError:
+ raise WriteAndCommitError(f'CANNOT save this file.\nPERMISSIONS incorrectly set on server for this file {filename}. Ask a nerd to fix this.')
- try:
- 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_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_commit = subprocess.run([git, "commit", "-m", message], 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
diff --git a/templates/editexpopage.html b/templates/editexpopage.html
index ef3aa20..b71c91f 100644
--- a/templates/editexpopage.html
+++ b/templates/editexpopage.html
@@ -5,6 +5,8 @@
<!--<script src="{{ settings.TINY_MCE_MEDIA_URL }}tiny_mce.js" type="text/javascript"></script>-->
<!-- <script type="text/javascript"> tinyMCE.init({ mode : "textareas" }); </script>-->
+<script src="{{ settings.MEDIA_URL }}admin/js/vendor/jquery/jquery.js" type="text/javascript"></script>
+
<script src={{ settings.MEDIA_URL }}codemirror/codemirror.js></script>
<script src={{ settings.MEDIA_URL }}codemirror/xml.js></script>
<script src={{ settings.MEDIA_URL }}codemirror/javascript.js></script>
@@ -38,9 +40,82 @@
height: 5%;
}
</style>
+ <style type=text/css>
+ html {
+ font-family: "Helvetica Neue", sans-serif;
+ width: 100%;
+ color: #666666;
+ text-align: center;
+ }
+
+ .popup-overlay {
+ /*Hides pop-up when there is no "active" class*/
+ visibility: hidden;
+ position: absolute;
+ background: #ffffff;
+ border: 3px solid #666666;
+ width: 90%;
+ height: 80%;
+ overflow: scroll;
+ left: 5%;
+ z-index: 20;
+ }
+
+ .popup-overlay.active {
+ /*displays pop-up when "active" class is present*/
+ visibility: visible;
+ text-align: center;
+ }
+
+ .popup-content {
+ /*Hides pop-up content when there is no "active" class */
+ visibility: hidden;
+ }
+
+ .popup-content.active {
+ /*Shows pop-up content when "active" class is present */
+ visibility: visible;
+ }
+
+ button {
+ display: inline-block;
+ vertical-align: middle;
+ border-radius: 30px;
+ margin: .20rem;
+ font-size: 1rem;
+ color: #666666;
+ background: #ffffff;
+ border: 1px solid #666666;
+ }
+
+ button:hover {
+ border: 1px solid #666666;
+ background: #666666;
+ color: #ffffff;
+ }
+ </style>
{% endblock %}
{% block body %}
<h1>Edit {{ path }}</h1>
+<!--Creates the add image popup-->
+<div class="add-image-popup popup-overlay">
+ <div class="add-image-popup popup-content">
+ <h2>Select Image</h2>
+ <p id="image_popup_content"> Loading ...</p>
+ <button onclick="new_image_popup()">Upload Image</button>
+ <button class="close" onclick="$('.add-image-popup').removeClass('active');">Close</button>
+ </div>
+</div>
+
+<!--Creates the new image popup-->
+<div class="new-image-popup popup-overlay">
+ <div class="new-image-popup popup-content">
+ <h2>New Image</h2>
+ <p id="new_image_popup_content"> Loading ...</p>
+ <button class="close" onclick="$('.new-image-popup').removeClass('active');">Close</button>
+ </div>
+</div>
+
<form action="" method="post">{% csrf_token %}
{{ form.non_field_errors }}
<div class="fieldWrapper">
@@ -62,6 +137,7 @@
<button type="button" onclick="addTag('h4', '')">heading 4</button>
<button type="button" onclick="addTag('a', 'href=&quot;&quot;')">hyperlink</button>
<button type="button" onclick="addTag('p', '')">paragraph</button>
+<button type="button" onclick="add_image_popup()">image</button>
<div class="fieldWrapper">
{{ form.change_message.errors }}
<label for="{{ form.title.id_for_label }}">Git change message:</label>
@@ -70,6 +146,60 @@
{% include "menu.html" %}
<p><input type="submit" value="Submit" /></p>
</form>
+
+
+<script>
+
+
+function add_image_popup() {
+ $('.add-image-popup').addClass('active');
+ $('#image_popup_content').load("{% url 'image_selector' path %}", function() {
+ $('.thumbnail').click(function(){
+ $(".add-image-popup").removeClass("active");
+ addStr($( this ).attr("data-html"))
+ });
+ })
+ }
+function new_image_popup() {
+ $('.add-image-popup').removeClass('active');
+ $('.new-image-popup').addClass('active');
+ $.ajax({
+ type : "GET",
+ dataType: "json",
+ url: "{% url 'new_image_form' path %}",
+ success: function(data){handle_new_image(data)}
+ });
+ }
+
+function handle_new_image(data) {
+ if (data.hasOwnProperty('form')) {
+ $('#new_image_popup_content').html(data.form);
+ $('#new_image_form').on('submit', function(e){
+ e.preventDefault();
+ data = $('#new_image_form').serialize();
+
+ $.ajax({
+ type : "POST",
+ dataType: "json",
+ url: "{% url 'new_image_form' path %}",
+ data: new FormData($('#new_image_form')[0]),
+ processData: false,
+ contentType: false,
+ success: function(data){
+ handle_new_image(data);
+ }
+ });
+ });
+ }
+ else if (data.hasOwnProperty('html')) {
+ $('.new-image-popup').removeClass('active');
+ addStr(data.html);
+ }
+ else {
+ alert(data.error);
+ }
+ }
+</script>
<script>
var delay;
// Initialize CodeMirror editor with a nice html5 canvas demo.
@@ -103,5 +233,12 @@
editor.focus();
editor.setCursor({line: to.line , ch : to.ch + 2 + tag.length + attr.length });
}
+
+ function addStr(x){
+ var to = editor.getCursor(false);
+ editor.replaceRange(x, to);
+ editor.focus();
+ editor.setCursor({line: to.line , ch : to.ch + x.length });
+ }
</script>
{% endblock %}
diff --git a/templates/image_page_template.html b/templates/image_page_template.html
new file mode 100644
index 0000000..4a240e8
--- /dev/null
+++ b/templates/image_page_template.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf8" />
+<title>
+{{ header }}
+</title>
+<link rel="stylesheet" type="text/css" href="../../../css/main2.css" />
+</head>
+
+<body>
+<H1>{{ header }}</H1>
+<div class="centre"><img alt="" src="{{ filepath }}" />
+</div>
+
+<p>{{ description }}</p>
+
+{% if photographer %}
+<p class="caption">Photo &copy; {{ photographer }}{% if year %}, {{ year }}{% endif %}</p>
+{% endif %}
+
+<hr />
+</body>
+</html>
diff --git a/templates/image_selector.html b/templates/image_selector.html
new file mode 100644
index 0000000..5ac4e01
--- /dev/null
+++ b/templates/image_selector.html
@@ -0,0 +1,3 @@
+{% for thumbnail in thumbnails %}
+ <img class = "thumbnail" src = "{{ thumbnail.thumbnail_url }}" data-html = "{% include 'linked_image_template.html' with thumbnail_url=thumbnail.thumbnail_url page_url=thumbnail.page_url %}"/>
+{% endfor %}
diff --git a/templates/linked_image_template.html b/templates/linked_image_template.html
new file mode 100644
index 0000000..336f1dd
--- /dev/null
+++ b/templates/linked_image_template.html
@@ -0,0 +1 @@
+<a href='{{ page_url }}'><img src='{{ thumbnail_url }}' /></a>
diff --git a/templates/new_image_form.html b/templates/new_image_form.html
new file mode 100644
index 0000000..1a2f636
--- /dev/null
+++ b/templates/new_image_form.html
@@ -0,0 +1,5 @@
+<form id="new_image_form" action="{% url 'new_image_form' path %}" method="post" enctype="multipart/form-data">
+ {% csrf_token %}
+ {{ form.as_p }}
+ <input type="submit" value="Submit">
+</form>
diff --git a/urls.py b/urls.py
index da420a0..5f5a711 100644
--- a/urls.py
+++ b/urls.py
@@ -23,6 +23,7 @@ from troggle.core.views.statistics import pathsreport, stats, dataissues
from troggle.core.views.expo import expofiles_redirect, expofilessingle, expopage, editexpopage, mediapage, map, mapfile
from troggle.core.views.survex import survexcaveslist, survexcavesingle, svx
from troggle.core.views.auth import expologin, expologout
+from troggle.core.views.editor_helpers import image_selector, new_image_form
"""This sets the actualurlpatterns[] and urlpatterns[] lists which django uses
to resolve urls - in both directions as these are declarative.
@@ -190,6 +191,11 @@ trogglepatterns = [
re_path(r'^/loser/(?P<subpath>.*)$', mediapage, {'doc_root': settings.SURVEX_DATA}, name="mediapage"), # Oddly not working !?
re_path(r'^map/map.html', map, name="map"), # Redirects to OpenStreetMap JavaScript
re_path(r'^map/(?P<path>.*)$', mapfile, name="mapfile"), # css, js, gpx
+
+# Helpers to edit HTML
+ re_path(r'^image_selector/(?P<path>.*)', image_selector, name = 'image_selector'),
+ 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
re_path(r'^(.*)$', expopage, name="expopage"), # CATCHALL assumed relative to EXPOWEB