diff --git a/common/migrations/0010_auto_20200429_1805.py b/common/migrations/0010_auto_20200429_1805.py new file mode 100644 index 00000000..e32ea1aa --- /dev/null +++ b/common/migrations/0010_auto_20200429_1805.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.12 on 2020-04-29 18:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0009_upload_hosting'), + ] + + operations = [ + migrations.AlterField( + model_name='upload', + name='destination', + field=models.CharField(max_length=256, verbose_name='destination path'), + ), + ] diff --git a/common/util.py b/common/util.py index 79935171..0238efed 100644 --- a/common/util.py +++ b/common/util.py @@ -7,6 +7,8 @@ from PIL import Image from django.contrib.auth import get_user_model from django.utils.text import slugify as django_slugify +from django.conf import settings +from django.db.models.fields.files import ImageFieldFile SLUG_MAX_LENGTH = 50 @@ -160,3 +162,33 @@ def crop_banner(img_path, dest_path): image = image.crop(box) image.save(dest_path) + + +def check_image_size(image_type: str, image: ImageFieldFile) -> bool: + image_size = '{0}x{1}'.format(image.width, image.height) + if image_type == 'icon' and image_size != settings.ICON_SIZE: + return False + if image_type == 'large icon' and image_size != settings.ICON_LARGE_SIZE: + return False + if image_type == 'banner' and image_size != settings.BANNER_SIZE: + return False + return True + + +def resize_image(image_type: str, image: ImageFieldFile): + # using style of processing as described here: https://docs.djangoproject.com/en/2.2/topics/files/ + image_size = None + if image_type not in ['icon', 'large icon', 'banner']: + raise ValueError("Unsupported image type: %s" % image_type) + if image_type == 'icon': + image_size = settings.ICON_SIZE.split('x') + if image_type == 'large icon': + image_size = settings.ICON_LARGE_SIZE.split('x') + if image_type == 'banner': + image_size = settings.BANNER_SIZE.split('x') + image.open() + pil_image = Image.open(image) + resized_image = pil_image.resize((int(image_size[0]), int(image_size[1]))) + pil_image.close() + image.open(mode='w') + resized_image.save(image) diff --git a/games/forms.py b/games/forms.py index 1851463b..63be0c30 100644 --- a/games/forms.py +++ b/games/forms.py @@ -7,6 +7,7 @@ from crispy_forms.layout import Submit, ButtonHolder, Fieldset, Field from django import forms from django.conf import settings +from django.forms import ImageField from django.utils.safestring import mark_safe from django_select2.forms import ( ModelSelect2Widget, @@ -64,7 +65,7 @@ class Meta: "platforms", "genres", "description", - "title_logo", + "banner", ) widgets = { "platforms": Select2MultipleWidget, @@ -90,19 +91,11 @@ def __init__(self, *args, **kwargs): "it. Don't write your own. For old games, check Mobygame's Ad " "Blurbs, look for the English back cover text." ) - - self.fields["title_logo"] = CroppieField( - options={ - "viewport": {"width": 875, "height": 345}, - "boundary": {"width": 875, "height": 345}, - "showZoomer": True, - } - ) - self.fields["title_logo"].label = "Upload an image" - self.fields["title_logo"].help_text = ( + self.fields["banner"].label = "Upload an image" + self.fields["banner"].help_text = ( "The banner should include the game's title. " - "Please make sure that your banner doesn't rely on " - "transparency as those won't be reflected in the final image" + "Please make sure that your banner doesn't rely on " + "transparency as those won't be reflected in the final image." ) self.helper = FormHelper() @@ -118,7 +111,7 @@ def __init__(self, *args, **kwargs): "platforms", "genres", "description", - Field("title_logo", template="includes/upload_button.html"), + Field("banner", style='width: 100%'), ), ButtonHolder(Submit("submit", "Submit")), ) @@ -181,7 +174,7 @@ class Meta: "platforms", "genres", "description", - "title_logo", + "banner", "reason", ) @@ -200,19 +193,11 @@ def __init__(self, payload, *args, **kwargs): super(GameEditForm, self).__init__(payload, *args, **kwargs) self.fields["name"].label = "Title" self.fields["year"].label = "Release year" - self.fields["title_logo"] = CroppieField( - options={ - "viewport": {"width": 875, "height": 345}, - "boundary": {"width": 875, "height": 345}, - "showZoomer": True, - "url": payload["title_logo"].url if payload.get("title_logo") else "", - } - ) - self.fields["title_logo"].label = "Upload an image" - self.fields["title_logo"].required = False + self.fields["banner"].label = "Upload an image" + self.fields["banner"].required = False self.helper = FormHelper() - self.helper.include_media = False + self.helper.include_media = True self.helper.layout = Layout( Fieldset( None, @@ -224,7 +209,7 @@ def __init__(self, payload, *args, **kwargs): "platforms", "genres", "description", - Field("title_logo", template="includes/upload_button.html"), + Field("banner", style='width: 100%'), "reason", ), ButtonHolder(Submit("submit", "Submit")), diff --git a/games/management/commands/checkbanners.py b/games/management/commands/checkbanners.py index 6e94d043..5bd9d0ed 100644 --- a/games/management/commands/checkbanners.py +++ b/games/management/commands/checkbanners.py @@ -11,9 +11,9 @@ def handle(self, *args, **kwargs): icon_path = os.path.join(settings.MEDIA_ROOT, game.icon.name) if game.icon.name and not os.path.exists(icon_path): print("%s is missing icon" % game) - banner_path = os.path.join(settings.MEDIA_ROOT, game.title_logo.name) - if game.title_logo.name and not os.path.exists(banner_path): + banner_path = os.path.join(settings.MEDIA_ROOT, game.banner.name) + if game.banner.name and not os.path.exists(banner_path): print("%s is missing banner" % game) - if game.is_public and not game.title_logo.name: + if game.is_public and not game.banner.name: print("%s has no banner" % game) diff --git a/games/migrations/0041_auto_20200429_1805.py b/games/migrations/0041_auto_20200429_1805.py new file mode 100644 index 00000000..61f394dd --- /dev/null +++ b/games/migrations/0041_auto_20200429_1805.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.12 on 2020-04-29 18:05 + +from django.db import migrations, models +import games.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0040_auto_20200116_0249'), + ] + + operations = [ + migrations.RenameField( + model_name='game', + old_name='title_logo', + new_name='banner' + ), + migrations.AlterField( + model_name='game', + name='banner', + field=models.ImageField(blank=True, upload_to=games.models.game_banner_path), + ), + migrations.AlterField( + model_name='company', + name='logo', + field=models.ImageField(blank=True, upload_to=games.models.company_logo_path), + ), + migrations.AlterField( + model_name='game', + name='icon', + field=models.ImageField(blank=True, upload_to=games.models.game_icon_path), + ), + ] diff --git a/games/models.py b/games/models.py index 24917564..65d31622 100644 --- a/games/models.py +++ b/games/models.py @@ -3,6 +3,7 @@ import datetime import json import logging +import os import random import re from collections import defaultdict @@ -33,12 +34,28 @@ } +# Path functions for ImageFields +def company_logo_path(instance, filename): + file_ext = os.path.splitext(filename)[1] + return 'companies/logos/{0}{1}'.format(instance.slug, file_ext) + + +def game_icon_path(instance, filename): + file_ext = os.path.splitext(filename)[1] + return 'games/icons/{0}{1}'.format(instance.slug, file_ext) + + +def game_banner_path(instance, filename): + file_ext = os.path.splitext(filename)[1] + return 'games/banners/{0}{1}'.format(instance.slug, file_ext) + + class Company(models.Model): """Gaming company""" name = models.CharField(max_length=127) slug = models.SlugField(unique=True) - logo = models.ImageField(upload_to="companies/logos", blank=True) + logo = models.ImageField(upload_to=company_logo_path, blank=True) website = models.CharField(max_length=128, blank=True) class Meta: @@ -182,7 +199,7 @@ class Game(models.Model): # These model fields are editable by the user TRACKED_FIELDS = [ "name", "year", "platforms", "genres", "publisher", "developer", - "website", "description", "title_logo" + "website", "description", "banner" ] name = models.CharField(max_length=200) @@ -205,8 +222,8 @@ class Game(models.Model): on_delete=models.SET_NULL, ) website = models.CharField(max_length=200, blank=True) - icon = models.ImageField(upload_to="games/icons", blank=True) - title_logo = models.ImageField(upload_to="games/banners", blank=True) + icon = models.ImageField(upload_to=game_icon_path, blank=True) + banner = models.ImageField(upload_to=game_banner_path, blank=True) description = models.TextField(blank=True) is_public = models.BooleanField("Published", default=False) created = models.DateTimeField(auto_now_add=True) @@ -262,14 +279,14 @@ def website_url_hr(self): @property def banner_url(self): """Return URL for the game banner""" - if self.title_logo: - return reverse("get_banner", kwargs={"slug": self.slug}) + if self.banner: + return self.banner.url @property def icon_url(self): """Return URL for the game icon""" if self.icon: - return reverse("get_icon", kwargs={"slug": self.slug}) + return self.icon.url @property def flag_labels(self): @@ -291,7 +308,7 @@ def get_change_model(self): "website": self.website, "description": self.description, - "title_logo": self.title_logo, + "banner": self.banner, } def get_changes(self): @@ -329,7 +346,7 @@ def apply_changes(self, change_set): self.developer = change_set.developer self.website = change_set.website self.description = change_set.description - self.title_logo = change_set.title_logo + self.banner = change_set.banner def has_installer(self): """Return whether this game has an installer""" @@ -349,15 +366,15 @@ def get_absolute_url(self): def set_logo_from_steam(self): """Fetch the banner from Steam and use it for the game""" - if self.title_logo or not self.steamid: + if self.banner or not self.steamid: return - self.title_logo = ContentFile( + self.banner = ContentFile( steam.get_capsule(self.steamid), "%s.jpg" % self.steamid ) def set_logo_from_steam_api(self, img_url): """Sets the game banner from the Steam API URLs""" - self.title_logo = ContentFile( + self.banner = ContentFile( steam.get_image(self.steamid, img_url), "%s.jpg" % self.steamid ) @@ -369,9 +386,9 @@ def set_icon_from_steam_api(self, img_url): def set_logo_from_gog(self, gog_game): """Sets the game logo from the data retrieved from GOG""" - if self.title_logo or not self.gogid: + if self.banner or not self.gogid: return - self.title_logo = ContentFile( + self.banner = ContentFile( gog.get_logo(gog_game), "gog-%s.jpg" % self.gogid ) diff --git a/games/signals.py b/games/signals.py new file mode 100644 index 00000000..f29d030b --- /dev/null +++ b/games/signals.py @@ -0,0 +1,28 @@ +from django.db.models.signals import pre_save +from django.dispatch import receiver +from common.util import check_image_size, resize_image + +from games.models import Game + + +@receiver(pre_save, sender=Game) +def prepare_images(sender, **kwargs): + # Ensure icon and banner are of proper sizes + instance = kwargs.get('instance') + if instance.pk: + # Game already in database, check if files need to be changed + try: + game = Game.objects.get(pk=instance.pk) + old_icon = game.icon + old_banner = game.banner + if instance.icon and old_icon and old_icon.name != instance.icon.name: + old_icon.delete(save=False) + if instance.banner and old_banner and old_banner.name != instance.banner.name: + old_banner.delete(save=False) + except Game.DoesNotExist: + return + # Check icon and banner sizes and resize if required + if instance.icon and not check_image_size('icon', instance.icon): + resize_image('icon', instance.icon) + if instance.banner and not check_image_size('banner', instance.banner): + resize_image('banner', instance.banner) diff --git a/games/tests/test_forms.py b/games/tests/test_forms.py index 5b2a34a7..c096f28c 100644 --- a/games/tests/test_forms.py +++ b/games/tests/test_forms.py @@ -69,11 +69,11 @@ def test_can_validate_basic_data(self): 'developer': self.developer.id, 'publisher': self.publisher.id }, { - 'title_logo': SimpleUploadedFile('front.png', image.getvalue()) + 'banner': SimpleUploadedFile('front.png', image.getvalue()) }) # XXX there's a problem with django-croppie preventing testing this form properly # The title_photo is made optional until this is fixed - form.fields['title_logo'].required = False + form.fields['banner'].required = False form.is_valid() # self.assertTrue(form.is_valid()) self.assertFalse(form.errors) diff --git a/games/urls/pages.py b/games/urls/pages.py index e66fec1c..64386629 100644 --- a/games/urls/pages.py +++ b/games/urls/pages.py @@ -30,10 +30,10 @@ path('game-issue', views.submit_issue, name='game-submit-issue'), - path('banner/.jpg', + path('banner//', views.get_banner, name='get_banner'), - path('icon/.png', + path('icon//', views.get_icon, name='get_icon'), path('install//view/', diff --git a/games/views/pages.py b/games/views/pages.py index 5450d874..b37668a5 100644 --- a/games/views/pages.py +++ b/games/views/pages.py @@ -20,7 +20,6 @@ from django.views.decorators.cache import never_cache from django.views.decorators.http import require_POST from django.views.generic import ListView -from sorl.thumbnail import get_thumbnail from accounts.decorators import check_installer_restrictions, user_confirmed_required from games import models @@ -480,19 +479,14 @@ def item_link(self, item): def get_banner(request, slug): - """Serve game title in an appropriate format for the client.""" + """Serve game banner in an appropriate format for the client.""" try: game = Game.objects.get(slug=slug) except Game.DoesNotExist: game = None - if not game or not game.title_logo: + if not game or not game.banner: raise Http404 - try: - thumbnail = get_thumbnail(game.title_logo, settings.BANNER_SIZE, crop="center") - except AttributeError: - game.title_logo.delete() - raise Http404 - return redirect(thumbnail.url) + return redirect(game.banner.url) def get_icon(request, slug): @@ -502,14 +496,7 @@ def get_icon(request, slug): game = None if not game or not game.icon: raise Http404 - try: - thumbnail = get_thumbnail( - game.icon, settings.ICON_SIZE, crop="center", format="PNG" - ) - except AttributeError: - game.icon.delete() - raise Http404 - return redirect(thumbnail.url) + return redirect(game.icon.url) def game_list(request): diff --git a/scripts/sync_gog_games.py b/scripts/sync_gog_games.py index 97362b81..bc023ab5 100644 --- a/scripts/sync_gog_games.py +++ b/scripts/sync_gog_games.py @@ -200,7 +200,7 @@ def run(): game = create_game(gog_game) if not game: continue - if not game.title_logo: + if not game.banner: raise RuntimeError("No") LOGGER.info("Created game %s", game) i += 1 diff --git a/templates/admin/change-submission.html b/templates/admin/change-submission.html index 2c0fc7da..0c09ab3c 100644 --- a/templates/admin/change-submission.html +++ b/templates/admin/change-submission.html @@ -44,7 +44,7 @@ -- - {% if entry == "title_logo" %} + {% if entry == "banner" %} {% else %} {{ old_value }} @@ -56,7 +56,7 @@ ++ - {% if entry == "title_logo" %} + {% if entry == "banner" %} {% else %} {{ new_value }} diff --git a/templates/emails/daily_mod_mail.html b/templates/emails/daily_mod_mail.html index 32b22ec3..51013e5d 100644 --- a/templates/emails/daily_mod_mail.html +++ b/templates/emails/daily_mod_mail.html @@ -42,11 +42,11 @@

Unpublished installers

{% for installer in installers %} {% with installer.game as game %} - {% thumbnail game.title_logo "184x69" crop="center" as img %} - {{ game.name }} - {% empty %} - no image - {% endthumbnail %} + {% if game.banner %} + {{ game.name }} + {% else %} + {{ game.name }} + {% endif %} {% endwith %} {% endfor %} @@ -68,11 +68,11 @@

Unpublished submissions

{% for submission in submissions %} {% with submission.game as game %} - {% thumbnail game.title_logo "184x69" crop="center" as img %} - {{ game.name }} - {% empty %} - no image - {% endthumbnail %} + {% if game.banner %} + {{ game.name }} + {% else %} + {{ game.name }} + {% endif %} {% endwith %} {% endfor %} diff --git a/templates/games/detail.html b/templates/games/detail.html index abc7334a..9b10c5b9 100644 --- a/templates/games/detail.html +++ b/templates/games/detail.html @@ -6,10 +6,13 @@ {% block extra_head %} -{% thumbnail game.title_logo "300" as img %} - - -{% endthumbnail %} +{% if game.banner %} + + +{% else %} + + +{% endif %} @@ -52,9 +55,11 @@
- {% thumbnail game.title_logo "300" as img %} - - {% endthumbnail %} + {% if game.banner %} + + {% else %} + + {% endif %}
    {% if game.genres.count %}
  • diff --git a/templates/games/submit.html b/templates/games/submit.html index bf67ba90..b2de592c 100644 --- a/templates/games/submit.html +++ b/templates/games/submit.html @@ -6,39 +6,6 @@ {% endblock %} {% block extra_head %} {{ form.media.css }} - - {% endblock %} {% block content %} diff --git a/templates/home.html b/templates/home.html index 0491815c..aefa5179 100644 --- a/templates/home.html +++ b/templates/home.html @@ -105,11 +105,11 @@

    New games

    {% for game in new_games %} @@ -124,11 +124,11 @@

    Updates

    {% for game in updated_games %} diff --git a/templates/includes/game_preview.html b/templates/includes/game_preview.html index 2871b7dd..621a1b0c 100644 --- a/templates/includes/game_preview.html +++ b/templates/includes/game_preview.html @@ -5,10 +5,18 @@
    {% if not game.change_for %} - {{ game.name }} + {% if game.banner %} + {{ game.name }} + {% else %} + {{ game.name }} + {% endif %} {% else %} - {{ game.change_for.name }} + {% if game.change_for.banner %} + {{ game.change_for.name }} + {% else %} + {{ game.change_for.name }} + {% endif %} {% endif %}
    diff --git a/templates/installers/view.html b/templates/installers/view.html index c3ff85a5..eab4e6de 100644 --- a/templates/installers/view.html +++ b/templates/installers/view.html @@ -3,10 +3,13 @@ {% block extra_head %} -{% thumbnail installer.game.title_logo "300" as img %} - - -{% endthumbnail %} +{% if game.banner %} + + +{% else %} + + +{% endif %}