diff --git a/fotos/forms.py b/fotos/forms.py index fc83da7c86136988933c46c020406909c73dad7a..e967a90687e1a8caa42523050ddf2af962d7fbf5 100644 --- a/fotos/forms.py +++ b/fotos/forms.py @@ -1,12 +1,12 @@ from django import forms from django.forms import ClearableFileInput -from fotos.models import UploadFile +from fotos.models import UploadedImage -class FileUpload(forms.ModelForm): +class UploadedImageForm(forms.ModelForm): class Meta: - model = UploadFile - fields = ['files'] + model = UploadedImage + fields = [ 'files' ] widgets = { 'files': ClearableFileInput(attrs = { 'multiple': True }) } diff --git a/fotos/models.py b/fotos/models.py index 69251d70a48a39ebddd492b699090761ae684902..b6120873c2037dfde868c23ee358d8e57c35263e 100644 --- a/fotos/models.py +++ b/fotos/models.py @@ -127,7 +127,7 @@ class Photo(models.Model): 'location']) ] ordering = ('timestamp',) -class UploadFile(models.Model): +class UploadedImage(models.Model): - files = models.FileField(upload_to=UPLOAD_TMP_DIR, - blank=True, null=True) + files = models.ImageField(upload_to=UPLOAD_TMP_DIR, + blank=True, null=True) diff --git a/fotos/serializers.py b/fotos/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..dc981f3cb07d1ba6846ad459d472b7a0508da8d0 --- /dev/null +++ b/fotos/serializers.py @@ -0,0 +1,29 @@ +from rest_framework import serializers + +from fotos.models import Album, Photo, UploadedImage + +class AlbumSerializer(serializers.ModelSerializer): + + class Meta: + model = Album + fields = [ 'the_parent', 'name', 'slug', + 'first_timestamp', 'last_timestamp', + 'time_offset' ] + +class PhotoSerializer(serializers.ModelSerializer): + + class Meta: + model = Photo + fields = [ 'title', 'description', 'the_album', + 'latitude', 'longitude', 'width', 'height', + 'view_width', 'view_height', + 'thumb_width', 'thumb_height', 'timestamp', + 'view_timestamp', 'time_offset', 'url', + 'view_url', 'thumb_url', 'basename', + 'view_latitude', 'view_longitude' ] + +class UploadedImageSerializer(serializers.ModelSerializer): + + class Meta: + model = UploadedImage + fields = [ 'files' ] diff --git a/fotos/templates/base.html b/fotos/templates/base.html index b00d04623e6034cac9f41aab2b51e827d659029d..ff7faccd5ab5ada3ad8cf6063c94eabfa8c695ec 100644 --- a/fotos/templates/base.html +++ b/fotos/templates/base.html @@ -19,11 +19,14 @@ {% else %} diff --git a/fotos/views.py b/fotos/views.py index 723e053c17db2dea116daa5da0ddedd79d5bcc1a..ed7664e15ff7d52e6a49a0f7f4ccad548cfb1dec 100644 --- a/fotos/views.py +++ b/fotos/views.py @@ -1,17 +1,25 @@ import os +import os.path import sys from django.views.generic.edit import FormView +from django.views.decorators.csrf import csrf_exempt from django.shortcuts import render, redirect from django.contrib.auth import get_user from django.http import HttpResponse, Http404, JsonResponse from django.db import transaction +from rest_framework.parsers import JSONParser +from rest_framework.decorators import api_view +from rest_framework.response import Response + from galeria.settings import GALLERY_INFO, LANGUAGE_CODE -from fotos.models import Album, Photo, UploadFile -from fotos.forms import FileUpload +from fotos.models import Album, Photo, UploadedImage +from fotos.forms import UploadedImageForm from fotos.util import get_image_metadata, normalize +from fotos.serializers import AlbumSerializer, PhotoSerializer, \ + UploadedImageSerializer from datetime import datetime from babel.dates import get_month_names @@ -50,30 +58,156 @@ def _get_events(ma): def _get_photos(ea): return Photo.objects.filter(the_album=ea).order_by('timestamp') -def _get_prev_year(ya): +def _get_prev_year_url(ya): + prev_year = _get_prev_year(ya) + if prev_year is not None: + return prev_year.get_url_path() return None -def _get_next_year(ya): +def _get_next_year_url(ya): + next_year = _get_next_year(ya) + if next_year is not None: + return next_year.get_url_path() return None -def _get_prev_month(ma): +def _get_prev_month_url(ma): + prev_month = _get_prev_month(ma) + if prev_month is not None: + return prev_month.get_url_path() return None -def _get_next_month(ma): +def _get_next_month_url(ma): + next_month = _get_next_month(ma) + if next_month is not None: + return next_month.get_url_path() return None -def _get_prev_event(ea): +def _get_prev_event_url(ea): + prev_event = _get_prev_event(ea) + if prev_event is not None: + return prev_event.get_url_path() return None -def _get_next_event(ea): +def _get_next_event_url(ea): + next_event = _get_next_event(ea) + if next_event is not None: + return next_event.get_url_path() return None -def _get_prev_photo(ph): +def _get_prev_photo_url(ph): + prev_photo = _get_prev_photo(ph) + if prev_photo is not None: + return prev_photo.get_url_path() return None -def _get_next_photo(ph): +def _get_next_photo_url(ph): + next_photo = _get_next_photo(ph) + if next_photo is not None: + return next_photo.get_url_path() return None +def _get_prev_year(ya): + years = _get_years() + if years is None or years.first() == ya: + return None + i = 1 + while years[i] != ya: + i += 1 + return years[i-1] + +def _get_next_year(ya): + years = _get_years() + if years is None or years.last() == ya: + return None + i = years.count() - 2 + while years[i] != ya: + i -= 1 + return years[i+1] + +def _get_prev_month(ma): + ya = ma.the_parent + months = _get_months(ya) + if ma == months.first(): + prev_year = _get_prev_year(ya) + if prev_year is None: + return None + prev_year_months = _get_months(prev_year) + return prev_year_months.last() + i = 1 + while months[i] != ma: + i += 1 + return months[i-1] + +def _get_next_month(ma): + ya = ma.the_parent + months = _get_months(ya) + if ma == months.last(): + next_year = _get_next_year(ya) + if next_year is None: + return None + next_year_months = _get_months(next_year) + return next_year_months.first() + i = months.count() - 2 + while months[i] != ma: + i -= 1 + return months[i+1] + +def _get_prev_event(ea): + ma = ea.the_parent + events = _get_events(ma) + if ea == events.first(): + prev_month = _get_prev_month(ma) + if prev_month is None: + return None + prev_month_events = _get_events(prev_month) + return prev_month_events.last() + i = 1 + while events[i] != ea: + i += 1 + return events[i-1] + +def _get_next_event(ea): + ma = ea.the_parent + events = _get_events(ma) + if ea == events.last(): + next_month = _get_next_month(ma) + if next_month is None: + return None + next_month_events = _get_events(next_month) + return next_month_events.first() + i = events.count() - 2 + while events[i] != ea: + i -= 1 + return events[i+1] + +def _get_prev_photo(ph): + ea = ph.the_album + photos = _get_photos(ea) + if ph == photos.first(): + prev_event = _get_prev_event(ea) + if prev_event is None: + return None + prev_event_photos = _get_photos(prev_event) + return prev_event_photos.last() + i = 1 + while photos[i] != ph: + i += 1 + return photos[i-1] + +def _get_next_photo(ph): + ea = ph.the_album + photos = _get_photos(ea) + if ph == photos.last(): + next_event = _get_next_event(ea) + if next_event is None: + return None + next_event_photos = _get_photos(next_event) + return next_event_photos.first() + i = photos.count() - 2 + while photos[i] != ph: + i -= 1 + return photos[i+1] + def main(request): page_title = 'Años' albums = _get_years() @@ -98,8 +232,8 @@ def year(request, year): 'breadcrumbs': [ { 'link': ya.get_url_path(), 'name': 'Año ' + year } ], 'gallery': GALLERY_INFO, - 'navigation': { 'prev': _get_prev_year(ya), - 'next': _get_next_year(ya), + 'navigation': { 'prev': _get_prev_year_url(ya), + 'next': _get_next_year_url(ya), 'up': '/galeria' }, 'now_year': datetime.now().year, 'page_title': 'Año ' + year, @@ -111,7 +245,7 @@ def month(request, year, month): ya = _get_year_album(year) ma = _get_month_album(ya, month) if ya is None or ma is None: - raise Http404(year + '/' + month + ' does not exist') + raise Http404(os.path.join(year, month) + ' does not exist') attrs = { 'album': ma, 'albums': _get_events(ma), 'breadcrumbs': [ { 'link': ya.get_url_path(), @@ -119,14 +253,14 @@ def month(request, year, month): { 'link': ma.get_url_path(), 'name': ma.name } ], 'gallery': GALLERY_INFO, - 'navigation': { 'prev': _get_prev_month(ma), - 'next': _get_next_month(ma), + 'navigation': { 'prev': _get_prev_month_url(ma), + 'next': _get_next_month_url(ma), 'up': ya.get_url_path() }, 'now_year': datetime.now().year, 'page_title': ('Mes de %s, %d' % \ ( get_month_name(ma.first_timestamp), ya.first_timestamp.year )), - 'prefix': '/galeria/' + year + '/' + month, + 'prefix': os.path.join('/galeria/', year, month), 'user': get_user(request) } return render(request, 'album.html', attrs) @@ -135,7 +269,7 @@ def event(request, year, month, event): ma = _get_month_album(ya, month) ea = _get_event_album(ma, event) if ya is None or ma is None or ea is None: - raise Http404(year + '/' + month + '/' + event + ' does not exist') + raise Http404(os.path.join(year, month, event) + ' does not exist') attrs = { 'album': ea, 'breadcrumbs': [ { 'link': ya.get_url_path(), 'name': ya.name }, @@ -144,13 +278,13 @@ def event(request, year, month, event): { 'link': ea.get_url_path(), 'name': ea.name } ], 'gallery': GALLERY_INFO, - 'navigation': { 'prev': _get_prev_event(ea), - 'next': _get_next_event(ea), + 'navigation': { 'prev': _get_prev_event_url(ea), + 'next': _get_next_event_url(ea), 'up': ma.get_url_path() }, 'now_year': datetime.now().year, 'page_title': ea.name, 'photos': _get_photos(ea), - 'prefix': '/galeria/' + year + '/' + month + '/' + event, + 'prefix': os.path.join('/galeria', year, month, event), 'user': get_user(request) } return render(request, 'album.html', attrs) @@ -160,8 +294,8 @@ def photo(request, year, month, event, photo): ea = _get_event_album(ma, event) ph = _get_photo(ea, photo) if ya is None or ma is None or ea is None or ph is None: - raise Http404(year + '/' + month + '/' + - event + '/' + photo + ' does not exist') + raise Http404(os.path.join(year, month, event, photo + + ' does not exist')) attrs = { 'breadcrumbs': [ { 'link': ya.get_url_path(), 'name': ya.name }, { 'link': ma.get_url_path(), @@ -171,13 +305,13 @@ def photo(request, year, month, event, photo): { 'link': ph.get_url_path(), 'name': ph.title } ], 'gallery': GALLERY_INFO, - 'navigation': { 'prev': _get_prev_photo(ph), - 'next': _get_next_photo(ph), + 'navigation': { 'prev': _get_prev_photo_url(ph), + 'next': _get_next_photo_url(ph), 'up': ea.get_url_path() }, 'now_year': datetime.now().year, 'page_title': ph.title, 'photo': ph, - 'prefix': '/galeria/' + year + '/' + month + '/' + event, + 'prefix': os.path.join('/galeria', year, month, event), 'user': get_user(request) } return render(request, 'photo.html', attrs) @@ -221,9 +355,9 @@ def _get_photo_album(metadata): return year_album, month_album, event_album @transaction.atomic -def _add_photo(upload_file): - path = '/' + upload_file.files.name - upload_file.delete() +def _add_photo(uploaded_image): + path = '/' + uploaded_image.files.name + uploaded_image.delete() md = get_image_metadata(path) if md is None: os.remove(path) @@ -252,28 +386,36 @@ def _add_photo(upload_file): if event_album.the_photo is None: event_album.the_photo = photo event_album.save() + if month_album.first_timestamp > event_album.first_timestamp: + month_album.first_timestamp = event_album.first_timestamp + if month_album.last_timestamp < event_album.last_timestamp: + month_album.last_timestamp = event_album.last_timestamp if month_album.the_photo is None: month_album.the_photo = photo - month_album.save() + month_album.save() + if year_album.first_timestamp > month_album.first_timestamp: + year_album.first_timestamp = month_album.first_timestamp + if year_album.last_timestamp < month_album.last_timestamp: + year_album.last_timestamp = month_album.last_timestamp if year_album.the_photo is None: year_album.the_photo = photo - year_album.save() + year_album.save() os.remove(path) -def admin_upload(request): +def upload(request): if request.method == 'POST': - form = FileUpload(request.POST, request.FILES) + form = UploadedImageForm(request.POST, request.FILES) files = request.FILES.getlist('files') if form.is_valid(): for f in files: - upload_file = UploadFile(files=f) - upload_file.save() - _add_photo(upload_file) + uploaded_image = UploadedImage(files=f) + uploaded_image.save() + _add_photo(uploaded_image) return JsonResponse({'data': 'Data uploaded'}) else: return JsonResponse({'data': 'Something went wrong'}) else: - form = FileUpload() + form = UploadedImageForm() return render(request, 'upload.html', { 'form': form, 'breadcrumbs': [], @@ -283,3 +425,88 @@ def admin_upload(request): 'up': None }, 'now_year': datetime.now().year, 'page_title': 'Subir fotos' }) + +def highlight_month(request, year, month): + ya = _get_year_album(year) + ma = _get_month_album(ya, month) + if ya is None or ma is None: + raise Http404(os.path.join(year, month) + ' does not exist') + + if ya.the_photo == ma.the_photo: + return redirect(os.path.join('/galeria', year, month)) + + ya.the_photo = ma.the_photo + ya.save() + return redirect(os.path.join('/galeria', year)) + +def highlight_event(request, year, month, event): + ya = _get_year_album(year) + ma = _get_month_album(ya, month) + ea = _get_event_album(ma, event) + if ya is None or ma is None or ea is None: + raise Http404(os.path.join(year, month, event) + ' does not exist') + + if ma.the_photo == ea.the_photo: + return redirect(os.path.join('/galeria', year, month, event)) + + old_ph = ma.the_photo + if ya.the_photo == old_ph: + ya.the_photo = ea.the_photo + ya.save() + ma.the_photo = ea.the_photo + ma.save() + return redirect(os.path.join('/galeria', year, month)) + +def highlight_photo(request, year, month, event, photo): + ya = _get_year_album(year) + ma = _get_month_album(ya, month) + ea = _get_event_album(ma, event) + ph = _get_photo(ea, photo) + if ya is None or ma is None or ea is None or ph is None: + raise Http404(os.path.join(year, month, event, photo) + + ' does not exist') + + if ea.the_photo == ph: + return redirect(os.path.join('/galeria', year, month, event, photo)) + + old_ph = ea.the_photo + if ya.the_photo == old_ph: + ya.the_photo = ph + ya.save() + if ma.the_photo == old_ph: + ma.the_photo = ph + ma.save() + ea.the_photo = ph + ea.save() + return redirect(os.path.join('/galeria', year, month, event)) + +@api_view(['GET']) +def album_list(request): + + if request.method == 'GET': + albums = Album.objects.all() + serializer = AlbumSerializer(albums, many=True) + return Response(serializer.data) + +@api_view(['GET']) +def photo_list(request): + + if request.method == 'GET': + photos = Photo.objects.all() + serializer = PhotoSerializer(photos, many=True) + return Response(serializer.data) + +@api_view(['POST']) +def upload_image(request): + + if request.method == 'POST': + form = UploadedImageSerializer(request.POST, request.FILES) + files = request.FILES.getlist('files') + if form.is_valid(): + for f in files: + uploaded_image = UploadedImage(files=f) + uploaded_image.save() + _add_photo(uploaded_image) + return JsonResponse({'response':'OK'}) + else: + return JsonResponse({'response':'Error'}) diff --git a/galeria/settings.py b/galeria/settings.py index 049073f0950c94f5c9219963cd3d05ee04ea7b8b..587e616e335d27466e9486206a81e6fc87951679 100644 --- a/galeria/settings.py +++ b/galeria/settings.py @@ -23,7 +23,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = 'django-insecure-4=b2093i%q)ypq1smb^00p7(=ap*1(7q4pi82g%!o3+a63vs23' # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = False ALLOWED_HOSTS = [ '127.0.0.1', 'localhost', 'aztlan.fciencias.unam.mx' ] @@ -37,6 +37,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', ] MIDDLEWARE = [ diff --git a/galeria/urls.py b/galeria/urls.py index 53cc4644beb871ff6db3cfc0722a650ebb75fd51..57c33db20f32813c5a26d4a50ba5b61c2a9e7ce2 100644 --- a/galeria/urls.py +++ b/galeria/urls.py @@ -24,6 +24,15 @@ urlpatterns = [ url(r'^(\d{4})/(\d{2})/$', views.month), url(r'^(\d{4})/(\d{2})/([\w-]+)/$', views.event), url(r'^(\d{4})/(\d{2})/([\w-]+)/([\w\.-]+)$', views.photo), - url(r'^admin/upload', views.admin_upload), + url(r'^admin/upload', views.upload), + url(r'^admin/highlight/(\d{4})/(\d{2})/$', + views.highlight_month), + url(r'^admin/highlight/(\d{4})/(\d{2})/([\w-]+)/$', + views.highlight_event), + url(r'^admin/highlight/(\d{4})/(\d{2})/([\w-]+)/([\w\.-]+)$', + views.highlight_photo), + url(r'^rest/album_list$', views.album_list), + url(r'^rest/photo_list$', views.photo_list), + url(r'^rest/upload_image$', views.upload_image), url('admin/', admin.site.urls), ] diff --git a/static/galeria.js b/static/galeria.js index 72d418eb1c58e7789f5867f5014097818588f556..b4beea2ff09d9b5a5dac3646f6f67ad42d090c2a 100644 --- a/static/galeria.js +++ b/static/galeria.js @@ -1,4 +1,16 @@ -/* When the user clicks on the button, +function goTo(url) { + window.location.href=url; +} + +function highlight(breadcrumbs) { + breadcrumbs = breadcrumbs.replaceAll("'", '"'); + var bc = JSON.parse(breadcrumbs); + var leaf = bc.at(-1); + var link = leaf['link'].replaceAll('/galeria/', '/galeria/admin/highlight/'); + goTo(link); +} + +/* When the user clicks on the menu Administration, toggle between hiding and showing the dropdown content */ function adminDropdown() { document.getElementById("admin-dropdown").classList.toggle("show"); diff --git a/static/progress-bar.js b/static/progress-bar.js index 8f951ad63162aa0cf80528d4105c285e2b30274b..035f355b2055961bc39bc9caeae50d7b6cc6e656 100644 --- a/static/progress-bar.js +++ b/static/progress-bar.js @@ -10,7 +10,8 @@ $("#upload_form").submit(function(e) { const media_data = input_file.files[0]; if (media_data != null) { console.log(media_data); - progress_bar.classList.remove("not-visible"); + progress_bar.classList.remove('not-visible'); + result_message.classList.add('not-visible'); } $.ajax({ @@ -45,6 +46,11 @@ $("#upload_form").submit(function(e) { }, error: function(err){ console.log(err); + uploadForm.reset() + progress_bar.classList.add('not-visible') + result_message.classList.remove('not-visible') + result_message.innerHTML = + 'Ocurrió un error al subier los archivos.'; }, cache: false, contentType: false, diff --git a/static/style.css b/static/style.css index 491212817f18374243022c72b40099ac97cb2a07..2be19135e17125bfa5c2dc2fcb30e8ff90ecabb2 100644 --- a/static/style.css +++ b/static/style.css @@ -268,6 +268,14 @@ form.upload { display: block; } +button.menu-item { + border: none; + color: black; + padding: 6px 8px; + text-decoration: none; + display: block; +} + div.signin-link { position: absolute; right: 20px;