diff --git a/.gitignore b/.gitignore index d926c6e..34e0a4f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,8 @@ venv/ *.JPEG /static/lightbox/images/ /static/ckeditor/ckeditor/skins/ -apps/*/migrations/*.py -!**/migrations/__init__.py +# apps/*/migrations/*.py +# !**/migrations/__init__.py .DS_Store db.sqlite3 .idea/ diff --git a/apps/blog/migrations/0001_initial.py b/apps/blog/migrations/0001_initial.py new file mode 100644 index 0000000..5af108f --- /dev/null +++ b/apps/blog/migrations/0001_initial.py @@ -0,0 +1,89 @@ +# Generated by Django 5.2 on 2025-05-25 21:49 + +import django.db.models.deletion +import django.utils.timezone +import taggit.managers +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ( + "taggit", + "0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx", + ), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Contact", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(default="", max_length=255)), + ("email", models.EmailField(max_length=254)), + ("subject", models.CharField(max_length=255)), + ("message", models.TextField()), + ], + ), + migrations.CreateModel( + name="Post", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=50, unique=True)), + ("slug", models.SlugField(unique=True)), + ("updated_on", models.DateField(default=django.utils.timezone.now)), + ("created_on", models.DateField(default=django.utils.timezone.now)), + ("content", models.TextField()), + ( + "status", + models.IntegerField( + choices=[(0, "Draft"), (1, "Publish")], default=0 + ), + ), + ("thumb", models.ImageField(blank=True, upload_to="")), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="blog_posts", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "tag", + taggit.managers.TaggableManager( + help_text="A comma-separated list of tags.", + through="taggit.TaggedItem", + to="taggit.Tag", + verbose_name="Tags", + ), + ), + ], + options={ + "verbose_name": "Blog", + "verbose_name_plural": "Blogs", + "ordering": ["-created_on"], + }, + ), + ] diff --git a/apps/gallery/admin.py b/apps/gallery/admin.py index 00d4014..eeadf30 100644 --- a/apps/gallery/admin.py +++ b/apps/gallery/admin.py @@ -3,18 +3,24 @@ Author: Jared Paubel Version: 0.1 """ +from django.utils.text import slugify +from django.urls import path, reverse +from django.shortcuts import render, redirect +from django.contrib import messages +from django import forms +from django.utils.translation import gettext_lazy as _ +from photologue.forms import UploadZipForm +from zipfile import ZipFile +from io import BytesIO from django.contrib import admin from apps.gallery.models import ( CountryAlbum, CityGallery, CityPhoto, City, - Country, - PhotoGallery -) -from photologue.admin import ( - PhotoAdmin as BasePhotoAdmin + Country ) +from photologue.admin import PhotoAdmin class CityGalleryInline(admin.StackedInline): @@ -58,12 +64,88 @@ def get_model_perms(self, request): return {} +class CityPhotoZipUploadForm(UploadZipForm): + """Form for uploading a zip file as CityPhoto objects.""" + gallery = forms.ModelChoiceField( + queryset=CityGallery.objects.select_related('city', 'album__country').all(), + label=_("City Gallery"), + required=True + ) + city = forms.ModelChoiceField( + queryset=City.objects.all(), + label=_("City"), + required=False + ) + country = forms.ModelChoiceField( + queryset=Country.objects.all(), + label=_("Country"), + required=False + ) + + @admin.register(CityPhoto) -class CityPhotoAdmin(BasePhotoAdmin): +class CityPhotoAdmin(PhotoAdmin): autocomplete_fields = ['country', 'city'] - def get_model_perms(self, request): - """ - Return empty perms dict, hiding the model from admin index. - """ - return {} + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + 'upload_zip/', + self.admin_site.admin_view(self.upload_zip), + name='gallery_cityphoto_upload_zip', + ), + ] + return custom_urls + urls + + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context['upload_zip_url'] = reverse( + 'admin:gallery_cityphoto_upload_zip' + ) + return super().changelist_view(request, extra_context=extra_context) + + def upload_zip(self, request): + opts = self.model._meta + app_label = opts.app_label + + if request.method == 'POST': + form = CityPhotoZipUploadForm(request.POST, request.FILES) + if form.is_valid(): + city = form.cleaned_data.get('city') + country = form.cleaned_data.get('country') + zip_file = request.FILES['zip_file'] + with ZipFile(zip_file) as archive: + for idx, filename in enumerate( + archive.namelist(), + start=1 + ): + if filename.lower().endswith( + ('.jpg', '.jpeg', '.png', '.gif') + ): + data = archive.read(filename) + photo = CityPhoto() + photo.title = "{} {}".format(city.name, idx) + photo.image.save(filename, BytesIO(data)) + photo.city = city + photo.country = country + photo.save() + messages.success(request, _("Photos uploaded successfully.")) + return redirect('admin:gallery_cityphoto_changelist') + else: + form = CityPhotoZipUploadForm() + + context = { + 'form': form, + 'opts': opts, + 'app_label': app_label, + 'has_change_permission': self.has_change_permission(request), + } + return render(request, 'admin/gallery/upload_zip.html', context) + + # def get_model_perms(self, request): + # """ + # Return empty perms dict, hiding the model from admin index. + # """ + # return {} diff --git a/apps/gallery/migrations/0001_initial.py b/apps/gallery/migrations/0001_initial.py new file mode 100644 index 0000000..4acbd36 --- /dev/null +++ b/apps/gallery/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# Generated by Django 5.2 on 2025-05-22 20:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("photologue", "0013_alter_watermark_image"), + ] + + operations = [ + migrations.CreateModel( + name="PhotoGallery", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("country", models.CharField(max_length=199)), + ("content", models.TextField()), + ( + "slug", + models.SlugField(allow_unicode=True, blank=True, max_length=250), + ), + ("galleries", models.ManyToManyField(to="photologue.gallery")), + ], + options={ + "verbose_name": "Gallery", + "verbose_name_plural": "Galleries", + "ordering": ["country"], + }, + ), + ] diff --git a/apps/gallery/migrations/0004_city_country_cityphoto_city_country_countryalbum_and_more.py b/apps/gallery/migrations/0004_city_country_cityphoto_city_country_countryalbum_and_more.py new file mode 100644 index 0000000..bb1169e --- /dev/null +++ b/apps/gallery/migrations/0004_city_country_cityphoto_city_country_countryalbum_and_more.py @@ -0,0 +1,196 @@ +# Generated by Django 5.2 on 2025-05-25 21:49 + +import django.db.models.deletion +import sortedm2m.fields +import taggit.managers +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("gallery", "0003_rename_countryalbum_photogallery"), + ("photologue", "0013_alter_watermark_image"), + ( + "taggit", + "0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx", + ), + ] + + operations = [ + migrations.CreateModel( + name="City", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ], + options={ + "verbose_name_plural": "Cities", + "ordering": ["name"], + }, + ), + migrations.CreateModel( + name="Country", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ], + options={ + "verbose_name_plural": "Countries", + "ordering": ["name"], + }, + ), + migrations.CreateModel( + name="CityPhoto", + fields=[ + ( + "photo_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="photologue.photo", + ), + ), + ( + "city", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="gallery.city", + ), + ), + ( + "country", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="gallery.country", + ), + ), + ], + options={ + "verbose_name": "City Photo", + "verbose_name_plural": "City Photos", + }, + bases=("photologue.photo",), + ), + migrations.AddField( + model_name="city", + name="country", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="cities", + to="gallery.country", + ), + ), + migrations.CreateModel( + name="CountryAlbum", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=50, unique=True)), + ("slug", models.SlugField(unique=True)), + ( + "country", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="album", + to="gallery.country", + ), + ), + ( + "tags", + taggit.managers.TaggableManager( + blank=True, + help_text="A comma-separated list of tags.", + through="taggit.TaggedItem", + to="taggit.Tag", + verbose_name="Tags", + ), + ), + ], + options={ + "verbose_name": "Country Album", + "verbose_name_plural": "Country Albums", + }, + ), + migrations.CreateModel( + name="CityGallery", + fields=[ + ( + "gallery_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="photologue.gallery", + ), + ), + ( + "city", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="gallery", + to="gallery.city", + ), + ), + ( + "city_photos", + sortedm2m.fields.SortedManyToManyField( + blank=True, + help_text=None, + related_name="city_gallery", + to="gallery.cityphoto", + verbose_name="photos", + ), + ), + ( + "album", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="city_galleries", + to="gallery.countryalbum", + ), + ), + ], + options={ + "verbose_name": "City Gallery", + "verbose_name_plural": "City Galleries", + }, + bases=("photologue.gallery",), + ), + migrations.AlterUniqueTogether( + name="city", + unique_together={("name", "country")}, + ), + ] diff --git a/apps/gallery/migrations/0005_delete_photogallery.py b/apps/gallery/migrations/0005_delete_photogallery.py new file mode 100644 index 0000000..98ec2f6 --- /dev/null +++ b/apps/gallery/migrations/0005_delete_photogallery.py @@ -0,0 +1,16 @@ +# Generated by Django 5.2 on 2025-05-25 22:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("gallery", "0004_city_country_cityphoto_city_country_countryalbum_and_more"), + ] + + operations = [ + migrations.DeleteModel( + name="PhotoGallery", + ), + ] diff --git a/apps/gallery/models.py b/apps/gallery/models.py index 7888a2e..8b6a183 100644 --- a/apps/gallery/models.py +++ b/apps/gallery/models.py @@ -10,27 +10,6 @@ from sortedm2m.fields import SortedManyToManyField -class PhotoGallery(models.Model): - """Photo gallery model.""" - - # TODO: Change format to get Photos directly from - # Photologue, then generate new to enforce pagination - galleries = models.ManyToManyField(Gallery) - country = models.CharField(max_length=199) - content = models.TextField() - slug = models.SlugField(max_length=250, allow_unicode=True, blank=True) - - class Meta: - """Metadata for PhotoGallery class.""" - ordering = ["country"] - verbose_name = "Gallery" - verbose_name_plural = "Galleries" - - def __str__(self): - """String representation of name.""" - return f"{self.country}" - - class Country(models.Model): """A specified country.""" name = models.CharField(max_length=100, unique=True) @@ -86,7 +65,13 @@ def save(self, *args, **kwargs): if not self.title: self.title = "{}, {}".format(self.city, self.country) if not self.slug: - self.slug = "{} {}".format(self.city, self.country) + base_slug = slugify("{}-{}".format(self.city, self.country)) + slug = base_slug + counter = 1 + while CityPhoto.objects.filter(slug=slug).exists(): + slug = "{}-{}".format(base_slug, counter) + counter += 1 + self.slug = slug super().save(*args, **kwargs) diff --git a/apps/index/migrations/0001_initial.py b/apps/index/migrations/0001_initial.py new file mode 100644 index 0000000..2d884af --- /dev/null +++ b/apps/index/migrations/0001_initial.py @@ -0,0 +1,85 @@ +# Generated by Django 5.2 on 2025-05-25 21:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Index", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "hero_banner", + models.ImageField( + default="/images/default/default_hero_banner.png", + upload_to="images", + ), + ), + ( + "hero_image", + models.ImageField( + default="/images/default/default_hero_image.png", + upload_to="images", + ), + ), + ( + "greeting_title", + models.CharField( + default="Welcome to Prokope.io!", max_length=200, null=True + ), + ), + ( + "greeting_description", + models.TextField( + default="My personal portfolio for my thoughts and achievements.", + null=True, + ), + ), + ( + "about_me_title", + models.CharField(default="About Jay", max_length=200, null=True), + ), + ( + "about_me_description", + models.TextField( + default=( + "I'm from Kansas, served in the Marines, and work as a software ", + "developer intern while studying at Kansas State University", + ), + null=True, + ), + ), + ( + "about_prokope_title", + models.CharField( + default="What is 'Prokope'", max_length=200, null=True + ), + ), + ( + "about_prokope_description", + models.TextField( + default="Prokope means 'to chop down what gets in the way'", + null=True, + ), + ), + ], + options={ + "verbose_name": "Index", + "verbose_name_plural": "Index", + }, + ), + ] diff --git a/apps/index/views.py b/apps/index/views.py index 82146fb..f144cc9 100644 --- a/apps/index/views.py +++ b/apps/index/views.py @@ -9,7 +9,6 @@ from apps.index.models import Index from apps.blog.models import Post -from apps.gallery.models import PhotoGallery def index_view(request: HttpRequest) -> HttpResponse: diff --git a/prokope/settings.py b/prokope/settings.py index 31efe93..9717424 100755 --- a/prokope/settings.py +++ b/prokope/settings.py @@ -19,8 +19,7 @@ SECRET_KEY = os.environ["SECRET_KEY"] # SECURITY WARNING: don't run with debug turned on in production! -# DEBUG = os.environ.get("ENVIRONMENT", "development") == "development" -DEBUG = True +DEBUG = os.environ.get("ENVIRONMENT", "development") == "development" # The `DYNO` env var is set on Heroku CI, but it's not a real Heroku app, so we have to # also explicitly exclude CI: diff --git a/templates/admin/gallery/cityphoto/change_list.html b/templates/admin/gallery/cityphoto/change_list.html new file mode 100644 index 0000000..c9e9418 --- /dev/null +++ b/templates/admin/gallery/cityphoto/change_list.html @@ -0,0 +1,7 @@ +{% extends "admin/change_list.html" %} +{% block object-tools-items %} + {{ block.super }} +
On this page you can upload many photos at once, as long as you have + put them all in a zip archive. The photos can be either:
++ {% if form.errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} +
+ {{ form.non_field_errors }} + {% endif %} + + + +{% endblock %}