diff --git a/fotos/models.py b/fotos/models.py index 14a8425f96cee8580192f0708cc0ada18c5bfee0..ac3317b0fced6a8630e3a2decb50cc9d8c64a765 100644 --- a/fotos/models.py +++ b/fotos/models.py @@ -11,7 +11,8 @@ from galeria.settings import PREFIX, STATIC_PATH_PREFIX, UPLOAD_TMP_DIR, \ from babel.dates import format_datetime from datetime import datetime, timezone, timedelta -from fotos.util import get_image_metadata, get_month_name, normalize +from fotos.util import get_image_metadata, get_month_name, get_year_name, \ + normalize def float2degrees(d): i = math.floor(d) @@ -60,10 +61,12 @@ class Album(models.Model): .filter(the_parent=self, slug=slug) \ .order_by('first_timestamp').first() - def get_tree(self): + def get_tree(self, auth=False): tree = {} subalbums = self.get_subalbums() for subalbum in subalbums: + if not auth and subalbum.private: + continue tree[subalbum.slug] = { 'album': subalbum, 'label': subalbum.get_day_range(), 'tree': subalbum.get_tree() } @@ -118,6 +121,54 @@ class Album(models.Model): if not self.the_parent.the_parent.the_parent: self._highlight_event() + def has_public_photos(self): + photos = self.get_photos() + if photos: + for photo in photos: + if not photo.private: + return True + else: + for subalbum in self.get_subalbums(): + if subalbum.has_public_photos(): + return True + return False + + def make_private(self): + if self.private: + return + self.private = True + self.save() + photos = self.get_photos() + if photos: + for photo in photos: + if not photo.private: + photo.private = True + photo.save() + else: + for subalbum in self.get_subalbums(): + if not subalbum.private: + subalbum.make_private() + if self.the_parent and not self.the_parent.has_public_photos(): + self.the_parent.make_private() + + def make_public(self): + if not self.private: + return + if self.the_parent and self.the_parent.private: + return + self.private = False + self.save() + photos = self.get_photos() + if photos: + for photo in photos: + if photo.private: + photo.private = False + photo.save() + else: + for subalbum in self.get_subalbums(): + if subalbum.private: + subalbum.make_public() + @transaction.atomic def do_delete(self): if not self.the_parent: @@ -370,6 +421,14 @@ class Album(models.Model): def get_years(klass): return Album.objects.filter(the_parent=None).order_by('first_timestamp') + @classmethod + def has_public_years(klass): + for year in Album.objects.filter(the_parent=None)\ + .order_by('first_timestamp'): + if not year.private: + return True + return False + @classmethod def get_year(klass, year): return Album.objects.filter(the_parent=None, slug=year).first() @@ -386,12 +445,13 @@ class Album(models.Model): @classmethod def get_random_photo(klass, blacklist=None): - if not blacklist: - return Photo.objects.all().order_by('?').first() + photo = Photo.objects.filter(private=False).order_by('?').first() + if not photo or not blacklist: + return photo terms = [ t.lower() for t in blacklist.split(',') ] blacklisted = True while blacklisted: - photo = Photo.objects.all().order_by('?').first() + photo = Photo.objects.filter(private=False).order_by('?').first() blacklisted = False for term in terms: if photo.matches(term): @@ -461,10 +521,12 @@ class Album(models.Model): year.reset() @classmethod - def full_tree(klass): + def full_tree(klass, auth=False): tree = {} years = Album.objects.filter(the_parent=None).order_by('first_timestamp') for year in years: + if not auth and year.private: + continue tree[year.slug] = { 'album': year, 'label': year.get_day_range(), 'tree': year.get_tree() } @@ -649,6 +711,22 @@ class Photo(models.Model): ea.the_photo = self ea.save() + def make_private(self): + if self.private: + return + self.private = True + self.save() + if not self.the_album.has_public_photos(): + self.the_album.make_private() + + def make_public(self): + if not self.private: + return + if self.the_album.private: + return + self.private = False + self.save() + def __str__(self): s = '%s (%s): %04d/%02d/%02d %02d:%02d:%02d' % \ (self.title, self.basename, @@ -720,13 +798,13 @@ class Photo(models.Model): @classmethod def _get_photo_album(klass, metadata): ts = metadata['timestamp'] - tz = timezone(timedelta(hours=metadata['time_offset'])) + offset = metadata['time_offset'] sy = '%04d' % ts.year sm = '%02d' % ts.month year = Album.objects.filter(the_parent=None, slug=sy).first() if not year: year = Album(the_parent = None, - name = _('Year %(year)s') % { 'year': sy }, + name = get_year_name(sy), slug = sy, first_timestamp = ts, last_timestamp = ts, @@ -736,7 +814,7 @@ class Photo(models.Model): month = Album.objects.filter(the_parent=year, slug=sm).first() if not month: month = Album(the_parent = year, - name = get_month_name(ts, tz, capitalize=True), + name = get_month_name(ts, offset, True), slug = sm, first_timestamp = ts, last_timestamp = ts, diff --git a/fotos/templates/album.html b/fotos/templates/album.html index 526fb1c1a8464187ae7c519d145c4a4c24985a0d..6e904d92630524d901c2e48d1adee1820546eb47 100644 --- a/fotos/templates/album.html +++ b/fotos/templates/album.html @@ -3,67 +3,24 @@ {% load static %} {% load galeria %} {% block content %} - <div class="album"> - {% if photos %} - {% for photo in photos %} - <div class="preview-box" id="p-{{photo.basename}}"> - <div class="preview-frame {% if photo != highlight %} bgnormal {% else %} bghighlight {% endif %}"> - <div class="preview"> - <a href="{{path}}/{{photo.basename}}"> - <img class="preview" src="{{sup}}{{photo.thumb_url}}" - width="{{photo.thumb_width}}px" - height="{{photo.thumb_height}}px" - alt="{{photo.title}}" title="{{photo.title}}" /> - </a> - </div> - <div class="preview-caption"> - {{photo.title}} - </div> - </div> - </div> - {% endfor %} - {% elif results %} - {% for result in results %} - <div class="preview-box" id="p-{{result.basename}}"> - <div class="preview-frame bgnormal"> - <div class="preview"> - <a href="{{path}}/{{result.url|photo_path}}"> - <img src="{{sup}}{{result.thumb_url}}" class="preview" - width="{{result.thumb_width}}px" - height="{{result.thumb_height}}px" - alt="{{result.title}}" title="{{result.title}}" /> - </a> - </div> - <div class="preview-caption"> - {{result.title}} - </div> - </div> - </div> - {% endfor %} - {% elif albums %} - {% for album in albums %} - {% if album.the_photo %} - <div class="preview-box" id="a-{{album.slug}}"> - <div class="preview-frame {% if album != highlight %} bgnormal {% else %} bghighlight {% endif %}"> - <div class="preview"> - <a href="{{path}}/{{album.slug}}"> - <img src="{{sup}}{{album.the_photo.thumb_url}}" class="preview" - width="{{album.the_photo.thumb_width}}px" - height="{{album.the_photo.thumb_height}}px" - alt="{{album.name}}" title="{{album.name}}" /> - </a> - </div> - <div class="preview-caption"> - {{album.name}} - </div> - </div> - </div> - {% endif %} - {% endfor %} - {% else %} - <div class="message"> - <a href="{{link}}">{{message}}</a> - </div> +<div class="album"> + {% for photo in photos %} + {% if not photo.private or user.is_authenticated %} + {% include "photo-preview.html" %} {% endif %} + {% endfor %} + {% for photo in results %} + {% if not photo.private or user.is_authenticated %} + {% include "photo-preview.html" %} + {% endif %} + {% endfor %} + {% for album in albums %} + {% if not album.private or user.is_authenticated %} + {% include "album-preview.html" %} + {% endif %} + {% endfor %} + <div class="message"> + <a href="{{link}}">{{message}}</a> </div> +</div> {% endblock %} diff --git a/fotos/templates/base.html b/fotos/templates/base.html index 9bbe64c4a4d4e4cbfd0842dcaa4dffd331374927..bce4de40bf136f3ade2ae617f863372cc575fd08 100644 --- a/fotos/templates/base.html +++ b/fotos/templates/base.html @@ -134,7 +134,8 @@ </div> <hr/> <div class="top"> - <h2><span{% if highlighted %} class="bghighlight"{% endif %}>{{page_title}}</span></h2> + <h2><span class="{% if highlighted %}bghighlight{% endif %}">{{page_title}}</span> + <span class="{% if locked %}fa fa-lock lock{% endif %}"> </span></h2> {% if summary %}<div class="summary">{{summary}}</div>{% endif %} {% if search %} <div class="search-box"> diff --git a/fotos/templatetags/galeria.py b/fotos/templatetags/galeria.py index 2c778c5bd0392cd3de56258dca156654a70b32d3..9709d9b2db3b65dbc8eac4769db564438d203002 100644 --- a/fotos/templatetags/galeria.py +++ b/fotos/templatetags/galeria.py @@ -31,9 +31,18 @@ def next_page(value): def _range(min=1,max=5): return range(min,max+1) -@register.filter(name='photo_path') -def photo_path(value): - t = value.split('/') - if len(t) < 4: - return '' - return os.path.join(*t[-4:]) +@register.filter(name='get_highlight') +def get_highlight(photo, request): + if not photo.private or request.user.is_authenticated: + return photo + for p in photo.the_album.get_photos(): + if not p.private: + return p + message = _('This should not be possible: %(photo)s' % { 'photo': photo }) + raise ValueError(message) + +@register.filter(name='get_bgclass') +def get_bgclass(album, highlight): + if album == highlight: + return 'bghighlight' + return 'bgnormal' diff --git a/fotos/urls.py b/fotos/urls.py index bd0e441fe0bdd4b24492d41dea272bcd06aeb3bf..b1e3f3b6af1efcc2b42f82ba059303d29f581fd0 100644 --- a/fotos/urls.py +++ b/fotos/urls.py @@ -28,6 +28,8 @@ urlpatterns = [ re_path(r'^admin/map/?', views.map, name='map'), re_path(r'^admin/reset/?', views.reset, name='reset'), re_path(r'^admin/highlight/?', views.highlight, name='highlight'), + re_path(r'^admin/private/?', views.private, name='private'), + re_path(r'^admin/public/?', views.public, name='public'), re_path(r'^admin/delete/?', views.delete, name='delete'), re_path(r'^admin/logout/?', views.logout, name='logout'), re_path(r'^admin/login/?', views.login, name='login'), diff --git a/fotos/util.py b/fotos/util.py index 8e18efd0987e6afd6da84bc42fe5eb4bda8ad4dc..26b5b128f08c58f49c8bbf66df0cb38ca5491772 100644 --- a/fotos/util.py +++ b/fotos/util.py @@ -4,6 +4,8 @@ import sys import math from shutil import copyfile +from django.utils.translation import ngettext, gettext as _, ngettext + from galeria.settings import GALERIA_PREFIX, EXPORT_WIDTH, LANGUAGE_CODE, \ THUMB_MAX, VIEW_MAX, DEFAULT_LATITUDE, DEFAULT_LONGITUDE @@ -60,13 +62,17 @@ def fractions2float(fractions): i += 1 return d[0] + d[1] / 60.0 + d[2] / 3600.0 -def get_month_name(ts, tz=None, capitalize=False): +def get_month_name(ts, offset=0, capitalize=False): + tz = timezone(timedelta(hours=offset)) locale = LANGUAGE_CODE.replace('-', '_') name = format_datetime(ts, 'MMMM', locale=locale, tzinfo=tz) if capitalize: name = name.capitalize() return name +def get_year_name(year): + return _('Year %(year)s') % { 'year': year } + def get_image_metadata(path): image = Image.open(path) if image is None: @@ -118,7 +124,7 @@ def get_image_metadata(path): else: md['description'] = '' if TIME_ZONE in metadata: - md['time_offset'] = metadata.get(TIME_ZONE).value + md['time_offset'] = int(metadata.get(TIME_ZONE).value) else: md['time_offset'] = 0 tz = timezone(timedelta(hours=md['time_offset'])) diff --git a/fotos/views.py b/fotos/views.py index df33dea5edcf63ae2393d8b925a7912dd7201d41..b30799d5af6ff08e62f509805375bf54f4fb5775 100644 --- a/fotos/views.py +++ b/fotos/views.py @@ -93,7 +93,7 @@ def index(request): link = message = '' page_title = 'Galería' albums = Album.get_years() - if albums.count() == 0: + if not Album.has_public_years() or albums.count() == 0: page_title = _('The gallery is empty') if request.user.is_authenticated: link = PREFIX + '/admin/upload' @@ -126,7 +126,7 @@ def index(request): urlencode({'path': '/'})) attrs = { 'albums': albums, - 'albumtree': Album.full_tree(), + 'albumtree': Album.full_tree(request.user.is_authenticated), 'breadcrumbs': [], 'galeria': GALERIA_INFO, 'link': link, @@ -153,7 +153,7 @@ def index(request): def year(request, year): ya = Album.get_year(year) - if not ya: + if not ya:# or (ya.private and not request.user.is_authenticated): raise Http404(year + ' does not exist') months = ya.get_subalbums() @@ -181,19 +181,33 @@ def year(request, year): PREFIX + '/admin/delete?' + urlencode({'path': path})) + if ya.private: + access_icon = 'unlock' + access_name = _('Make year public') + access_url = PREFIX + '/admin/public?' + urlencode({'path': path}) + else: + access_icon = 'lock' + access_name = _('Make year private') + access_url = PREFIX + '/admin/private?' + urlencode({'path': path}) + highlight = ya.the_photo.the_album.the_parent attrs = { 'album': ya, 'albums': months, - 'albumtree': ya.get_tree(), + 'albumtree': ya.get_tree(request.user.is_authenticated), 'breadcrumbs': [ { 'link': ya.get_url_path(), 'name': ya.name } ], 'galeria': GALERIA_INFO, 'highlight': highlight, + 'locked': ya.private, 'menus': [{ 'confirm': True, 'icon': 'window-restore', 'id': 'menu-reset-hl', 'name': _('Reset year highlighted items'), 'onclick': reset_onclick}, + { 'link': access_url, + 'icon': access_icon, + 'id': 'menu-access', + 'name': access_name }, { 'confirm': True, 'icon': 'trash', 'id': 'menu-delete', @@ -221,9 +235,8 @@ def month(request, year, month): summary = (ngettext('One event', '%(events)s events', n_events) % { 'events': f'{n_events:n}' }) - tz = timezone(timedelta(hours=ma.time_offset)) page_title = (_('Month of %(month)s, %(year)s') % \ - { 'month': get_month_name(ma.first_timestamp, tz), + { 'month': get_month_name(ma.first_timestamp, ma.time_offset), 'year': ya.slug }) highlighted = (ya.the_photo == ma.the_photo) @@ -248,15 +261,23 @@ def month(request, year, month): urlencode({'path': path})) highlight_url = PREFIX + '/admin/highlight?' + urlencode({'path': path}) - highlight = ma.the_photo.the_album + if ma.private: + access_icon = 'unlock' + access_name = _('Make month public') + access_url = PREFIX + '/admin/public?' + urlencode({'path': path}) + else: + access_icon = 'lock' + access_name = _('Make month private') + access_url = PREFIX + '/admin/private?' + urlencode({'path': path}) + highlight = ma.the_photo.the_album page = request.GET.get('page', 1) paginator = Paginator(events, PAGINATION_ITEMS) the_page = paginator.get_page(page) attrs = { 'album': ma, 'albums': the_page, - 'albumtree': ma.get_tree(), + 'albumtree': ma.get_tree(request.user.is_authenticated), 'breadcrumbs': [ { 'link': ya.get_url_path(), 'name': ya.name }, { 'link': ma.get_url_path(), @@ -264,6 +285,7 @@ def month(request, year, month): 'galeria': GALERIA_INFO, 'highlight': highlight, 'highlighted': highlighted, + 'locked': ma.private, 'menus': [{ 'confirm': True, 'icon': 'window-restore', 'id': 'menu-reset-hl', @@ -273,6 +295,10 @@ def month(request, year, month): 'icon': 'check-circle', 'id': 'menu-highlight', 'name': _('Highlight month') }, + { 'link': access_url, + 'icon': access_icon, + 'id': 'menu-access', + 'name': access_name }, { 'confirm': True, 'icon': 'trash', 'id': 'menu-delete', @@ -293,7 +319,8 @@ def month(request, year, month): def event(request, year, month, event): ya, ma, ea = Album.get_event(year, month, event) - if not ya or not ma or not ea: + if not ya or not ma or not ea or \ + (ea.private and not request.user.is_authenticated): raise Http404(os.path.join(year, month, event) + ' does not exist') photos = ea.get_photos() @@ -324,6 +351,15 @@ def event(request, year, month, event): urlencode({'path': path})) highlight_url = PREFIX + '/admin/highlight?' + urlencode({'path': path}) + if ea.private: + access_icon = 'unlock' + access_name = _('Make event public') + access_url = PREFIX + '/admin/public?' + urlencode({'path': path}) + else: + access_icon = 'lock' + access_name = _('Make event private') + access_url = PREFIX + '/admin/private?' + urlencode({'path': path}) + highlight = ea.the_photo up = ma.get_url_path() page = request.GET.get('page', 1) @@ -348,6 +384,7 @@ def event(request, year, month, event): 'galeria': GALERIA_INFO, 'highlight': highlight, 'highlighted': highlighted, + 'locked': ea.private, 'menus': [{ 'link': map_url, 'icon': 'map', 'id': 'menu-map', @@ -361,6 +398,10 @@ def event(request, year, month, event): 'icon': 'check-circle', 'id': 'menu-highlight', 'name': _('Highlight event') }, + { 'link': access_url, + 'icon': access_icon, + 'id': 'menu-access', + 'name': access_name }, { 'confirm': True, 'icon': 'trash', 'id': 'menu-delete', @@ -382,7 +423,8 @@ def event(request, year, month, event): def photo(request, year, month, event, photo): ya, ma, ea, ph = Photo.get_photo(year, month, event, photo) - if not ya or not ma or not ea or not ph: + if not ya or not ma or not ea or not ph or \ + (ph.private and not request.user.is_authenticated): raise Http404(os.path.join(year, month, event, photo) + ' does not exist') bgcolor = 'bgnormal' if ea.the_photo != ph else 'bghighlight' @@ -406,6 +448,14 @@ def photo(request, year, month, event, photo): up += '?page=%d' % up_page highlight_url = PREFIX + '/admin/highlight?' + urlencode({'path': path}) + if ph.private: + access_icon = 'unlock' + access_name = _('Make photograph public') + access_url = PREFIX + '/admin/public?' + urlencode({'path': path}) + else: + access_icon = 'lock' + access_name = _('Make photograph private') + access_url = PREFIX + '/admin/private?' + urlencode({'path': path}) attrs = { 'breadcrumbs': [ { 'link': ya.get_url_path(), 'name': ya.name }, @@ -417,10 +467,15 @@ def photo(request, year, month, event, photo): 'name': ph.title } ], 'bgcolor': bgcolor, 'galeria': GALERIA_INFO, + 'locked': ph.private, 'menus': [{ 'link': highlight_url, 'icon': 'check-circle', 'id': 'menu-highlight', 'name': _('Highlight photograph') }, + { 'link': access_url, + 'icon': access_icon, + 'id': 'menu-access', + 'name': access_name }, { 'confirm': True, 'icon': 'trash', 'id': 'menu-delete', @@ -718,7 +773,7 @@ def login(request): if next: return redirect(next) token = Token.objects.get_or_create(user=request.user)[0] - message = _('User %(user)s authenticated.') % username + message = _('User %(user)s authenticated.') % { 'user': username } return render(request, 'info.html', { 'title': 'Información', 'galeria': GALERIA_INFO, @@ -788,9 +843,78 @@ def highlight(request): ya, ma = Album.get_month(*t) if not ya or not ma: raise Http404(os.path.join(year, month) + ' does not exist') + ma.highlight() return redirect(ma.get_url_path()) return redirect(PREFIX + '/') +@login_required +def private(request): + path = request.GET.get('path', '') + if path.endswith('/'): + path = path[:-1] + year = month = event = photo = None + t = path.split('/') + if len(t) == 4: + ya, ma, ea, ph = Photo.get_photo(*t) + if not ya or not ma or not ea or not ph: + raise Http404(os.path.join(year, month, event, photo) + + ' does not exist') + ph.make_private() + return redirect(ph.get_url_path()) + elif len(t) == 3: + ya, ma, ea = Album.get_event(*t) + if not ya or not ma or not ea: + raise Http404(os.path.join(year, month, event) + ' does not exist') + ea.make_private() + return redirect(ea.get_url_path()) + elif len(t) == 2: + ya, ma = Album.get_month(*t) + if not ya or not ma: + raise Http404(os.path.join(year, month) + ' does not exist') + ma.make_private() + return redirect(ma.get_url_path()) + elif len(t) == 1: + ya = Album.get_year(t[0]) + if not ya: + raise Http404(year + ' does not exist') + ya.make_private() + return redirect(ya.get_url_path()) + return redirect(PREFIX + '/') + +@login_required +def public(request): + path = request.GET.get('path', '') + if path.endswith('/'): + path = path[:-1] + year = month = event = photo = None + t = path.split('/') + if len(t) == 4: + ya, ma, ea, ph = Photo.get_photo(*t) + if not ya or not ma or not ea or not ph: + raise Http404(os.path.join(year, month, event, photo) + + ' does not exist') + ph.make_public() + return redirect(ph.get_url_path()) + elif len(t) == 3: + ya, ma, ea = Album.get_event(*t) + if not ya or not ma or not ea: + raise Http404(os.path.join(year, month, event) + ' does not exist') + ea.make_public() + return redirect(ea.get_url_path()) + elif len(t) == 2: + ya, ma = Album.get_month(*t) + if not ya or not ma: + raise Http404(os.path.join(year, month) + ' does not exist') + ma.make_public() + return redirect(ma.get_url_path()) + elif len(t) == 1: + ya = Album.get_year(t[0]) + if not ya: + raise Http404(year + ' does not exist') + ya.make_public() + return redirect(ya.get_url_path()) + return redirect(PREFIX + '/') + @login_required def download_highlights(request): token = Token.objects.get_or_create(user=request.user)[0] @@ -868,10 +992,14 @@ def list_photos(request): @api_view(['GET']) def random_photo(request): photo = Album.get_random_photo(request.GET.get('blacklist', None)) + if not photo: + return JsonResponse({ 'response': 'failure', + 'message': _('The gallery is empty') }) klass = 'gthumb' if photo.thumb_width > photo.thumb_height else 'gthumbv' image = STATIC_URL_PREFIX + photo.thumb_url link = photo.get_url_path() - return JsonResponse({ 'class': klass, + return JsonResponse({ 'response': 'success', + 'class': klass, 'image': image, 'link': link, 'title': photo.title }) diff --git a/galeria/settings.sample.py b/galeria/settings.sample.py index 2aff0934d1cd284ba285012da46fd44715f1ad35..6a9882ee6c8e7319e28abf0dc98c509462404764 100644 --- a/galeria/settings.sample.py +++ b/galeria/settings.sample.py @@ -10,6 +10,9 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.2/ref/settings/ """ +import os +import os.path + from pathlib import Path from datetime import datetime @@ -21,13 +24,14 @@ BASE_DIR = Path(__file__).resolve().parent.parent # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-4=b2093i%q)ypq1smb^00p7(=ap*1(7q4pi82g%!o3+a63vs23' +# Generate with django-admin startproject or with Python secrets module +SECRET_KEY = '' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True DEBUG_PROPAGATE_EXCEPTIONS = True -ALLOWED_HOSTS = [ '127.0.0.1', 'localhost', 'aztlan.fciencias.unam.mx' ] +ALLOWED_HOSTS = [ '127.0.0.1', 'localhost', YOUR_HOST ] # Application definition @@ -82,7 +86,7 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'OPTIONS': { - 'read_default_file': '/home/canek/django/galeria/galeria/mysql.cnf', + 'read_default_file': os.path.join(BASE_DIR, 'galeria/mysql.cnf'), 'init_command': 'SET foreign_key_checks = 0;', } } @@ -111,9 +115,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ -LANGUAGE_CODE = 'es-MX' +LANGUAGE_CODE = 'en-US' -TIME_ZONE = 'America/Mexico_City' +TIME_ZONE = 'UTC' USE_I18N = True @@ -121,13 +125,15 @@ USE_L10N = False USE_TZ = True +# Change it accordingly +HOME = os.getenv('HOME') # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -LOCALE_PATHS = [ '/home/canek/django/galeria/locale' ] +LOCALE_PATHS = [ os.path.join(BASE_DIR, 'locale') ] # Chante to True to run the django server RUN_DJANGO_SERVER = False @@ -136,7 +142,6 @@ if RUN_DJANGO_SERVER: # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ STATIC_URL = '/static/' - STATICFILES_DIRS = ('/home/canek/django/galeria/static', ) PREFIX = '' STATIC_URL_PREFIX = '/static/public' else: @@ -144,13 +149,14 @@ else: # https://docs.djangoproject.com/en/3.2/howto/static-files/ STATIC_URL = '/galeria/static/' PREFIX = '/galeria' - STATIC_URL_PREFIX = '/~canek' + STATIC_URL_PREFIX = '/' # /~user, for example -LOGIN_URL = PREFIX + '/admin/login' +LOGIN_URL = os.path.join(PREFIX, 'admin/login') -STATIC_PATH_PREFIX = '/home/canek/public_html' -GALERIA_PREFIX = STATIC_PATH_PREFIX + '/galeria/photos' -UPLOAD_TMP_DIR = 'home/canek/.tmp/galeria-images' +STATIC_PATH_PREFIX = os.path.join(HOME, 'public_html') +GALERIA_PREFIX = os.path.join(STATIC_PATH_PREFIX, 'galeria/photos') +# The TMP dir cannot be absolute +UPLOAD_TMP_DIR = os.path.join(HOME, '.tmp/galeria-images')[1:] PAGINATION_ITEMS = 12 @@ -158,14 +164,15 @@ EXPORT_WIDTH = 490 THUMB_MAX = 400 VIEW_MAX = 840 -GALERIA_INFO = { 'title': 'Fotos de Canek', - 'subtitle': 'La vida a través de una cámara digital', - 'owner': 'Canek Peláez Valdés', - 'email': 'canek@ciencias.unam.mx', - 'first_year': 1999, +GALERIA_INFO = { 'title': 'Your title', + 'subtitle': 'Your subtitle', + 'owner': 'Your name', + 'email': 'your@email.com', + 'first_year': 1900, # Start of copyright 'now_year': datetime.now().year, 'items_per_page': PAGINATION_ITEMS } +# Mexico City; change it to whatever you want DEFAULT_LATITUDE = 19.433333333 DEFAULT_LONGITUDE = -99.133333333 diff --git a/locale/es_MX/LC_MESSAGES/django.po b/locale/es_MX/LC_MESSAGES/django.po index 196552db343bdcbd4eb7b37ddfa0fa6c71eca8d6..37d990f6cbb515b943c91f0957c522d48144d9c3 100644 --- a/locale/es_MX/LC_MESSAGES/django.po +++ b/locale/es_MX/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: galeria 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-11-16 10:45-0600\n" -"PO-Revision-Date: 2021-11-16 10:46-0600\n" +"POT-Creation-Date: 2021-11-17 01:49-0600\n" +"PO-Revision-Date: 2021-11-17 01:51-0600\n" "Last-Translator: Canek Peláez Valdés <canek@ciencias.unam.mx>\n" "Language-Team: Spanish <canek@ciencias.unam.mx>\n" "Language: es_MX\n" @@ -26,32 +26,27 @@ msgstr "Archivos" msgid "File" msgstr "Archivo" -#: fotos/models.py:729 fotos/views.py:205 -#, python-format -msgid "Year %(year)s" -msgstr "Año %(year)s" - #: fotos/templates/admin-menu.html:6 fotos/templates/login.html:13 msgid "Go back to the gallery" msgstr "Regresar a la galería" #: fotos/templates/admin-menu.html:10 fotos/templates/base.html:48 -#: fotos/views.py:578 +#: fotos/views.py:633 msgid "Information" msgstr "Información" #: fotos/templates/admin-menu.html:14 fotos/templates/base.html:54 -#: fotos/templates/password.html:28 fotos/views.py:597 fotos/views.py:604 +#: fotos/templates/password.html:28 fotos/views.py:652 fotos/views.py:659 msgid "Change password" msgstr "Cambiar contraseña" #: fotos/templates/admin-menu.html:18 fotos/templates/base.html:60 -#: fotos/views.py:614 +#: fotos/views.py:669 msgid "REST API" msgstr "API REST" #: fotos/templates/admin-menu.html:22 fotos/templates/api.html:161 -#: fotos/templates/base.html:67 fotos/views.py:800 +#: fotos/templates/base.html:67 fotos/views.py:924 msgid "Download highlighted items" msgstr "Descargar elementos destacados" @@ -62,7 +57,7 @@ msgstr "Cargar elementos destacados" #: fotos/templates/admin-menu.html:30 fotos/templates/api.html:79 #: fotos/templates/base.html:79 fotos/templates/upload.html:42 -#: fotos/views.py:650 +#: fotos/views.py:705 msgid "Upload photographs" msgstr "Cargar fotografías" @@ -219,8 +214,8 @@ msgstr "" "<li><strong>Uso:</strong>" #: fotos/templates/api.html:138 fotos/views.py:110 fotos/views.py:113 -#: fotos/views.py:169 fotos/views.py:232 fotos/views.py:235 fotos/views.py:308 -#: fotos/views.py:311 +#: fotos/views.py:169 fotos/views.py:245 fotos/views.py:248 fotos/views.py:335 +#: fotos/views.py:338 msgid "Reset highlighted items" msgstr "Restablecer elementos destacados" @@ -304,36 +299,36 @@ msgstr "Seleccionar subálbum" msgid "Home" msgstr "Inicio" -#: fotos/templates/base.html:145 +#: fotos/templates/base.html:146 msgid "Search" msgstr "Buscar" -#: fotos/templates/base.html:155 +#: fotos/templates/base.html:156 msgid "Previous" msgstr "Anterior" -#: fotos/templates/base.html:160 +#: fotos/templates/base.html:161 msgid "Next" msgstr "Siguiente" -#: fotos/templates/base.html:165 +#: fotos/templates/base.html:166 msgid "Up" msgstr "Arriba" -#: fotos/templates/base.html:180 fotos/templates/base.html:187 -#: fotos/templates/base.html:193 +#: fotos/templates/base.html:181 fotos/templates/base.html:188 +#: fotos/templates/base.html:194 msgid "Page" msgstr "Página" -#: fotos/templates/base.html:199 +#: fotos/templates/base.html:200 msgid "of" msgstr "de" -#: fotos/templates/base.html:211 +#: fotos/templates/base.html:212 msgid "Valid XHTML 1.0 Strict" msgstr "XHTML 1.0 Estricto válido" -#: fotos/templates/base.html:217 +#: fotos/templates/base.html:218 msgid "Valid CSS!" msgstr "CSS Válido!" @@ -536,12 +531,22 @@ msgstr "Suba un archivo JSON con los elementos destacados de la galería." msgid "Upload JSON" msgstr "Cargar JSON" +#: fotos/templatetags/galeria.py:41 +#, python-format +msgid "This should not be possible: %(photo)s" +msgstr "Esto no debería ser posible: %(photo)s" + +#: fotos/util.py:74 fotos/views.py:219 +#, python-format +msgid "Year %(year)s" +msgstr "Año %(year)s" + #: fotos/views.py:65 #, python-format msgid "Error deleting: %(error)s\n" msgstr "Error al eliminar: %(error)s\n" -#: fotos/views.py:97 +#: fotos/views.py:97 fotos/views.py:997 msgid "The gallery is empty" msgstr "La galería está vacía" @@ -571,8 +576,8 @@ msgstr "" "seguro de esto?" #: fotos/views.py:114 fotos/views.py:123 fotos/views.py:170 fotos/views.py:179 -#: fotos/views.py:236 fotos/views.py:245 fotos/views.py:312 fotos/views.py:321 -#: fotos/views.py:394 fotos/views.py:544 +#: fotos/views.py:249 fotos/views.py:258 fotos/views.py:339 fotos/views.py:348 +#: fotos/views.py:436 fotos/views.py:599 msgid "Cancel" msgstr "Cancelar" @@ -587,7 +592,7 @@ msgstr "" "Esto eliminará todas las fotos de la galería de manera permanente. ¿Está " "seguro de esto?" -#: fotos/views.py:122 fotos/views.py:178 fotos/views.py:244 fotos/views.py:320 +#: fotos/views.py:122 fotos/views.py:178 fotos/views.py:257 fotos/views.py:347 msgid "Delete photographs" msgstr "Eliminar fotografías" @@ -602,7 +607,7 @@ msgid_plural "%(months)s months" msgstr[0] "Un mes" msgstr[1] "%(months)s meses" -#: fotos/views.py:166 fotos/views.py:195 +#: fotos/views.py:166 fotos/views.py:205 msgid "Reset year highlighted items" msgstr "Restablecer elementos destacados en el año" @@ -612,7 +617,7 @@ msgstr "" "Esto restablecerá todos los elementos destacados en el año. ¿Está seguro de " "esto?" -#: fotos/views.py:175 fotos/views.py:200 +#: fotos/views.py:175 fotos/views.py:214 msgid "Delete year" msgstr "Eliminar año" @@ -623,210 +628,242 @@ msgstr "" "Esto eliminará todas las fotos del año de manera permanente. ¿Está seguro de " "esto?" -#: fotos/views.py:221 +#: fotos/views.py:186 +msgid "Make year public" +msgstr "Hacer público al año" + +#: fotos/views.py:190 +msgid "Make year private" +msgstr "Hacer privado al año" + +#: fotos/views.py:235 #, python-format msgid "One event" msgid_plural "%(events)s events" msgstr[0] "Un evento" msgstr[1] "%(events)s eventos" -#: fotos/views.py:225 +#: fotos/views.py:238 #, python-format msgid "Month of %(month)s, %(year)s" msgstr "Mes de %(month)s, %(year)s" -#: fotos/views.py:233 +#: fotos/views.py:246 msgid "This will reset all the month highlighted items. Are you sure?" msgstr "" "Esto reestablecerá todos los elementos destacados en el mes. ¿Está seguro de " "esto?" -#: fotos/views.py:241 fotos/views.py:279 +#: fotos/views.py:254 fotos/views.py:305 msgid "Delete month" msgstr "Eliminar mes" -#: fotos/views.py:242 +#: fotos/views.py:255 msgid "" "This will delete all the photographs from the month forever. Are you sure?" msgstr "" "Esto eliminará todas las fotos del mes de manera permanente. ¿Está seguro de " "esto?" -#: fotos/views.py:270 fotos/views.py:358 +#: fotos/views.py:266 +msgid "Make month public" +msgstr "Hacer público al mes" + +#: fotos/views.py:270 +msgid "Make month private" +msgstr "Hacer privado al mes" + +#: fotos/views.py:292 fotos/views.py:395 msgid "Reset month highlighted items" msgstr "Restablecer elementos destacados en el mes" -#: fotos/views.py:275 +#: fotos/views.py:297 msgid "Highlight month" msgstr "Destacar mes" -#: fotos/views.py:301 fotos/views.py:457 fotos/views.py:685 +#: fotos/views.py:328 fotos/views.py:512 fotos/views.py:740 #, python-format msgid "One photograph" msgid_plural "%(photos)s photographs" msgstr[0] "Una fotografía" msgstr[1] "%(photos)s fotografías" -#: fotos/views.py:309 +#: fotos/views.py:336 msgid "This will reset all the event highlighted items. Are you sure?" msgstr "" "Esto restablecerá todos los elementos destacados en el evento. ¿Está seguro " "de esto?<" -#: fotos/views.py:317 fotos/views.py:367 +#: fotos/views.py:344 fotos/views.py:408 msgid "Delete event" msgstr "Eliminar evento" -#: fotos/views.py:318 +#: fotos/views.py:345 msgid "" "This will delete all the photographs from the event forever. Are you sure?" msgstr "" "Esto eliminará todas las fotos del evento de manera permanente. ¿Está seguro " "de esto?" -#: fotos/views.py:354 +#: fotos/views.py:356 +msgid "Make event public" +msgstr "Hacer público al evento" + +#: fotos/views.py:360 +msgid "Make event private" +msgstr "Hacer privado al evento" + +#: fotos/views.py:391 msgid "Event map" msgstr "Mapa del evento" -#: fotos/views.py:363 +#: fotos/views.py:400 msgid "Highlight event" msgstr "Destacar evento" -#: fotos/views.py:391 fotos/views.py:393 fotos/views.py:427 fotos/views.py:541 -#: fotos/views.py:543 fotos/views.py:561 +#: fotos/views.py:433 fotos/views.py:435 fotos/views.py:482 fotos/views.py:596 +#: fotos/views.py:598 fotos/views.py:616 msgid "Delete photograph" msgstr "Eliminar fotografía" -#: fotos/views.py:392 fotos/views.py:542 +#: fotos/views.py:434 fotos/views.py:597 msgid "This will delete the photograph forever. Are you sure?" msgstr "Esto eliminará la foto de manera permanente. ¿Está seguro de esto?" -#: fotos/views.py:423 +#: fotos/views.py:453 +msgid "Make photograph public" +msgstr "Hacer pública la fotografía" + +#: fotos/views.py:457 +msgid "Make photograph private" +msgstr "Hacer privada la fotografía" + +#: fotos/views.py:474 msgid "Highlight photograph" msgstr "Destacar fotografía" -#: fotos/views.py:451 +#: fotos/views.py:506 msgid "There are no photographs matching the search" msgstr "No hay fotografías que cacen la búsqueda" -#: fotos/views.py:452 +#: fotos/views.py:507 msgid "Search with no results" msgstr "Búsqueda sin resultados" -#: fotos/views.py:453 +#: fotos/views.py:508 msgid "Check the search term" msgstr "Revise el término de búsqueda" -#: fotos/views.py:455 +#: fotos/views.py:510 #, python-format msgid "Search results: “%(term)s”" msgstr "Resultados de la búsqueda: “%(term)s”" -#: fotos/views.py:466 fotos/views.py:550 +#: fotos/views.py:521 fotos/views.py:605 #, python-format msgid "Search: “%(term)s”" msgstr "Búsqueda: “%(term)s”" -#: fotos/views.py:592 +#: fotos/views.py:647 msgid "Password updated." msgstr "Contraseña actualizada." -#: fotos/views.py:595 +#: fotos/views.py:650 msgid "Invalid password." msgstr "Contraseña inválida." -#: fotos/views.py:634 +#: fotos/views.py:689 #, python-format msgid "The file was successfully uploaded." msgid_plural "A total of %(files)s files were successfully uploaded." msgstr[0] "El archivo se cargó exitosamente." msgstr[1] "Se cargaron exitosamente %(files)s archivos." -#: fotos/views.py:641 +#: fotos/views.py:696 msgid "Invalid request." msgstr "Solicitud inválida." -#: fotos/views.py:643 +#: fotos/views.py:698 msgid "Something went wrong." msgstr "Algo salió mal." -#: fotos/views.py:684 +#: fotos/views.py:739 #, python-format msgid "Map of %(map)s" msgstr "Mapa de %(map)s" -#: fotos/views.py:695 +#: fotos/views.py:750 msgid "Mapa" msgstr "Map" -#: fotos/views.py:721 +#: fotos/views.py:776 #, python-format msgid "User %(user)s authenticated." msgstr "Usuario %(user)s autenticado." -#: fotos/views.py:732 +#: fotos/views.py:787 msgid "Invalid username or password." msgstr "Usuario o contraseña inválidos." -#: fotos/views.py:741 +#: fotos/views.py:796 msgid "Login into Galería" msgstr "Iniciar sesión en Galería" -#: fotos/views.py:807 fotos/views.py:830 fotos/views.py:937 +#: fotos/views.py:931 fotos/views.py:954 fotos/views.py:1065 #, python-format msgid "Invalid %(kind)s: %(name)s" msgstr "Elemento %(kind)s inválido: %(name)s" -#: fotos/views.py:811 +#: fotos/views.py:935 msgid "Upload highlighted elements" msgstr "Cargar elementos destacados" -#: fotos/views.py:834 fotos/views.py:940 +#: fotos/views.py:958 fotos/views.py:1068 msgid "The highlighted items were successfully uploaded." msgstr "Los elementos destacados se cargaron exitosamente." -#: fotos/views.py:839 +#: fotos/views.py:963 #, python-format msgid "Invalid file %(file)s." msgstr "Archivo inválido: %(file)s" -#: fotos/views.py:895 +#: fotos/views.py:1023 msgid "Upload successful" msgstr "Carga exitosa." -#: fotos/views.py:898 +#: fotos/views.py:1026 msgid "No files provided" msgstr "No se proporcionaron archivos" -#: fotos/views.py:901 +#: fotos/views.py:1029 msgid "Error trying to upload photo" msgstr "Error al tratar de cargar fotografías" -#: fotos/views.py:909 +#: fotos/views.py:1037 #, python-format msgid "Error deleting item: %(item)s" msgstr "Error eliminando elemento: %(item)s" -#: fotos/views.py:911 +#: fotos/views.py:1039 msgid "Item deleted" msgstr "Elemento eliminado" -#: fotos/views.py:919 +#: fotos/views.py:1047 msgid "Highlighted items reset" msgstr "Elementos destacados restablecidos" -#: fotos/views.py:933 +#: fotos/views.py:1061 msgid "Invalid request" msgstr "Solicitud inválida" -#: fotos/views.py:947 +#: fotos/views.py:1075 msgid "The requested page does not exists." msgstr "La página solicitada no existe." -#: fotos/views.py:950 +#: fotos/views.py:1078 msgid "Page not found" msgstr "Página no encontrada" -#: fotos/views.py:953 +#: fotos/views.py:1081 msgid "Check the URL" msgstr "Revise la dirección" diff --git a/static/admin.css b/static/admin.css index cff771246f8e8664acd4952b38c52c50533c2b91..e6c75896025de33d619bf58f38a5993dc9a8be05 100644 --- a/static/admin.css +++ b/static/admin.css @@ -16,7 +16,7 @@ h2 { div.sidebar { float: left; - width: 200px; + width: 250px; height: 100%; text-shadow: 3px 3px 3px #999; margin-bottom: 0px; @@ -38,7 +38,7 @@ div.menu-item > a:active { } div.content { - margin-left: 210px; + margin-left: 260px; } div.progress { diff --git a/static/galeria.js b/static/galeria.js index 0180bd0fcc244a77f96f181928f103f3fe5c67a2..9e7297064c9875dfd5215b695e55df1a5dd63194 100644 --- a/static/galeria.js +++ b/static/galeria.js @@ -90,6 +90,8 @@ document.addEventListener("keydown", function onEvent(event) { clickElement("prev-page"); } else if (event.shiftKey && event.altKey && event.key == "PageDown") { clickElement("next-page"); + } else if (event.ctrlKey && event.altKey && _isKey(event.key, "C", "c")) { + clickElement("menu-access"); } else if (event.ctrlKey && event.altKey && _isKey(event.key, "I", "i")) { clickElement("menu-info"); } else if (event.ctrlKey && event.altKey && _isKey(event.key, "P", "p")) { diff --git a/static/style.css b/static/style.css index b3f06e7e282a06e87562e4ccca4e3fa86ee6fafd..c9171f408d1ba18f7bb3ae3cbbffcfbe2357f0c8 100644 --- a/static/style.css +++ b/static/style.css @@ -104,6 +104,10 @@ span.highlighted { font-size: small; } +span.lock { + font-size: 0.7em; +} + div.summary { margin-left: 20px; margin-top: 5px; @@ -299,7 +303,7 @@ div.footer > a:active { position: absolute; right: 0px; background-color: #f1f1f1; - min-width: 320px; + min-width: 380px; overflow: auto; box-shadow: 8px 8px 8px #222; z-index: 1;