diff options
author | luxagraf <sng@luxagraf.net> | 2022-12-02 14:16:08 -0600 |
---|---|---|
committer | luxagraf <sng@luxagraf.net> | 2022-12-02 14:16:08 -0600 |
commit | 656505098a80e653319236ac302fd6dd9f485b33 (patch) | |
tree | 03fe2f552496e2a2b459f5227dc11273d1b94211 /app | |
parent | bf2fa131cba6430ba93f584f4693c3444e0c455f (diff) |
reset migrations to zero out some changes (deleting the geodata for
example)
Diffstat (limited to 'app')
53 files changed, 1067 insertions, 470 deletions
diff --git a/app/accounts/migrations/0001_initial.py b/app/accounts/migrations/0001_initial.py deleted file mode 100644 index 36aa6ba..0000000 --- a/app/accounts/migrations/0001_initial.py +++ /dev/null @@ -1,55 +0,0 @@ -# Generated by Django 2.1.2 on 2018-11-24 04:41 - -from django.conf import settings -import django.contrib.auth.models -import django.contrib.auth.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0009_alter_user_last_name_max_length'), - ] - - operations = [ - migrations.CreateModel( - name='User', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), - ], - options={ - 'ordering': ['-date_joined'], - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='UserProfile', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('photo', models.ImageField(blank=True, null=True, upload_to='profile')), - ('website', models.CharField(blank=True, default='', max_length=300, null=True)), - ('location', models.CharField(blank=True, default='', max_length=300, null=True)), - ('bio', models.TextField(blank=True, default='', null=True)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/app/accounts/migrations/0002_auto_20190108_2115.py b/app/accounts/migrations/0002_auto_20190108_2115.py deleted file mode 100644 index 1ebb280..0000000 --- a/app/accounts/migrations/0002_auto_20190108_2115.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 2.1.2 on 2019-01-09 03:15 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='userprofile', - name='bio', - field=models.CharField(blank=True, default='', max_length=350), - ), - migrations.AlterField( - model_name='userprofile', - name='location', - field=models.CharField(blank=True, default='', max_length=300), - ), - migrations.AlterField( - model_name='userprofile', - name='user', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='userprofile', - name='website', - field=models.CharField(blank=True, default='', max_length=300), - ), - ] diff --git a/app/books/migrations/0001_initial.py b/app/books/migrations/0001_initial.py index 6257e40..03447a1 100644 --- a/app/books/migrations/0001_initial.py +++ b/app/books/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.6 on 2021-08-14 12:50 +# Generated by Django 4.1.3 on 2022-12-02 20:08 import books.models from django.db import migrations, models @@ -28,6 +28,7 @@ class Migration(migrations.Migration): ('amazon_link', models.CharField(blank=True, max_length=400, null=True)), ('bookshop_link', models.CharField(blank=True, max_length=400, null=True)), ('thriftbooks_link', models.CharField(blank=True, max_length=400, null=True)), + ('other_link', models.CharField(blank=True, max_length=400, null=True)), ('image', models.FileField(blank=True, null=True, upload_to=books.models.get_upload_path)), ], options={ diff --git a/app/books/migrations/0002_book_other_link.py b/app/books/migrations/0002_book_other_link.py deleted file mode 100644 index bf64351..0000000 --- a/app/books/migrations/0002_book_other_link.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.6 on 2021-08-14 13:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('books', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='book', - name='other_link', - field=models.CharField(blank=True, max_length=400, null=True), - ), - ] diff --git a/app/lib/django_comments/abstracts.py b/app/lib/django_comments/abstracts.py index e74ea02..5428f1a 100644 --- a/app/lib/django_comments/abstracts.py +++ b/app/lib/django_comments/abstracts.py @@ -9,7 +9,7 @@ from django.utils.html import mark_safe from django.db import models from django.utils import timezone from six import python_2_unicode_compatible -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ try: from django.urls import reverse except ImportError: diff --git a/app/lib/django_comments/admin.py b/app/lib/django_comments/admin.py index 8451c70..6a1f5f8 100644 --- a/app/lib/django_comments/admin.py +++ b/app/lib/django_comments/admin.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.contrib import admin from django.contrib.auth import get_user_model -from django.utils.translation import ugettext_lazy as _, ungettext +from django.utils.translation import gettext_lazy as _, ngettext from django_comments import get_model from django_comments.views.moderation import perform_flag, perform_approve, perform_delete diff --git a/app/lib/django_comments/forms.py b/app/lib/django_comments/forms.py index 7b7eafd..b4de54d 100644 --- a/app/lib/django_comments/forms.py +++ b/app/lib/django_comments/forms.py @@ -5,10 +5,10 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.forms.utils import ErrorDict from django.utils.crypto import salted_hmac, constant_time_compare -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.utils.text import get_text_list from django.utils import timezone -from django.utils.translation import pgettext_lazy, ungettext, ugettext, ugettext_lazy as _ +from django.utils.translation import pgettext_lazy, ngettext, gettext, gettext_lazy as _ from . import get_model @@ -137,7 +137,7 @@ class CommentDetailsForm(CommentSecurityForm): """ return dict( content_type=ContentType.objects.get_for_model(self.target_object), - object_pk=force_text(self.target_object._get_pk_val()), + object_pk=force_str(self.target_object._get_pk_val()), user_name=self.cleaned_data["name"], user_email=self.cleaned_data["email"], user_url=self.cleaned_data["url"], diff --git a/app/lib/django_comments/managers.py b/app/lib/django_comments/managers.py index 9e1fc77..33d9e2a 100644 --- a/app/lib/django_comments/managers.py +++ b/app/lib/django_comments/managers.py @@ -1,6 +1,6 @@ from django.db import models from django.contrib.contenttypes.models import ContentType -from django.utils.encoding import force_text +from django.utils.encoding import force_str class CommentManager(models.Manager): @@ -18,5 +18,5 @@ class CommentManager(models.Manager): ct = ContentType.objects.get_for_model(model) qs = self.get_queryset().filter(content_type=ct) if isinstance(model, models.Model): - qs = qs.filter(object_pk=force_text(model._get_pk_val())) + qs = qs.filter(object_pk=force_str(model._get_pk_val())) return qs diff --git a/app/lib/django_comments/models.py b/app/lib/django_comments/models.py index 204cf2e..6eac46b 100644 --- a/app/lib/django_comments/models.py +++ b/app/lib/django_comments/models.py @@ -2,7 +2,7 @@ from six import python_2_unicode_compatible from django.conf import settings from django.db import models from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from .abstracts import ( COMMENT_MAX_LENGTH, BaseCommentAbstractModel, CommentAbstractModel, diff --git a/app/lib/django_comments/moderation.py b/app/lib/django_comments/moderation.py index 3e5c412..39ac356 100644 --- a/app/lib/django_comments/moderation.py +++ b/app/lib/django_comments/moderation.py @@ -62,7 +62,7 @@ from django.core.mail import send_mail from django.db.models.base import ModelBase from django.template import loader from django.utils import timezone -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ import django_comments from django_comments import signals diff --git a/app/lib/django_comments/signals.py b/app/lib/django_comments/signals.py index 079afaf..3aac192 100644 --- a/app/lib/django_comments/signals.py +++ b/app/lib/django_comments/signals.py @@ -9,13 +9,14 @@ from django.dispatch import Signal # discarded and a 400 response. This signal is sent at more or less # the same time (just before, actually) as the Comment object's pre-save signal, # except that the HTTP request is sent along with this signal. -comment_will_be_posted = Signal(providing_args=["comment", "request"]) + +comment_will_be_posted = Signal() # providing_args=["comment", "request"] # Sent just after a comment was posted. See above for how this differs # from the Comment object's post-save signal. -comment_was_posted = Signal(providing_args=["comment", "request"]) +comment_was_posted = Signal() # providing_args=["comment", "request"] # Sent after a comment was "flagged" in some way. Check the flag to see if this # was a user requesting removal of a comment, a moderator approving/removing a # comment, or some other custom user flag. -comment_was_flagged = Signal(providing_args=["comment", "flag", "created", "request"]) +comment_was_flagged = Signal() # providing_args=["comment", "flag", "created", "request"] diff --git a/app/lib/django_comments/templates/comments/form.html b/app/lib/django_comments/templates/comments/form.html index 858d5eb..939f7e1 100644 --- a/app/lib/django_comments/templates/comments/form.html +++ b/app/lib/django_comments/templates/comments/form.html @@ -9,7 +9,7 @@ {% if field.errors %}{{ field.errors }}{% endif %} <p {% if field.errors %} class="error"{% endif %} - {% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}> + {% if field.name == "honeypot" %} style="display:none;"{% endif %}> {{ field.label_tag }} {{ field }} </p> {% endif %} diff --git a/app/lib/django_comments/templates/comments/preview.html b/app/lib/django_comments/templates/comments/preview.html index e335466..9fd5f1c 100644 --- a/app/lib/django_comments/templates/comments/preview.html +++ b/app/lib/django_comments/templates/comments/preview.html @@ -27,7 +27,7 @@ {% if field.errors %}{{ field.errors }}{% endif %} <p {% if field.errors %} class="error"{% endif %} - {% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}> + {% if field.name == "honeypot" %} style="display:none;"{% endif%}> {{ field.label_tag }} {{ field }} </p> {% endif %} diff --git a/app/lib/django_comments/templatetags/comments.py b/app/lib/django_comments/templatetags/comments.py index 9b2d1a4..440a8f6 100644 --- a/app/lib/django_comments/templatetags/comments.py +++ b/app/lib/django_comments/templatetags/comments.py @@ -3,7 +3,7 @@ from django.template.loader import render_to_string from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib.sites.shortcuts import get_current_site -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str import django_comments @@ -85,7 +85,7 @@ class BaseCommentNode(template.Node): qs = self.comment_model.objects.filter( content_type=ctype, - object_pk=smart_text(object_pk), + object_pk=smart_str(object_pk), site__pk=site_id, ) diff --git a/app/lib/django_comments/urls.py b/app/lib/django_comments/urls.py index 45599dc..47d5b48 100644 --- a/app/lib/django_comments/urls.py +++ b/app/lib/django_comments/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path, re_path from django.contrib.contenttypes.views import shortcut from .views.comments import post_comment, comment_done @@ -8,14 +8,14 @@ from .views.moderation import ( urlpatterns = [ - url(r'^post/$', post_comment, name='comments-post-comment'), - url(r'^posted/$', comment_done, name='comments-comment-done'), - url(r'^flag/(\d+)/$', flag, name='comments-flag'), - url(r'^flagged/$', flag_done, name='comments-flag-done'), - url(r'^delete/(\d+)/$', delete, name='comments-delete'), - url(r'^deleted/$', delete_done, name='comments-delete-done'), - url(r'^approve/(\d+)/$', approve, name='comments-approve'), - url(r'^approved/$', approve_done, name='comments-approve-done'), + re_path(r'^post/$', post_comment, name='comments-post-comment'), + re_path(r'^posted/$', comment_done, name='comments-comment-done'), + re_path(r'^flag/(\d+)/$', flag, name='comments-flag'), + re_path(r'^flagged/$', flag_done, name='comments-flag-done'), + re_path(r'^delete/(\d+)/$', delete, name='comments-delete'), + re_path(r'^deleted/$', delete_done, name='comments-delete-done'), + re_path(r'^approve/(\d+)/$', approve, name='comments-approve'), + re_path(r'^approved/$', approve_done, name='comments-approve-done'), - url(r'^cr/(\d+)/(.+)/$', shortcut, name='comments-url-redirect'), + re_path(r'^cr/(\d+)/(.+)/$', shortcut, name='comments-url-redirect'), ] diff --git a/app/lib/django_comments/views/utils.py b/app/lib/django_comments/views/utils.py index a5f5c11..793fc43 100644 --- a/app/lib/django_comments/views/utils.py +++ b/app/lib/django_comments/views/utils.py @@ -12,7 +12,7 @@ except ImportError: # Python 2 from django.http import HttpResponseRedirect from django.shortcuts import render, resolve_url from django.core.exceptions import ObjectDoesNotExist -from django.utils.http import is_safe_url +from django.utils.http import url_has_allowed_host_and_scheme import django_comments @@ -28,7 +28,7 @@ def next_redirect(request, fallback, **get_kwargs): Returns an ``HttpResponseRedirect``. """ next = request.POST.get('next') - if not is_safe_url(url=next, allowed_hosts={request.get_host()}): + if not url_has_allowed_host_and_scheme(url=next, allowed_hosts={request.get_host()}): next = resolve_url(fallback) if get_kwargs: diff --git a/app/lib/templatetags/templatetags/number_to_word.py b/app/lib/templatetags/templatetags/number_to_word.py index c153932..5aa4eaf 100644 --- a/app/lib/templatetags/templatetags/number_to_word.py +++ b/app/lib/templatetags/templatetags/number_to_word.py @@ -1,4 +1,4 @@ -from django.utils.translation import ungettext, ugettext as _ +from django.utils.translation import gettext as _ import re from django import template from django.utils.safestring import mark_safe @@ -26,4 +26,4 @@ def number_to_word(value): else: word = PRIME_NUM[int(value[:1])-1] return word -
\ No newline at end of file + diff --git a/app/media/0002_auto_20201201_2054.py b/app/media/0002_auto_20201201_2054.py new file mode 100644 index 0000000..843f48b --- /dev/null +++ b/app/media/0002_auto_20201201_2054.py @@ -0,0 +1,46 @@ +# Generated by Django 3.1 on 2020-12-01 19:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0001_initial'), + ] + + operations = [ + migrations.RunSQL(""" + INSERT INTO media_luximagesize ( + id, + name, + width, + height, + quality + ) + SELECT + id, + name, + width, + height, + quality + FROM + photos_luximagesize; + """, reverse_sql=""" + INSERT INTO photos_luximagesize ( + id, + name, + width, + height, + quality + ) + SELECT + id, + name, + width, + height, + quality + FROM + media_luximagesize; + """) + ] diff --git a/app/media/0003_auto_20201201_2055.py b/app/media/0003_auto_20201201_2055.py new file mode 100644 index 0000000..4aeec12 --- /dev/null +++ b/app/media/0003_auto_20201201_2055.py @@ -0,0 +1,118 @@ +# Generated by Django 3.1 on 2020-12-01 20:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0002_auto_20201201_2054'), + ] + + operations = [ + migrations.RunSQL(""" + INSERT INTO media_luximage ( + id, + image, + title, + alt, + photo_credit_source, + photo_credit_url, + caption, + pub_date, + height, + width, + is_public, + exif_raw, + exif_aperture, + exif_make, + exif_model, + exif_exposure, + exif_iso, + exif_focal_length, + exif_lens, + exif_date, + point, + location, + sizes + ) + SELECT + id, + image, + title, + alt, + photo_credit_source, + photo_credit_url, + caption, + pub_date, + height, + width, + is_public, + exif_raw, + exif_aperture, + exif_make, + exif_model, + exif_exposure, + exif_iso, + exif_focal_length, + exif_lens, + exif_date, + point, + location, + sizes + FROM + photos_luximage; + """, reverse_sql=""" + INSERT INTO photos_luximage ( + id, + image, + title, + alt, + photo_credit_source, + photo_credit_url, + caption, + pub_date, + height, + width, + is_public, + sizes, + exif_raw, + exif_aperture, + exif_make, + exif_model, + exif_exposure, + exif_iso, + exif_focal_length, + exif_lens, + exif_date, + point, + location + ) + SELECT + id, + image, + title, + alt, + photo_credit_source, + photo_credit_url, + caption, + pub_date, + height, + width, + is_public, + sizes, + exif_raw, + exif_aperture, + exif_make, + exif_model, + exif_exposure, + exif_iso, + exif_focal_length, + exif_lens, + exif_date, + point, + location + FROM + media_luximage; + """) + ] diff --git a/app/media/admin.py b/app/media/admin.py index 12d0509..50eb879 100644 --- a/app/media/admin.py +++ b/app/media/admin.py @@ -1,12 +1,15 @@ from django.contrib import admin +from django import forms from django.contrib.gis.admin import OSMGeoAdmin - -from .models import LuxImage, LuxGallery, LuxImageSize, LuxVideo, LuxAudio +from .models import LuxImage, LuxGallery, LuxImageSize, LuxVideo +from django.shortcuts import render +from django.contrib.admin import helpers +from django.http import HttpResponseRedirect @admin.register(LuxImageSize) class LuxImageSizeAdmin(OSMGeoAdmin): - list_display = ('name', 'width', 'height', 'quality') + list_display = ('name','slug', 'width', 'height', 'quality') @admin.register(LuxVideo) @@ -18,20 +21,44 @@ class LuxVideoAdmin(OSMGeoAdmin): class LuxImageAdmin(OSMGeoAdmin): list_display = ('pk', 'admin_thumbnail', 'pub_date', 'caption') list_filter = ('pub_date',) - search_fields = ['title', 'caption'] + search_fields = ['title', 'caption', 'alt'] # Options for OSM map Using custom ESRI topo map + default_lon = -9285175 + default_lat = 4025046 + default_zoom = 6 + units = True + scrollable = False + map_width = 700 + map_height = 425 + map_template = 'gis/admin/osm.html' + openlayers_url = '/static/admin/js/OpenLayers.js' fieldsets = ( (None, { - 'fields': ('title', ('image'), 'pub_date', 'sizes', 'alt', 'caption', ('is_public'), ('photo_credit_source', 'photo_credit_url')) + 'fields': ( + 'image', + 'alt', + 'sizes', + 'caption', + 'pub_date', + 'title', + ) + }), + ('Exif and Other Data', { + 'classes': ('collapse',), + 'fields': ( + 'point', + ('is_public'), + ('photo_credit_source', 'photo_credit_url'), + 'exif_raw', 'exif_aperture', 'exif_make', 'exif_model', 'exif_exposure', 'exif_iso', 'exif_focal_length', 'exif_lens', 'exif_date', 'height', 'width'), }), ) + def save_related(self, request, form, formsets, change): + super(LuxImageAdmin, self).save_related(request, form, formsets, change) + if not form.instance.sizes.all(): + print("there are no sizes") + form.instance.sizes.add(*LuxImageSize.objects.filter(slug__in=["picwide-sm", "picwide-med", "picwide"])) + class Media: js = ('image-preview.js', 'next-prev-links.js') - - -@admin.register(LuxAudio) -class LuxAudioAdmin(OSMGeoAdmin): - list_display = ('pk', 'title', 'pub_date') - list_filter = ('pub_date',) diff --git a/app/media/migrations/0001_initial.py b/app/media/migrations/0001_initial.py index 8ca4631..886a36e 100644 --- a/app/media/migrations/0001_initial.py +++ b/app/media/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 3.1.3 on 2020-11-30 22:44 +# Generated by Django 4.1.3 on 2022-12-02 20:09 import datetime +import django.contrib.gis.db.models.fields from django.db import migrations, models import django.db.models.deletion import media.models @@ -17,7 +18,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='LuxAudio', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=200)), ('subtitle', models.CharField(blank=True, max_length=200)), ('slug', models.SlugField(blank=True, unique_for_date='pub_date')), @@ -26,6 +27,7 @@ class Migration(migrations.Migration): ('pub_date', models.DateTimeField(default=datetime.datetime.now)), ('mp3', models.FileField(blank=True, null=True, upload_to=media.models.get_audio_upload_path)), ('ogg', models.FileField(blank=True, null=True, upload_to=media.models.get_audio_upload_path)), + ('point', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326)), ], options={ 'verbose_name': 'Audio', @@ -37,8 +39,9 @@ class Migration(migrations.Migration): migrations.CreateModel( name='LuxImageSize', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(blank=True, max_length=30, null=True)), + ('slug', models.SlugField(blank=True, null=True)), ('width', models.IntegerField(blank=True, null=True)), ('height', models.IntegerField(blank=True, null=True)), ('quality', models.IntegerField()), @@ -51,7 +54,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='LuxVideo', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('video_mp4', models.FileField(blank=True, null=True, upload_to=media.models.get_vid_upload_path)), ('video_webm', models.FileField(blank=True, null=True, upload_to=media.models.get_vid_upload_path)), ('video_poster', models.FileField(blank=True, null=True, upload_to=media.models.get_vid_upload_path)), @@ -70,7 +73,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='LuxImage', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('image', models.FileField(blank=True, null=True, upload_to=media.models.get_upload_path)), ('title', models.CharField(blank=True, max_length=300, null=True)), ('alt', models.CharField(blank=True, max_length=300, null=True)), @@ -78,10 +81,21 @@ class Migration(migrations.Migration): ('photo_credit_url', models.CharField(blank=True, max_length=300, null=True)), ('caption', models.TextField(blank=True, null=True)), ('pub_date', models.DateTimeField(default=datetime.datetime.now)), + ('exif_raw', models.TextField(blank=True, null=True)), + ('exif_aperture', models.CharField(blank=True, max_length=50, null=True)), + ('exif_make', models.CharField(blank=True, max_length=50, null=True)), + ('exif_model', models.CharField(blank=True, max_length=50, null=True)), + ('exif_exposure', models.CharField(blank=True, max_length=50, null=True)), + ('exif_iso', models.CharField(blank=True, max_length=50, null=True)), + ('exif_focal_length', models.CharField(blank=True, max_length=50, null=True)), + ('exif_lens', models.CharField(blank=True, max_length=50, null=True)), + ('exif_date', models.DateTimeField(blank=True, null=True)), ('height', models.CharField(blank=True, max_length=6, null=True)), ('width', models.CharField(blank=True, max_length=6, null=True)), + ('point', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326)), ('is_public', models.BooleanField(default=True)), - ('sizes', models.ManyToManyField(blank=True, to='media.LuxImageSize')), + ('sizes_cache', models.CharField(blank=True, max_length=300, null=True)), + ('sizes', models.ManyToManyField(blank=True, related_name='sizes', to='media.luximagesize')), ], options={ 'verbose_name_plural': 'Images', @@ -92,14 +106,14 @@ class Migration(migrations.Migration): migrations.CreateModel( name='LuxGallery', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(blank=True, max_length=300)), ('description', models.TextField(blank=True, null=True)), ('slug', models.CharField(blank=True, max_length=300)), ('pub_date', models.DateTimeField(null=True)), ('is_public', models.BooleanField(default=True)), ('caption_style', models.CharField(blank=True, max_length=400, null=True)), - ('images', models.ManyToManyField(to='media.LuxImage')), + ('images', models.ManyToManyField(to='media.luximage')), ('thumb', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='gallery_thumb', to='media.luximage')), ], options={ diff --git a/app/media/migrations/0002_auto_20211030_1634.py b/app/media/migrations/0002_auto_20211030_1634.py deleted file mode 100644 index abebed9..0000000 --- a/app/media/migrations/0002_auto_20211030_1634.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-30 16:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('media', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='luxaudio', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), - ), - migrations.AlterField( - model_name='luxgallery', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), - ), - migrations.AlterField( - model_name='luximage', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), - ), - migrations.AlterField( - model_name='luximagesize', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), - ), - migrations.AlterField( - model_name='luxvideo', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), - ), - ] diff --git a/app/media/models.py b/app/media/models.py index 190026f..dcf7ccc 100644 --- a/app/media/models.py +++ b/app/media/models.py @@ -1,28 +1,28 @@ import os.path import io import datetime +from pathlib import Path from PIL import Image from django.core.exceptions import ValidationError -from django.db import models +from django.contrib.gis.db import models from django.contrib.sitemaps import Sitemap -from django.utils.encoding import force_text +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.db.models.signals import m2m_changed from django.utils.functional import cached_property from django.urls import reverse from django.apps import apps from django.utils.html import format_html -from django.utils.text import slugify from django.conf import settings from django import forms -from taggit.managers import TaggableManager - from resizeimage.imageexceptions import ImageSizeError +from taggit.managers import TaggableManager + +from .readexif import readexif from .utils import resize_image -from django.db.models.signals import post_save -from django.dispatch import receiver -from django.db.models.signals import m2m_changed def get_upload_path(self, filename): @@ -39,6 +39,7 @@ def get_audio_upload_path(self, filename): class LuxImageSize(models.Model): name = models.CharField(null=True, blank=True, max_length=30) + slug = models.SlugField(null=True, blank=True) width = models.IntegerField(null=True, blank=True) height = models.IntegerField(null=True, blank=True) quality = models.IntegerField() @@ -63,10 +64,21 @@ class LuxImage(models.Model): photo_credit_url = models.CharField(null=True, blank=True, max_length=300) caption = models.TextField(blank=True, null=True) pub_date = models.DateTimeField(default=datetime.datetime.now) + exif_raw = models.TextField(blank=True, null=True) + exif_aperture = models.CharField(max_length=50, blank=True, null=True) + exif_make = models.CharField(max_length=50, blank=True, null=True) + exif_model = models.CharField(max_length=50, blank=True, null=True) + exif_exposure = models.CharField(max_length=50, blank=True, null=True) + exif_iso = models.CharField(max_length=50, blank=True, null=True) + exif_focal_length = models.CharField(max_length=50, blank=True, null=True) + exif_lens = models.CharField(max_length=50, blank=True, null=True) + exif_date = models.DateTimeField(blank=True, null=True) height = models.CharField(max_length=6, blank=True, null=True) width = models.CharField(max_length=6, blank=True, null=True) + point = models.PointField(null=True, blank=True) is_public = models.BooleanField(default=True) - sizes = models.ManyToManyField(LuxImageSize, blank=True) + sizes = models.ManyToManyField(LuxImageSize, blank=True, related_name='sizes') + sizes_cache = models.CharField(null=True, blank=True, max_length=300) class Meta: ordering = ('-pub_date', 'id') @@ -85,10 +97,7 @@ class LuxImage(models.Model): def get_admin_image(self): for size in self.sizes.all(): if size.width and size.width <= 820 or size.height and size.height <= 800: - return self.get_image_by_size(size.name) - - def get_admin_insert(self): - return "/media/images/%s/%s_tn.%s" % (self.pub_date.strftime("%Y"), self.get_image_name(), self.get_image_ext()) + return self.get_image_url_by_size(size.name) def get_largest_image(self): t = [] @@ -98,74 +107,70 @@ class LuxImage(models.Model): t.reverse() return self.get_image_path_by_size(t[0]) - def get_image_name(self): - return self.image.url.split("original/")[1][5:-4] - - def get_image_ext(self): - return self.image.url[-3:] - @cached_property - def get_featured_jrnl(self): - ''' cached version of getting the primary image for archive page''' - return "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.get_image_name(), 'featured_jrnl', self.get_image_ext()) + def image_name(self): + return os.path.basename(self.image.path)[:-4] @cached_property - def get_picwide_sm(self): - ''' cached version of getting the second image for archive page''' - return "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.get_image_name(), 'picwide-sm', self.get_image_ext()) + def image_ext(self): + return self.image.url[-3:] @cached_property + def get_image_filename(self): + return os.path.basename(self.image.path) + + @property def get_srcset(self): srcset = "" length = len(self.sizes.all()) print(length) loopnum = 1 for size in self.sizes.all(): - srcset += "%s%s/%s_%s.%s %sw" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.get_image_name(), size.name, self.get_image_ext(), size.width) + srcset += "%s%s/%s_%s.%s %sw" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.image_name, size.slug, self.image_ext, size.width) if loopnum < length: srcset += ", " loopnum = loopnum+1 return srcset - @cached_property + @property def get_src(self): src = "" if self.sizes.all().count() > 1: - src += "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.get_image_name(), 'picwide-med', self.get_image_ext()) + src = self.get_image_url_by_size('picwide-med') else: - src += "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.get_image_name(), [size.name for size in self.sizes.all()], self.get_image_ext()) + size = "".join(size.name for size in self.sizes.all()) + src = self.get_image_url_by_size(size) return src - def get_image_by_size(self, size="original"): - base = self.get_image_name() - if size == "admin_insert": - return "images/%s/%s.%s" % (self.pub_date.strftime("%Y"), base, self.get_image_ext()) + def get_image_url_by_size(self, size="original"): if size == "original": - return "%soriginal/%s/%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), base, self.get_image_ext()) + return "%soriginal/%s/%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.image_name, self.image_ext) + if size == "admin_insert": + return "images/%s/%s.%s" % (self.pub_date.strftime("%Y"), self.image_name, self.image_ext) else: - if size != 'tn': - s = LuxImageSize.objects.get(name=size) - if s not in self.sizes.all(): - print("new size is "+s.name) - self.sizes.add(s) - return "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), base, size, self.get_image_ext()) + luximagesize = LuxImageSize.objects.get(slug=size) + #if luximagesize not in self.get_sizes: + #self.sizes.add(luximagesize) + return "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.image_name, luximagesize.slug, self.image_ext) def get_image_path_by_size(self, size="original"): - base = self.get_image_name() if size == "original": - return "%s/original/%s/%s.%s" % (settings.IMAGES_ROOT, self.pub_date.strftime("%Y"), base, self.get_image_ext()) + return self.image.path else: - return "%s/%s/%s_%s.%s" % (settings.IMAGES_ROOT, self.pub_date.strftime("%Y"), base, size, self.get_image_ext()) + luximagesize = LuxImageSize.objects.get(slug=size) + return "%s/%s/%s_%s.%s" % (settings.IMAGES_ROOT, self.pub_date.strftime("%Y"), self.image_name, luximagesize.slug, self.image_ext) + @cached_property def get_thumbnail_url(self): - return self.get_image_by_size("tn") + return self.get_image_url_by_size("tn") def admin_thumbnail(self): - return format_html('<a href="%s"><img src="%s"></a>' % (self.get_image_by_size(), self.get_image_by_size("tn"))) + return format_html('<a href="%s"><img src="%s"></a>' % (self.get_image_url_by_size(), self.get_image_url_by_size("tn"))) admin_thumbnail.short_description = 'Thumbnail' + @property def get_sizes(self): - return self.sizes.all() + return self.sizes_cache.split(",") @property def get_previous_published(self): @@ -196,6 +201,9 @@ class LuxImage(models.Model): return False def save(self, *args, **kwargs): + created = self.pk is None + if not created: + self.sizes_cache = ",".join(s.slug for s in self.sizes.all()) super(LuxImage, self).save() @@ -270,6 +278,7 @@ class LuxAudio(models.Model): pub_date = models.DateTimeField(default=datetime.datetime.now) mp3 = models.FileField(blank=True, null=True, upload_to=get_audio_upload_path) ogg = models.FileField(blank=True, null=True, upload_to=get_audio_upload_path) + point = models.PointField(blank=True, null=True) class Meta: ordering = ('-pub_date',) @@ -283,6 +292,9 @@ class LuxAudio(models.Model): def get_absolute_url(self): return reverse("prompt:detail", kwargs={"slug": self.slug}) + def get_image_url_by_size(self, size="original"): + pass + @property def get_previous_published(self): return self.get_previous_by_pub_date(status__exact=1) @@ -310,51 +322,84 @@ class LuxAudio(models.Model): def save(self, *args, **kwargs): md = render_images(self.body_markdown) self.body_html = markdown_to_html(md) + if not self.point: + self.point = CheckIn.objects.latest().point super(LuxAudio, self).save(*args, **kwargs) @receiver(post_save, sender=LuxImage) def post_save_events(sender, update_fields, created, instance, **kwargs): - if instance.exif_raw == '': - filename, file_extension = os.path.splitext(instance.image.path) - if file_extension != ".mp4": - img = Image.open(instance.image.path) - instance.height = img.height - instance.width = img.width - post_save.disconnect(post_save_events, sender=LuxImage) - instance.save() - post_save.connect(post_save_events, sender=LuxImage) + if created: + if instance.exif_raw == '': + instance = readexif(instance) + instance.sizes.add(LuxImageSize.objects.get(slug="tn")) + img = Image.open(instance.image.path) + instance.height = img.height + instance.width = img.width + post_save.disconnect(post_save_events, sender=LuxImage) + instance.save() + post_save.connect(post_save_events, sender=LuxImage) @receiver(m2m_changed, sender=LuxImage.sizes.through) def update_photo_sizes(sender, instance, **kwargs): - base_path = "%s/%s/" % (settings.IMAGES_ROOT, instance.pub_date.strftime("%Y")) - filename, file_extension = os.path.splitext(instance.image.path) - if file_extension != ".mp4": - img = Image.open(instance.image.path) - resize_image(img, 160, None, 78, base_path, "%s_tn.%s" % (instance.get_image_name(), instance.get_image_ext())) - for size in instance.sizes.all(): - if size.width: - print("Image width is:"+str(img.width)) - try: - if size.width <= img.width: - resize_image(img, size.width, None, size.quality, base_path, "%s_%s.%s" % (instance.get_image_name(), slugify(size.name), instance.get_image_ext())) - else: - raise ValidationError({'items': ["Size is larger than source image"]}) - except ImageSizeError: - m2m_changed.disconnect(update_photo_sizes, sender=LuxImage.sizes.through) - instance.sizes.remove(size) - m2m_changed.connect(update_photo_sizes, sender=LuxImage.sizes.through) - if size.height: - try: - if size.height <= img.height: - resize_image(img, None, size.height, size.quality, base_path, "%s_%s.%s" % (instance.get_image_name(), slugify(size.name), instance.get_image_ext())) - - else: - pass - except ImageSizeError: - m2m_changed.disconnect(update_photo_sizes, sender=LuxImage.sizes.through) - instance.sizes.remove(size) - m2m_changed.connect(update_photo_sizes, sender=LuxImage.sizes.through) - - + # update the local cache of sizes + sizes = instance.sizes.all() + if sizes: + instance.sizes_cache = ",".join(s.slug for s in sizes) + instance.save() + for size in instance.get_sizes: + print("SIZE is: %s" % size) + # check each size and see if there's an image there already + my_file = Path(instance.get_image_path_by_size(size)) + if not my_file.is_file(): + #file doesn't exist, so create it + new_size = LuxImageSize.objects.get(slug=size) + if new_size.width: + img = Image.open(instance.image.path) + try: + if new_size.width <= img.width: + resize_image(img, new_size.width, None, new_size.quality, instance.get_image_path_by_size(size)) + else: + raise ValidationError({'items': ["Size is larger than source image"]}) + except ImageSizeError: + m2m_changed.disconnect(update_photo_sizes, sender=LuxImage.sizes.through) + instance.sizes.remove(new_size) + m2m_changed.connect(update_photo_sizes, sender=LuxImage.sizes.through) + if new_size.height: + img = Image.open(instance.image.path) + try: + if new_size.height <= img.height: + resize_image(img, None, new_size.height, new_size.quality, instance.get_image_path_by_size(size)) + else: + pass + except ImageSizeError: + m2m_changed.disconnect(update_photo_sizes, sender=LuxImage.sizes.through) + instance.sizes.remove(new_size) + m2m_changed.connect(update_photo_sizes, sender=LuxImage.sizes.through) + else: + # file exists, might add something here to force it to do the above when I want + print("file %s exists" % size) + pass + + +def generate_image(luximage, size): + new_size = LuxImageSize.objects.get(slug=size) + if new_size.width: + img = Image.open(luximage.image.path) + try: + if new_size.width <= img.width: + resize_image(img, new_size.width, None, new_size.quality, luximage.get_image_path_by_size(size)) + else: + raise ValidationError({'items': ["Size is larger than source image"]}) + except ImageSizeError: + print("error creating size") + if new_size.height: + img = Image.open(luximage.image.path) + try: + if new_size.height <= img.height: + resize_image(img, None, new_size.height, new_size.quality, luximage.get_image_path_by_size(size)) + else: + pass + except ImageSizeError: + print("error creating size") diff --git a/app/media/readexif.py b/app/media/readexif.py new file mode 100644 index 0000000..d9e5d70 --- /dev/null +++ b/app/media/readexif.py @@ -0,0 +1,76 @@ +import time +from fractions import Fraction + +from django.contrib.gis.geos import Point + +import exiftool + + +def readexif(image): + """ + takes an image and fills in all the exif data tracked in the image model + + """ + with exiftool.ExifTool() as et: + meta = et.get_metadata(image.image.path) + et.terminate() + image.exif_raw = meta + try: + image.title = meta["EXIF:ImageDescription"] + except: + try: + image.title = meta["XMP:Title"] + except: + pass + try: + image.caption = meta["EXIF:UserComment"] + except: + pass + try: + image.exif_lens = meta["MakerNotes:LensType"] + except: + try: + image.exif_lens = meta["XMP:Lens"] + except: + pass + try: + image.exif_aperture = meta["EXIF:FNumber"] + except: + pass + try: + image.exif_make = meta["EXIF:Make"] + except: + pass + try: + image.exif_model = meta["EXIF:Model"] + except: + pass + try: + image.exif_exposure = str(Fraction(float(meta["EXIF:ExposureTime"])).limit_denominator()) + except: + pass + try: + image.exif_iso = meta["EXIF:ISO"] + except: + pass + try: + image.exif_focal_length = meta["EXIF:FocalLength"] + except: + pass + try: + fmt_date = time.strptime(meta["EXIF:DateTimeOriginal"], "%Y:%m:%d %H:%M:%S") + except: + pass + try: + image.exif_date = time.strftime("%Y-%m-%d %H:%M:%S", fmt_date) + except: + pass + try: + image.height = meta["File:ImageHeight"] + except: + pass + try: + image.width = meta["File:ImageWidth"] + except: + pass + return image diff --git a/app/media/static/image-preview.js b/app/media/static/image-preview.js index b8fead5..c829084 100644 --- a/app/media/static/image-preview.js +++ b/app/media/static/image-preview.js @@ -11,7 +11,7 @@ function build_image_preview () { var img = document.createElement("img"); var request = new XMLHttpRequest(); - request.open('GET', '/photos/luximage/data/admin/preview/'+cur+'/', true); + request.open('GET', '/photos/data/admin/preview/'+cur+'/', true); request.onload = function() { if (request.status >= 200 && request.status < 400) { var data = JSON.parse(request.responseText); diff --git a/app/media/templatetags/get_image_by_size.py b/app/media/templatetags/get_image_by_size.py index c56c44e..a0a62f0 100644 --- a/app/media/templatetags/get_image_by_size.py +++ b/app/media/templatetags/get_image_by_size.py @@ -4,5 +4,5 @@ register = template.Library() @register.simple_tag def get_image_by_size(obj, *args): - method = getattr(obj, "get_image_by_size") + method = getattr(obj, "get_image_url_by_size") return method(*args) diff --git a/app/media/utils.py b/app/media/utils.py index 84e72f5..893663c 100644 --- a/app/media/utils.py +++ b/app/media/utils.py @@ -1,28 +1,26 @@ import os -import re import subprocess -from django.apps import apps -from django.conf import settings - from PIL import ImageFile -from bs4 import BeautifulSoup # pip install python-resize-image from resizeimage import resizeimage -def resize_image(img, width=None, height=None, quality=72, base_path="", filename=""): +def resize_image(img, width=None, height=None, quality=72, filepath=""): + """ + given an image object, size, and filepath + resize the image, then save it , size, and filepath + resize the image, then save it at the filepath + """ + base_path = os.path.dirname(filepath) + if not os.path.isdir(base_path): + os.makedirs(base_path) if width and height: newimg = resizeimage.resize_cover(img, [width, height]) if width and not height: newimg = resizeimage.resize_width(img, width) if height and not width: newimg = resizeimage.resize_height(img, height) - if not os.path.isdir(base_path): - os.makedirs(base_path) - path = "%s%s" % (base_path, filename) ImageFile.MAXBLOCK = img.size[0] * img.size[1] * 4 - newimg.save(path, newimg.format, quality=quality) - subprocess.call(["jpegoptim", "%s" % path]) - - + newimg.save(filepath, newimg.format, quality=quality) + subprocess.call(["jpegoptim", "%s" % filepath]) diff --git a/app/media/views.py b/app/media/views.py index 915b022..04ac11c 100644 --- a/app/media/views.py +++ b/app/media/views.py @@ -1,10 +1,10 @@ import json -from django.shortcuts import render_to_response, render +from django.shortcuts import render from django.template import RequestContext from django.http import Http404, HttpResponse from django.core import serializers -from .models import Photo, PhotoGallery, LuxGallery, LuxImage +from .models import LuxGallery, LuxImage from locations.models import Country, Region from utils.views import PaginatedListView @@ -52,10 +52,10 @@ class GalleryList(PaginatedListView): class OldGalleryList(PaginatedListView): template_name = 'archives/gallery_list.html' - model = PhotoGallery + model = LuxGallery def get_queryset(self): - return PhotoGallery.objects.filter(is_public=True) + return LuxGallery.objects.filter(is_public=True) def get_context_data(self, **kwargs): # Call the base implementation first to get a context @@ -74,7 +74,7 @@ def gallery_list(request, page): request.page_url = '/photos/%d/' request.page = int(page) context = { - 'object_list': PhotoGallery.objects.all(), + 'object_list': LuxGallery.objects.all(), 'page': page, } return render(request, "archives/photos.html", context) @@ -82,13 +82,13 @@ def gallery_list(request, page): def gallery(request, slug): context = { - 'object': PhotoGallery.objects.get(set_slug=slug) + 'object': LuxGallery.objects.get(set_slug=slug) } - return render_to_response('details/photo_galleries.html', context, context_instance=RequestContext(request)) + return render(request, 'details/photo_galleries.html', context) def photo_json(request, slug): - p = PhotoGallery.objects.filter(set_slug=slug) + p = LuxGallery.objects.filter(set_slug=slug) return HttpResponse(serializers.serialize('json', p), mimetype='application/json') @@ -103,7 +103,7 @@ def photo_preview_json(request, pk): def thumb_preview_json(request, pk): p = LuxImage.objects.get(pk=pk) data = {} - data['url'] = p.get_admin_insert() + data['url'] = p.get_image_url_by_size('tn') data = json.dumps(data) return HttpResponse(data) @@ -114,10 +114,10 @@ def gallery_list_by_area(request, slug, page): request.page = int(page) try: region = Region.objects.get(slug__exact=slug) - qs = PhotoGallery.objects.filter(region=region).order_by('-id') + qs = LuxGallery.objects.filter(region=region).order_by('-id') except: region = Country.objects.get(slug__exact=slug) - qs = PhotoGallery.objects.filter(location__state__country=region).order_by('-id') + qs = LuxGallery.objects.filter(location__state__country=region).order_by('-id') if not region: raise Http404 context = { @@ -127,4 +127,4 @@ def gallery_list_by_area(request, slug, page): 'region': region, 'page': page } - return render_to_response("archives/photos.html", context, context_instance=RequestContext(request)) + return render(request, "archives/photos.html", context) diff --git a/app/normalize/migrations/0001_initial.py b/app/normalize/migrations/0001_initial.py index 358677a..08c4f5b 100644 --- a/app/normalize/migrations/0001_initial.py +++ b/app/normalize/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.3 on 2020-11-30 22:45 +# Generated by Django 4.1.3 on 2022-12-02 20:09 from django.db import migrations, models import django.db.models.deletion @@ -16,7 +16,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='RelatedPost', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('entry_id', models.IntegerField()), ('title', models.CharField(max_length=200)), ('slug', models.CharField(max_length=50)), diff --git a/app/pages/migrations/0001_initial.py b/app/pages/migrations/0001_initial.py index 61213e1..6227712 100644 --- a/app/pages/migrations/0001_initial.py +++ b/app/pages/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.3 on 2020-11-30 22:44 +# Generated by Django 4.1.3 on 2022-12-02 20:08 from django.db import migrations, models import django.db.models.deletion @@ -18,7 +18,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Page', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=200)), ('sub_title', models.CharField(blank=True, max_length=300)), ('slug', models.SlugField()), @@ -37,13 +37,11 @@ class Migration(migrations.Migration): migrations.CreateModel( name='HomePage', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('image_offset_vertical', models.CharField(help_text='add negative top margin to shift image (include css unit)', max_length=20)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('tag_line', models.CharField(blank=True, max_length=200, null=True)), ('template_name', models.CharField(blank=True, help_text='full path', max_length=200, null=True)), - ('featured', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='banner', to='posts.post')), - ('featured_image', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='media.luximage')), - ('popular', models.ManyToManyField(related_name='popular', to='posts.Post')), + ('featured', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='banner', to='posts.post')), + ('featured_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luximage')), ], ), ] diff --git a/app/pages/migrations/0002_auto_20211030_1634.py b/app/pages/migrations/0002_auto_20211030_1634.py deleted file mode 100644 index 7a8649b..0000000 --- a/app/pages/migrations/0002_auto_20211030_1634.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-30 16:34 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('posts', '0002_alter_post_id'), - ('media', '0002_auto_20211030_1634'), - ('pages', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='homepage', - name='image_offset_vertical', - ), - migrations.RemoveField( - model_name='homepage', - name='popular', - ), - migrations.AlterField( - model_name='homepage', - name='featured', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='banner', to='posts.post'), - ), - migrations.AlterField( - model_name='homepage', - name='featured_image', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luximage'), - ), - migrations.AlterField( - model_name='homepage', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), - ), - migrations.AlterField( - model_name='page', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), - ), - ] diff --git a/app/pages/templates/pages/luxagraf/homepage.html b/app/pages/templates/pages/luxagraf/homepage.html index c8075ea..bde11b9 100644 --- a/app/pages/templates/pages/luxagraf/homepage.html +++ b/app/pages/templates/pages/luxagraf/homepage.html @@ -2,7 +2,7 @@ {% load typogrify_tags %} {% block sitename %} <head itemscope itemtype="http://schema.org/WebSite"> - <title itemprop='name'>Luxagraf: thoughts on ecology, culture, travel, photography, walking and other ephemera</title> + <title itemprop='name'>Libregraf Publishing. Fine books for fine people</title> <link rel="canonical" href="https://luxagraf.net/">{%endblock%} {%block extrahead%} diff --git a/app/podcasts/admin.py b/app/podcasts/admin.py new file mode 100644 index 0000000..b6adfc6 --- /dev/null +++ b/app/podcasts/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin +from .models import Podcast + +@admin.register(Podcast) +class PodcastAdmin(admin.ModelAdmin): + list_display = ('title',) + search_fields = ['title', 'body_markdown'] + + class Media: + js = ('next-prev-links.js',) diff --git a/app/podcasts/feeds.py b/app/podcasts/feeds.py new file mode 100644 index 0000000..24d47bd --- /dev/null +++ b/app/podcasts/feeds.py @@ -0,0 +1,229 @@ +import datetime +from podcasts.models import Episode, Podcast +from django.contrib.syndication.views import Feed +from django.contrib.sites.shortcuts import get_current_site +from django.views.generic.base import RedirectView +from django.utils.feedgenerator import rfc2822_date, Rss201rev2Feed, Atom1Feed +from django.shortcuts import get_object_or_404 + +from .models import MIME_CHOICES + +class ITunesElements(object): + + def add_root_elements(self, handler): + """ Add additional elements to the podcast object""" + super(ITunesElements, self).add_root_elements(handler) + + podcast = self.feed["podcast"] + + if podcast.featured_image: + # grab thumbs here + #itunes_sm_url = thumbnailer.get_thumbnail(aliases["itunes_sm"]).url + #itunes_lg_url = thumbnailer.get_thumbnail(aliases["itunes_lg"]).url + if itunes_sm_url and itunes_lg_url: + handler.addQuickElement("itunes:image", attrs={"href": itunes_lg_url}) + handler.startElement("image", {}) + handler.addQuickElement("url", itunes_sm_url) + handler.addQuickElement("title", self.feed["title"]) + handler.addQuickElement("link", self.feed["link"]) + handler.endElement("image") + + handler.addQuickElement("guid", str(podcast.uuid), attrs={"isPermaLink": "false"}) + handler.addQuickElement("itunes:subtitle", self.feed["subtitle"]) + handler.addQuickElement("itunes:author", podcast.publisher) + handler.startElement("itunes:owner", {}) + handler.addQuickElement("itunes:name", podcast.publisher) + handler.addQuickElement("itunes:email", podcast.publisher_email) + handler.endElement("itunes:owner") + handler.addQuickElement("itunes:category", attrs={"text": self.feed["categories"][0]}) + handler.addQuickElement("itunes:summary", podcast.description) + handler.addQuickElement("itunes:explicit", "no") + handler.addQuickElement("keywords", podcast.keywords) + try: + handler.addQuickElement("lastBuildDate", + rfc2822_date(podcast.episode_set.filter(status=1)[1].pub_date)) + except IndexError: + pass + handler.addQuickElement("generator", "Luxagraf's Django Web Framework") + handler.addQuickElement("docs", "http://blogs.law.harvard.edu/tech/rss") + + def add_item_elements(self, handler, item): + """ Add additional elements to the episode object""" + super(ITunesElements, self).add_item_elements(handler, item) + + podcast = item["podcast"] + episode = item["episode"] + if episode.featured_image: + #grab episode thumbs + #itunes_sm_url = None + #itunes_lg_url = None + if itunes_sm_url and itunes_lg_url: + handler.addQuickElement("itunes:image", attrs={"href": itunes_lg_url}) + handler.startElement("image", {}) + handler.addQuickElement("url", itunes_sm_url) + handler.addQuickElement("title", episode.title) + handler.addQuickElement("link", episode.get_absolute_url()) + handler.endElement("image") + + handler.addQuickElement("guid", str(episode.uuid), attrs={"isPermaLink": "false"}) + handler.addQuickElement("copyright", "{0} {1}".format(podcast.license, + datetime.date.today().year)) + handler.addQuickElement("itunes:author", episode.podcast.publisher) + handler.addQuickElement("itunes:subtitle", episode.subtitle) + handler.addQuickElement("itunes:summary", episode.description) + handler.addQuickElement("itunes:duration", "%02d:%02d:%02d" % (episode.hours, + episode.minutes, + episode.seconds)) + handler.addQuickElement("itunes:keywords", episode.keywords) + handler.addQuickElement("itunes:explicit", "no") + if episode.block: + handler.addQuickElement("itunes:block", "yes") + + def namespace_attributes(self): + return {"xmlns:itunes": "http://www.itunes.com/dtds/podcast-1.0.dtd"} + + +class AtomITunesFeedGenerator(ITunesElements, Atom1Feed): + def root_attributes(self): + atom_attrs = super(AtomITunesFeedGenerator, self).root_attributes() + atom_attrs.update(self.namespace_attributes()) + return atom_attrs + + +class RssITunesFeedGenerator(ITunesElements, Rss201rev2Feed): + def rss_attributes(self): + rss_attrs = super(RssITunesFeedGenerator, self).rss_attributes() + rss_attrs.update(self.namespace_attributes()) + return rss_attrs + + +class ShowFeed(Feed): + """ + A feed of podcasts for iTunes and other compatible podcatchers. + """ + def title(self, podcast): + return podcast.title + + def link(self, podcast): + return podcast.get_absolute_url() + + def categories(self, podcast): + return ("Music",) + + def feed_copyright(self, podcast): + return "{0} {1}".format(podcast.license, datetime.date.today().year) + + def ttl(self, podcast): + return podcast.ttl + + def items(self, podcast): + return podcast.episode_set.filter(status=1)[:300] + + def get_object(self, request, *args, **kwargs): + self.mime = [mc[0] for mc in MIME_CHOICES if mc[0] == kwargs["mime_type"]][0] + site = get_current_site(request) + self.podcast = get_object_or_404(Podcast, slug=kwargs["show_slug"]) + return self.podcast + + def item_title(self, episode): + return episode.title + + def item_description(self, episode): + "renders summary for atom" + return episode.description + + def item_link(self, episode): + return reverse("podcasting_episode_detail", + kwargs={"podcast_slug": self.podcast.slug, "slug": episode.slug}) + + # def item_author_link(self, episode): + # return "todo" #this one doesn't add anything in atom or rss + # + # def item_author_email(self, episode): + # return "todo" #this one doesn't add anything in atom or rss + + def item_pubdate(self, episode): + return episode.pub_date + + def item_categories(self, episode): + return self.categories(self.podcast) + + def item_enclosure_url(self, episode): + try: + e = episode.enclosure_set.get(mime=self.mime) + return e.url + except Enclosure.DoesNotExist: + pass + + def item_enclosure_length(self, episode): + try: + e = episode.enclosure_set.get(mime=self.mime) + return e.size + except Enclosure.DoesNotExist: + pass + + def item_enclosure_mime_type(self, episode): + try: + e = episode.enclosure_set.get(mime=self.mime) + return e.get_mime_display() + except Enclosure.DoesNotExist: + pass + + def item_keywords(self, episode): + return episode.keywords + + def feed_extra_kwargs(self, obj): + extra = {} + extra["podcast"] = self.podcast + return extra + + def item_extra_kwargs(self, item): + extra = {} + extra["podcast"] = self.podcast + extra["episode"] = item + return extra + + +class AtomShowFeed(ShowFeed): + feed_type = AtomITunesFeedGenerator + + def subtitle(self, show): + return show.subtitle + + def author_name(self, show): + return show.publisher + + def author_email(self, show): + return show.publisher_email + + def author_link(self, show): + return show.get_absolute_url() + + +class RssShowFeed(ShowFeed): + feed_type = RssITunesFeedGenerator + + def item_guid(self, episode): + "ITunesElements can't add isPermaLink attr unless None is returned here." + return None + + def description(self, show): + return show.description + + +class AtomRedirectView(RedirectView): + permanent = False + + def get_redirect_url(self, show_slug, mime_type): + return reverse( + "podcasts_show_feed_atom", + kwargs={"show_slug": show_slug, "mime_type": mime_type}) + + +class RssRedirectView(RedirectView): + permanent = False + + def get_redirect_url(self, show_slug, mime_type): + return reverse( + "podcasts_show_feed_rss", + kwargs={"show_slug": show_slug, "mime_type": mime_type}) diff --git a/app/podcasts/migrations/0001_initial.py b/app/podcasts/migrations/0001_initial.py new file mode 100644 index 0000000..644e7dc --- /dev/null +++ b/app/podcasts/migrations/0001_initial.py @@ -0,0 +1,60 @@ +# Generated by Django 4.1.3 on 2022-12-02 20:09 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('media', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Podcast', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('title', models.CharField(max_length=255)), + ('subtitle', models.CharField(blank=True, max_length=255, null=True)), + ('slug', models.SlugField()), + ('publisher', models.CharField(max_length=255)), + ('publisher_email', models.CharField(max_length=255)), + ('description', models.TextField()), + ('keywords', models.CharField(blank=True, help_text='A comma-delimited list of words for searches, up to 12;', max_length=255)), + ('license', models.TextField(blank=True, null=True)), + ('featured_image', models.ForeignKey(blank=True, help_text='square JPEG (.jpg) or PNG (.png) image at a size of 1400x1400 pixels.', null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luximage')), + ], + options={ + 'verbose_name': 'Podcast', + 'verbose_name_plural': 'Podcasts', + 'ordering': ('title', 'slug'), + }, + ), + migrations.CreateModel( + name='Episode', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('title', models.CharField(max_length=255)), + ('subtitle', models.CharField(blank=True, max_length=255, null=True)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ('pub_date', models.DateTimeField(blank=True, null=True)), + ('enable_comments', models.BooleanField(default=True)), + ('slug', models.SlugField()), + ('status', models.IntegerField(choices=[(0, 'Draft'), (1, 'Published')], default=0)), + ('description', models.TextField()), + ('keywords', models.CharField(blank=True, help_text='A comma-delimited list of words for searches, up to 12;', max_length=255)), + ('featured_image', models.ForeignKey(blank=True, help_text='square JPEG (.jpg) or PNG (.png) image at a size of 1400x1400 pixels.', null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luximage')), + ('podcast', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='podcasts.podcast')), + ], + options={ + 'verbose_name': 'Episode', + 'verbose_name_plural': 'Episodes', + 'ordering': ('-pub_date', 'slug'), + }, + ), + ] diff --git a/app/accounts/migrations/__init__.py b/app/podcasts/migrations/__init__.py index e69de29..e69de29 100644 --- a/app/accounts/migrations/__init__.py +++ b/app/podcasts/migrations/__init__.py diff --git a/app/podcasts/models.py b/app/podcasts/models.py new file mode 100644 index 0000000..b574986 --- /dev/null +++ b/app/podcasts/models.py @@ -0,0 +1,132 @@ +import os +import uuid +from django.db import models +from django.urls import reverse +from django.template.defaultfilters import slugify +from django.conf import settings + +# optional external dependencies +try: + from licenses.models import License +except: + License = None + +from taggit.managers import TaggableManager +from mutagen.mp3 import MP3 + +from media.models import LuxAudio, LuxImage + +def get_show_upload_folder(instance, pathname): + "A standardized pathname for uploaded files and images." + root, ext = os.path.splitext(pathname) + return "{0}/podcasts/{1}/{2}{3}".format( + settings.PODCASTING_IMG_PATH, instance.slug, slugify(root), ext + ) + + +def get_episode_upload_folder(instance, pathname): + "A standardized pathname for uploaded files and images." + root, ext = os.path.splitext(pathname) + if instance.shows.count() == 1: + return "{0}/podcasts/{1}/episodes/{2}{3}".format( + settings.PODCASTING_IMG_PATH, instance.shows.all()[0].slug, slugify(root), ext + ) + else: + return "{0}/podcasts/episodes/{1}/{2}{3}".format( + settings.PODCASTING_IMG_PATH, instance.slug, slugify(root), ext + ) + +MIME_CHOICES = ( + ("mp3", "audio/mpeg"), + ("mp4", "audio/mp4"), + ("ogg", "audio/ogg"), +) + + +#audio = MP3("example.mp3") +#print(audio.info.length) + +class Podcast(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + title = models.CharField(max_length=255) + subtitle = models.CharField(max_length=255, blank=True, null=True) + slug = models.SlugField() + publisher = models.CharField(max_length=255) + publisher_email = models.CharField(max_length=255) + description = models.TextField() + keywords = models.CharField(max_length=255, blank=True, help_text="A comma-delimited list of words for searches, up to 12;") + license = models.TextField(blank=True, null=True) + featured_image = models.ForeignKey(LuxImage, on_delete=models.CASCADE, null=True, blank=True, help_text=("square JPEG (.jpg) or PNG (.png) image at a size of 1400x1400 pixels.")) + + + class Meta: + verbose_name = "Podcast" + verbose_name_plural = "Podcasts" + ordering = ("title", "slug") + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse("podcasts:list", kwargs={"slug": self.slug}) + + def ttl(self): + return "1440" + + +class Episode(models.Model): + """ + An individual podcast episode and it's unique attributes. + """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + title = models.CharField(max_length=255) + subtitle = models.CharField(max_length=255, blank=True, null=True) + date_created = models.DateTimeField(auto_now_add=True, editable=False) + date_updated = models.DateTimeField(auto_now=True, editable=False) + pub_date = models.DateTimeField(null=True, blank=True) + podcast = models.ForeignKey(Podcast, on_delete=models.PROTECT) + enable_comments = models.BooleanField(default=True) + slug = models.SlugField() + PUB_STATUS = ( + (0, 'Draft'), + (1, 'Published'), + ) + status = models.IntegerField(choices=PUB_STATUS, default=0) + description = models.TextField() + featured_image = models.ForeignKey(LuxImage, on_delete=models.CASCADE, null=True, blank=True, help_text=("square JPEG (.jpg) or PNG (.png) image at a size of 1400x1400 pixels.")) + # iTunes specific fields + keywords = models.CharField(max_length=255, blank=True, help_text="A comma-delimited list of words for searches, up to 12;") + + class Meta: + verbose_name = "Episode" + verbose_name_plural = "Episodes" + ordering = ("-pub_date", "slug") + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse("podcasting_episode_detail", kwargs={"show_slug": self.shows.all()[0].slug, "slug": self.slug}) + + def get_next(self): + next = self.__class__.objects.filter(published__gt=self.published) + try: + return next[0] + except IndexError: + return False + + def get_prev(self): + prev = self.__class__.objects.filter(published__lt=self.published).order_by("-published") + try: + return prev[0] + except IndexError: + return False + + def get_explicit_display(self): + return "no" + + def seconds_total(self): + try: + return self.minutes * 60 + self.seconds + except: + return 0 diff --git a/app/posts/templates/posts/podcast_detail.html b/app/podcasts/templates/podcasts/detail.html index 4fec3b1..361d822 100644 --- a/app/posts/templates/posts/podcast_detail.html +++ b/app/podcasts/templates/podcasts/detail.html @@ -29,7 +29,7 @@ <div class="entry-footer"> <aside class="narrow donate"> <h3>Support</h3> - <p>Want to help support Lulu and Birdie? You can buy the book, or you can donate a few dollars.</p> + <p>Want to help support Lulu and Birdie? You can <a href="/bookshop/">buy the book</a>, or you can donate a few dollars.</p> <div class="donate-btn"> <form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top"> <input type="hidden" name="cmd" value="_s-xclick"> diff --git a/app/posts/templates/posts/podcast_list.html b/app/podcasts/templates/podcasts/list.html index 8c73db4..89b7ea8 100644 --- a/app/posts/templates/posts/podcast_list.html +++ b/app/podcasts/templates/podcasts/list.html @@ -8,8 +8,8 @@ {% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %} {% block primary %}<main role="main" class="archive-wrapper"> <div class="archive-intro"> - <h1 class="archive-hed">The Adventures of Lulu, Birdie, and Henry: The Podcast.</h1> - <h2 class="list-subhed">Let's see what happens.</h2> + <h1 class="archive-hed">{{podcast.title}}</h1> + {% if object.subtitle %}<h2 class="list-subhed">{{podcast.subtitle}}</h2>{% endif %} </div> <h1 class="archive-sans">Episodes</h1>{% autopaginate object_list 24 %} @@ -22,7 +22,9 @@ </a> </li> {%endfor%}</ul> - + <a href="{% url 'podcasts_show_feed_atom' podcast.slug 'mp3' %}" >MP3</a> + <a href="{% url 'podcasts_show_feed_atom' podcast.slug 'mp4' %}" >MP4</a> + <a href="{% url 'podcasts_show_feed_rss' podcast.slug 'mp3' %}" >OGG</a> </main> diff --git a/app/podcasts/urls.py b/app/podcasts/urls.py new file mode 100644 index 0000000..622b203 --- /dev/null +++ b/app/podcasts/urls.py @@ -0,0 +1,19 @@ +from django.urls import path, re_path + +from . import views + +app_name = "podcasts" + +urlpatterns = [ + re_path( + r'<str:slug>/<int:page>', + views.PodcastListView.as_view(), + name="list" + ), + path( + r'<str:slug>/', + views.PodcastListView.as_view(), + {'page':1}, + name="list" + ), +] diff --git a/app/podcasts/urls_feeds.py b/app/podcasts/urls_feeds.py new file mode 100644 index 0000000..07e02ae --- /dev/null +++ b/app/podcasts/urls_feeds.py @@ -0,0 +1,17 @@ +from django.urls import include, re_path + +from .feeds import RssShowFeed, AtomShowFeed, AtomRedirectView, RssRedirectView +from .models import MIME_CHOICES + + +MIMES = "|".join([enclosure[0] for enclosure in MIME_CHOICES]) + + +urlpatterns = [ + # Episode list feed by podcast (RSS 2.0 and iTunes) + re_path(r"^(?P<show_slug>[-\w]+)/(?P<mime_type>{mimes})/rss/$".format(mimes=MIMES), + RssShowFeed(), name="podcasts_show_feed_rss"), + # Episode list feed by show (Atom) + re_path(r"^(?P<show_slug>[-\w]+)/(?P<mime_type>{mimes})/atom/$".format(mimes=MIMES), + AtomShowFeed(), name="podcasts_show_feed_atom"), +] diff --git a/app/podcasts/views.py b/app/podcasts/views.py new file mode 100644 index 0000000..a8edbfa --- /dev/null +++ b/app/podcasts/views.py @@ -0,0 +1,29 @@ +from django.views.generic import ListView +from django.views.generic.detail import DetailView +from django.views.generic.dates import DateDetailView +from django.urls import reverse +from django.contrib.syndication.views import Feed +from django.apps import apps +from django.shortcuts import get_object_or_404 +from django.conf import settings +from django.db.models import Q + +from utils.views import PaginatedListView + +from .models import Episode, Podcast + + +class PodcastListView(PaginatedListView): + """ + Return a list of Episodes in reverse chronological order + """ + model = Podcast + template_name = "podcasts/list.html" + queryset = Episode.objects.filter(podcast=1).filter(status__exact=1).order_by('-pub_date') + + def get_context_data(self, **kwargs): + context = super(PodcastListView, self).get_context_data(**kwargs) + context['breadcrumbs'] = ['podcast',] + context['podcast'] = Podcast.objects.get(title="The Lulu & Birdie Podcast") + return context + diff --git a/app/posts/migrations/0001_initial.py b/app/posts/migrations/0001_initial.py index af70a9f..218eed5 100644 --- a/app/posts/migrations/0001_initial.py +++ b/app/posts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.3 on 2020-11-30 22:44 +# Generated by Django 4.1.3 on 2022-12-02 20:08 from django.db import migrations, models import django.db.models.deletion @@ -10,17 +10,16 @@ class Migration(migrations.Migration): dependencies = [ ('books', '__first__'), - ('normalize', '__first__'), - ('taxonomy', '__first__'), - ('sites', '0002_alter_domain_unique'), ('media', '__first__'), + ('taxonomy', '__first__'), + ('normalize', '__first__'), ] operations = [ migrations.CreateModel( name='Post', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=200)), ('short_title', models.CharField(blank=True, max_length=200, null=True)), ('subtitle', models.CharField(blank=True, max_length=200)), @@ -37,18 +36,18 @@ class Migration(migrations.Migration): ('last_updated', models.DateTimeField(auto_now=True)), ('enable_comments', models.BooleanField(default=False)), ('status', models.IntegerField(choices=[(0, 'Draft'), (1, 'Published')], default=0)), - ('post_type', models.IntegerField(choices=[(0, 'field test'), (1, 'review'), (2, 'essay'), (3, 'src'), (4, 'jrnl'), (5, 'field note')], default=4)), - ('template_name', models.IntegerField(choices=[(0, 'single'), (1, 'double'), (2, 'single-dark'), (3, 'double-dark'), (4, 'single-black'), (5, 'double-black')], default=0)), + ('post_type', models.IntegerField(choices=[(0, 'Podcast'), (1, 'jrnl'), (2, 'field note')], default=1)), ('has_video', models.BooleanField(blank=True, default=False)), ('has_code', models.BooleanField(blank=True, default=False)), ('disclaimer', models.BooleanField(blank=True, default=False)), ('originally_published_by', models.CharField(blank=True, max_length=400, null=True)), ('originally_published_by_url', models.CharField(blank=True, max_length=400, null=True)), - ('books', models.ManyToManyField(blank=True, to='books.Book')), + ('issue', models.PositiveIntegerField(null=True)), + ('books', models.ManyToManyField(blank=True, to='books.book')), + ('featured_audio', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luxaudio')), ('featured_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luximage')), - ('related', models.ManyToManyField(blank=True, to='normalize.RelatedPost')), - ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.site')), - ('topics', models.ManyToManyField(blank=True, to='taxonomy.Category')), + ('related', models.ManyToManyField(blank=True, to='normalize.relatedpost')), + ('topics', models.ManyToManyField(blank=True, to='taxonomy.category')), ], options={ 'ordering': ('-pub_date',), diff --git a/app/posts/migrations/0002_alter_post_id.py b/app/posts/migrations/0002_alter_post_id.py deleted file mode 100644 index a41f999..0000000 --- a/app/posts/migrations/0002_alter_post_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-30 16:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('posts', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='post', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), - ), - ] diff --git a/app/posts/migrations/0003_auto_20211030_1955.py b/app/posts/migrations/0003_auto_20211030_1955.py deleted file mode 100644 index b54cdd8..0000000 --- a/app/posts/migrations/0003_auto_20211030_1955.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-30 19:55 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('media', '0002_auto_20211030_1634'), - ('posts', '0002_alter_post_id'), - ] - - operations = [ - migrations.RemoveField( - model_name='post', - name='site', - ), - migrations.RemoveField( - model_name='post', - name='template_name', - ), - migrations.AddField( - model_name='post', - name='featured_audio', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luxaudio'), - ), - migrations.AddField( - model_name='post', - name='issue', - field=models.PositiveIntegerField(null=True), - ), - migrations.AlterField( - model_name='post', - name='post_type', - field=models.IntegerField(choices=[(0, 'Podcast'), (1, 'jrnl'), (2, 'field note')], default=1), - ), - ] diff --git a/app/taxonomy/admin.py b/app/taxonomy/admin.py index 783584e..45e4e26 100644 --- a/app/taxonomy/admin.py +++ b/app/taxonomy/admin.py @@ -14,6 +14,9 @@ class CategoryAdmin(admin.ModelAdmin): 'name', 'color_rgb', 'slug', + "pluralized_name", + "description", + "intro_markdown", ), 'classes': ( 'show', diff --git a/app/taxonomy/migrations/0001_initial.py b/app/taxonomy/migrations/0001_initial.py index bde5e44..58e7cdb 100644 --- a/app/taxonomy/migrations/0001_initial.py +++ b/app/taxonomy/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.3 on 2020-11-30 22:45 +# Generated by Django 4.1.3 on 2022-12-02 20:08 from django.db import migrations, models import django.db.models.deletion @@ -16,9 +16,12 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Category', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=250)), - ('pluralized_name', models.CharField(max_length=60, null=True)), + ('pluralized_name', models.CharField(blank=True, max_length=60, null=True)), + ('description', models.CharField(blank=True, max_length=300, null=True)), + ('intro_markdown', models.TextField(blank=True, null=True)), + ('intro_html', models.TextField(blank=True, null=True)), ('slug', models.SlugField(blank=True)), ('color_rgb', models.CharField(blank=True, max_length=20)), ('date_created', models.DateTimeField(auto_now_add=True)), @@ -32,9 +35,9 @@ class Migration(migrations.Migration): migrations.CreateModel( name='LuxTag', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100, unique=True, verbose_name='name')), - ('slug', models.SlugField(max_length=100, unique=True, verbose_name='slug')), + ('slug', models.SlugField(allow_unicode=True, max_length=100, unique=True, verbose_name='slug')), ('color_rgb', models.CharField(blank=True, max_length=20)), ], options={ @@ -45,10 +48,10 @@ class Migration(migrations.Migration): migrations.CreateModel( name='TaggedItems', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('object_id', models.IntegerField(db_index=True, verbose_name='object ID')), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='taxonomy_taggeditems_tagged_items', to='contenttypes.contenttype', verbose_name='content type')), - ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='taxonomy_taggeditems_items', to='taxonomy.luxtag')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_tagged_items', to='contenttypes.contenttype', verbose_name='content type')), + ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_items', to='taxonomy.luxtag')), ], options={ 'abstract': False, diff --git a/app/taxonomy/models.py b/app/taxonomy/models.py index 4db3294..736fe15 100644 --- a/app/taxonomy/models.py +++ b/app/taxonomy/models.py @@ -1,8 +1,10 @@ from django.contrib.gis.db import models from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.utils.functional import cached_property +from utils.util import markdown_to_html + from taggit.models import TagBase, GenericTaggedItemBase @@ -27,7 +29,10 @@ class TaggedItems(GenericTaggedItemBase): class Category(models.Model): """ Generic model for Categories """ name = models.CharField(max_length=250) - pluralized_name = models.CharField(max_length=60, null=True) + pluralized_name = models.CharField(max_length=60, null=True, blank=True) + description = models.CharField(max_length=300, null=True, blank=True) + intro_markdown = models.TextField(null=True, blank=True) + intro_html = models.TextField(null=True, blank=True) slug = models.SlugField(blank=True) color_rgb = models.CharField(max_length=20, blank=True) date_created = models.DateTimeField(blank=True, auto_now_add=True, editable=False) @@ -42,3 +47,9 @@ class Category(models.Model): def get_absolute_url(self): return reverse("taxonomy:cat-detail", kwargs={"slug": self.slug}) + + + def save(self, *args, **kwargs): + if self.intro_markdown: + self.intro_html = markdown_to_html(self.intro_markdown) + super(Category, self).save(*args, **kwargs) diff --git a/app/taxonomy/views.py b/app/taxonomy/views.py index 2d749ab..354234c 100644 --- a/app/taxonomy/views.py +++ b/app/taxonomy/views.py @@ -1,14 +1,12 @@ from django.views.generic import ListView -from django.views.generic.detail import DetailView from django.contrib.syndication.views import Feed from django.urls import reverse from django.conf import settings -#from paypal.standard.forms import PayPalPaymentsForm - from .models import Category +from utils.views import LuxDetailView -class CategoryDetailView(DetailView): +class CategoryDetailView(LuxDetailView): model = Category slug_field = "slug" diff --git a/app/utils/static/image-loader.js b/app/utils/static/image-loader.js index 2744251..ca96565 100644 --- a/app/utils/static/image-loader.js +++ b/app/utils/static/image-loader.js @@ -13,9 +13,9 @@ function add_images(){ var loop = Number(element.dataset.loopcounter); if (cur != "") { if (loop <= 100) { - console.log(loop); + console.log('/photos/data/admin/tn/'+cur+'/'); var request = new XMLHttpRequest(); - request.open('GET', '/photos/luximage/data/admin/tn/'+cur+'/', true); + request.open('GET', '/photos/data/admin/tn/'+cur+'/', true); request.onload = function() { if (request.status >= 200 && request.status < 400) { var data = JSON.parse(request.responseText); @@ -41,7 +41,11 @@ function add_images(){ } document.addEventListener("DOMContentLoaded", function(event) { add_images(); - md = document.forms["entry_form"].elements["body_markdown"]; + if (document.forms["post_form"]) { + md = document.forms["post_form"].elements["body_markdown"]; + } else { + md = document.forms["track_form"].elements["body_markdown"]; + } md.style.maxHeight = "300rem"; md.style.maxWidth = "300rem"; }); diff --git a/app/utils/util.py b/app/utils/util.py index dc04b0b..d9b2318 100644 --- a/app/utils/util.py +++ b/app/utils/util.py @@ -45,7 +45,7 @@ def extract_main_image(markdown): try: image = soup.find_all('img')[0]['id'] img_pk = image.split('image-')[1] - return apps.get_model('photos', 'LuxImage').objects.get(pk=img_pk) + return apps.get_model('media', 'LuxImage').objects.get(pk=img_pk) except IndexError: return None @@ -74,7 +74,7 @@ def parse_image(s): else: try: image_id = img['id'].split("image-")[1] - i = apps.get_model('photos', 'LuxImage').objects.get(pk=image_id) + i = apps.get_model('media', 'LuxImage').objects.get(pk=image_id) caption = False exif = False cluster_class = None @@ -141,7 +141,7 @@ def parse_reg_bio_page(): try: image = soup.find_all('img')[0]['id'] img_pk = image.split('image-')[1] - return apps.get_model('photos', 'LuxImage').objects.get(pk=img_pk) + return apps.get_model('media', 'LuxImage').objects.get(pk=img_pk) except IndexError: return None diff --git a/app/utils/views.py b/app/utils/views.py index 6b69b25..b678a33 100644 --- a/app/utils/views.py +++ b/app/utils/views.py @@ -6,16 +6,17 @@ from django.views.generic import ListView, DetailView from django.apps import apps from django.shortcuts import render from django.template import RequestContext +from django.template.defaultfilters import slugify from media.models import LuxImage, LuxVideo, LuxAudio BREADCRUMBS = { - 'SrcPost':'SRC', + 'AP':'dialogue', 'Book':'Book Notes', 'Entry':'Jrnl', 'NewsletterMailing':'lttr', - 'LuxImage':'lttr' + 'LuxImage':'lttr', } class PaginatedListView(ListView): @@ -35,13 +36,6 @@ class PaginatedListView(ListView): request.base_path = path return super(PaginatedListView, self).dispatch(request, *args, **kwargs) - def get_context_data(self, **kwargs): - ''' - Adds breadcrumb path to every view - ''' - # Call the base implementation first to get a context - context = super(PaginatedListView, self).get_context_data(**kwargs) - print('model=', self.model) try: context['breadcrumbs'] = (BREADCRUMBS[self.model.__name__],) except KeyError: @@ -61,22 +55,31 @@ class LuxDetailView(DetailView): context = super(LuxDetailView, self).get_context_data(**kwargs) print(self.object._meta.verbose_name_plural) try: - context['breadcrumbs'] = (BREADCRUMBS[self.object._meta.model],) + context['breadcrumbs'] = (BREADCRUMBS[self.object._meta.label.split(".")[1]],) except KeyError: if self.object._meta.verbose_name_plural == 'posts': - context['breadcrumbs'] = (self.object.get_post_type_display()+"s",) - context['crumb_url'] = "/%ss/" % self.object.get_post_type_display() + if self.object.get_post_type_display() != 'src': + context['breadcrumbs'] = (self.object.get_post_type_display()+"s",) + else: + context['breadcrumbs'] = (self.object.get_post_type_display(),) + context['crumb_url'] = "/%ss/" % slugify(self.object.get_post_type_display()) else: context['breadcrumbs'] = (self.object._meta.verbose_name_plural,) try: context['crumb_url'] except KeyError: try: - context['crumb_url'] = reverse('%s:list' % self.object._meta.verbose_name_plural.slugify()) + context['crumb_url'] = reverse('%s:list' % slugify(self.object._meta.verbose_name_plural)) except: - # special case for pages: - context['breadcrumbs'] = (self.object.title,) - context['crumb_url'] = None + # special case for books: + if self.object._meta.verbose_name_plural == 'books': + context['crumb_url'] = reverse('books:list') + elif self.object._meta.verbose_name_plural == 'Animal/Plant': + context['crumb_url'] = reverse('sightings:list') + else: + # special case for pages: + context['breadcrumbs'] = (self.object.title,) + context['crumb_url'] = None return context diff --git a/app/utils/widgets.py b/app/utils/widgets.py index f4a7a4a..87b39ec 100644 --- a/app/utils/widgets.py +++ b/app/utils/widgets.py @@ -4,7 +4,7 @@ from django.contrib import admin from django.contrib.admin.widgets import AdminFileWidget from django.contrib.gis.admin import OSMGeoAdmin from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string from django.template import Context from django.forms.widgets import SelectMultiple |