From 8199304c0da629bfb93afd0e8a6235474c42b4a3 Mon Sep 17 00:00:00 2001 From: Victoria Earl Date: Tue, 5 Nov 2024 10:52:06 -0500 Subject: [PATCH] Add guidebook images to MIVS show info step Fixes https://magfest.atlassian.net/browse/MAGDEV-1339 by adding the ability to upload images formatted for Guidebook, and requires studios to do this for every game before confirming their show info. Also checks other show info required fields when completing the Show Info checklist step, which seemed to be missing from the page handler. --- ...dd_header_thumbnail_flag_to_mivs_images.py | 67 +++++++++++++++++++ uber/models/mivs.py | 40 ++++++++--- uber/site_sections/guests.py | 14 ++++ uber/site_sections/mits.py | 19 ++---- uber/site_sections/mivs.py | 63 +++++++++++++---- uber/templates/mivs/show_info.html | 37 ++++++++-- uber/utils.py | 11 ++- 7 files changed, 210 insertions(+), 41 deletions(-) create mode 100644 alembic/versions/f01a2ad10d79_add_header_thumbnail_flag_to_mivs_images.py diff --git a/alembic/versions/f01a2ad10d79_add_header_thumbnail_flag_to_mivs_images.py b/alembic/versions/f01a2ad10d79_add_header_thumbnail_flag_to_mivs_images.py new file mode 100644 index 000000000..204a04055 --- /dev/null +++ b/alembic/versions/f01a2ad10d79_add_header_thumbnail_flag_to_mivs_images.py @@ -0,0 +1,67 @@ +"""Add header/thumbnail flag to MIVS images + +Revision ID: f01a2ad10d79 +Revises: 58756e2dfe4d +Create Date: 2024-11-05 15:23:12.065060 + +""" + + +# revision identifiers, used by Alembic. +revision = 'f01a2ad10d79' +down_revision = '58756e2dfe4d' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + + +try: + is_sqlite = op.get_context().dialect.name == 'sqlite' +except Exception: + is_sqlite = False + +if is_sqlite: + op.get_context().connection.execute('PRAGMA foreign_keys=ON;') + utcnow_server_default = "(datetime('now', 'utc'))" +else: + utcnow_server_default = "timezone('utc', current_timestamp)" + +def sqlite_column_reflect_listener(inspector, table, column_info): + """Adds parenthesis around SQLite datetime defaults for utcnow.""" + if column_info['default'] == "datetime('now', 'utc')": + column_info['default'] = utcnow_server_default + +sqlite_reflect_kwargs = { + 'listeners': [('column_reflect', sqlite_column_reflect_listener)] +} + +# =========================================================================== +# HOWTO: Handle alter statements in SQLite +# +# def upgrade(): +# if is_sqlite: +# with op.batch_alter_table('table_name', reflect_kwargs=sqlite_reflect_kwargs) as batch_op: +# batch_op.alter_column('column_name', type_=sa.Unicode(), server_default='', nullable=False) +# else: +# op.alter_column('table_name', 'column_name', type_=sa.Unicode(), server_default='', nullable=False) +# +# =========================================================================== + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('indie_game_image', sa.Column('is_header', sa.Boolean(), server_default='False', nullable=False)) + op.add_column('indie_game_image', sa.Column('is_thumbnail', sa.Boolean(), server_default='False', nullable=False)) + op.create_unique_constraint(op.f('uq_lottery_application_attendee_id'), 'lottery_application', ['attendee_id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('uq_lottery_application_attendee_id'), 'lottery_application', type_='unique') + op.drop_column('indie_game_image', 'is_thumbnail') + op.drop_column('indie_game_image', 'is_header') + # ### end Alembic commands ### diff --git a/uber/models/mivs.py b/uber/models/mivs.py index f3143ee02..ba6594d8c 100644 --- a/uber/models/mivs.py +++ b/uber/models/mivs.py @@ -491,23 +491,41 @@ def guidebook_location(self): return '' @property - def guidebook_image(self): - return self.best_screenshot_download_filenames()[0] + def guidebook_header(self): + for image in self.images: + if image.is_header: + return image + return '' @property def guidebook_thumbnail(self): - return self.best_screenshot_download_filenames()[1] \ - if len(self.best_screenshot_download_filenames()) > 1 else self.best_screenshot_download_filenames()[0] + for image in self.images: + if image.is_thumbnail: + return image + return '' @property def guidebook_images(self): - image_filenames = [self.best_screenshot_download_filenames()[0]] - images = [self.best_screenshot_downloads()[0]] - if self.guidebook_image != self.guidebook_thumbnail: - image_filenames.append(self.guidebook_thumbnail) - images.append(self.best_screenshot_downloads()[1]) + if not self.images: + return ['', ''] + + header = None + thumbnail = None + for image in self.images: + if image.is_header and not header: + header = image + if image.is_thumbnail and not thumbnail: + thumbnail = image + + if not header: + header = self.images[0] + if not thumbnail: + thumbnail = self.images[1] if len(self.images) > 1 else self.images[0] - return image_filenames, images + if header == thumbnail: + return [header.filename], [header] + else: + return [header.filename, thumbnail.filename], [header, thumbnail] class IndieGameImage(MagModel): @@ -518,6 +536,8 @@ class IndieGameImage(MagModel): description = Column(UnicodeText) use_in_promo = Column(Boolean, default=False) is_screenshot = Column(Boolean, default=True) + is_header = Column(Boolean, default=False) + is_thumbnail = Column(Boolean, default=False) @property def url(self): diff --git a/uber/site_sections/guests.py b/uber/site_sections/guests.py index 02bcff2ff..a2ffb7587 100644 --- a/uber/site_sections/guests.py +++ b/uber/site_sections/guests.py @@ -10,6 +10,7 @@ from uber.decorators import ajax, all_renderable, render from uber.errors import HTTPRedirect from uber.models import GuestMerch, GuestDetailedTravelPlan, GuestTravelPlans +from uber.model_checks import mivs_show_info_required_fields from uber.utils import check from uber.tasks.email import send_email @@ -564,6 +565,19 @@ def mivs_show_info(self, session, guest_id, message='', **params): if not params.get('show_info_updated'): message = "Please confirm you have updated your studio's and game's information." + if not message and not guest.group.studio.contact_phone: + message = 'Please update your show information to enter a contact phone number for MIVS staff.' + + if not message: + for game in guest.group.studio.games: + if not game.guidebook_header or not game.guidebook_thumbnail: + message = "Please upload a Guidebook header and thumbnail." + else: + message = mivs_show_info_required_fields(game) + if message: + message = f"{game.title} show info is missing something: {message}" + break + if not message: guest.group.studio.show_info_updated = True session.add(guest) diff --git a/uber/site_sections/mits.py b/uber/site_sections/mits.py index c4aee822e..91ae98ad3 100644 --- a/uber/site_sections/mits.py +++ b/uber/site_sections/mits.py @@ -14,21 +14,14 @@ from uber.errors import HTTPRedirect from uber.models import Email, MITSDocument, MITSPicture, MITSTeam from uber.tasks.email import send_email -from uber.utils import check, check_image_size, localized_now - - -def _check_pic_filetype(pic): - if pic.filename.split('.')[-1].lower() not in c.GUIDEBOOK_ALLOWED_IMAGE_TYPES: - return f'Image {pic.filename} is not one of the allowed extensions: '\ - f'{readable_join(c.GUIDEBOOK_ALLOWED_IMAGE_TYPES)}.' - return '' +from uber.utils import check, check_image_size, localized_now, check_guidebook_image_filetype def add_new_image(pic, game): new_pic = MITSPicture(game_id=game.id, - filename=pic.filename, - content_type=pic.content_type.value, - extension=pic.filename.split('.')[-1].lower()) + filename=pic.filename, + content_type=pic.content_type.value, + extension=pic.filename.split('.')[-1].lower()) with open(new_pic.filepath, 'wb') as f: shutil.copyfileobj(pic.file, f) return new_pic @@ -230,7 +223,7 @@ def game(self, session, message='', **params): # MITSPicture objects BEFORE checking image size if header_image and header_image.filename: - message = _check_pic_filetype(header_image) + message = check_guidebook_image_filetype(header_image) if not message: header_pic = add_new_image(header_image, game) header_pic.is_header = True @@ -241,7 +234,7 @@ def game(self, session, message='', **params): if not message: if thumbnail_image and thumbnail_image.filename: - message = _check_pic_filetype(thumbnail_image) + message = check_guidebook_image_filetype(thumbnail_image) if not message: thumbnail_pic = add_new_image(thumbnail_image, game) thumbnail_pic.is_thumbnail = True diff --git a/uber/site_sections/mivs.py b/uber/site_sections/mivs.py index b2e51456d..187f89f55 100644 --- a/uber/site_sections/mivs.py +++ b/uber/site_sections/mivs.py @@ -5,10 +5,21 @@ from cherrypy.lib.static import serve_file from uber.config import c +from uber.custom_tags import format_image_size from uber.decorators import all_renderable, csrf_protected from uber.errors import HTTPRedirect -from uber.models import Attendee, Group, GuestGroup, IndieDeveloper, IndieStudio -from uber.utils import add_opt, check, check_csrf +from uber.models import Attendee, Group, GuestGroup, IndieDeveloper, IndieGameImage +from uber.utils import add_opt, check, check_csrf, check_image_size, check_guidebook_image_filetype + + +def add_new_image(pic, game): + new_pic = IndieGameImage(game_id=game.id, + filename=pic.filename, + content_type=pic.content_type.value, + extension=pic.filename.split('.')[-1].lower()) + with open(new_pic.filepath, 'wb') as f: + shutil.copyfileobj(pic.file, f) + return new_pic @all_renderable(public=True) @@ -253,30 +264,56 @@ def confirm(self, session, csrf_token=None, decision=None): 'developers': developers } - def show_info(self, session, id, message='', promo_image=None, **params): + def show_info(self, session, id, message='', **params): game = session.indie_game(id=id) + header_pic, thumbnail_pic = None, None cherrypy.session['studio_id'] = game.studio.id if cherrypy.request.method == 'POST': + header_image = params.get('header_image') + thumbnail_image = params.get('thumbnail_image') game.apply(params, bools=['tournament_at_event', 'has_multiplayer', 'leaderboard_challenge'], restricted=False) # Setting restricted to false lets us define custom bools and checkgroups game.studio.name = params.get('studio_name', '') + if not params.get('contact_phone', ''): message = "Please enter a phone number for MIVS staff to contact your studio." else: game.studio.contact_phone = params.get('contact_phone', '') - if promo_image: - image = session.indie_game_image(params) - image.game = game - image.content_type = promo_image.content_type.value - image.extension = promo_image.filename.split('.')[-1].lower() - image.is_screenshot = False - message = check(image) + + if header_image and header_image.filename: + message = check_guidebook_image_filetype(header_image) if not message: - with open(image.filepath, 'wb') as f: - shutil.copyfileobj(promo_image.file, f) - message = check(game) or check(game.studio) + header_pic = add_new_image(header_image, game) + header_pic.is_header = True + if not check_image_size(header_pic.filepath, c.GUIDEBOOK_HEADER_SIZE): + message = f"Your header image must be {format_image_size(c.GUIDEBOOK_HEADER_SIZE)}." + elif not game.guidebook_header: + message = f"You must upload a {format_image_size(c.GUIDEBOOK_HEADER_SIZE)} header image." + + if not message: + if thumbnail_image and thumbnail_image.filename: + message = check_guidebook_image_filetype(thumbnail_image) + if not message: + thumbnail_pic = add_new_image(thumbnail_image, game) + thumbnail_pic.is_thumbnail = True + if not check_image_size(thumbnail_pic.filepath, c.GUIDEBOOK_THUMBNAIL_SIZE): + message = f"Your thumbnail image must be {format_image_size(c.GUIDEBOOK_THUMBNAIL_SIZE)}." + elif not game.guidebook_thumbnail: + message = f"You must upload a {format_image_size(c.GUIDEBOOK_THUMBNAIL_SIZE)} thumbnail image." + + if not message: + message = check(game) or check(game.studio) if not message: session.add(game) + if header_pic: + if game.guidebook_header: + session.delete(game.guidebook_header) + session.add(header_pic) + if thumbnail_pic: + if game.guidebook_thumbnail: + session.delete(game.guidebook_thumbnail) + session.add(thumbnail_pic) + if game.studio.group.guest: raise HTTPRedirect('../guests/mivs_show_info?guest_id={}&message={}', game.studio.group.guest.id, 'Game information uploaded') diff --git a/uber/templates/mivs/show_info.html b/uber/templates/mivs/show_info.html index fb9670e9e..83425dec0 100644 --- a/uber/templates/mivs/show_info.html +++ b/uber/templates/mivs/show_info.html @@ -25,7 +25,7 @@

Show Information for MIVS

Gameplay Images

Please mark your two best gameplay images, or upload new ones. - {% for screenshot in game.screenshots %} + {% for screenshot in game.screenshots|rejectattr('is_header')|rejectattr('is_thumbnail') %} @@ -49,10 +49,39 @@

Gameplay Images

{% endfor %}
{{ screenshot.filename }}
Upload a Screenshot - - {# TODO: Add Guidebook image upload!! #} - +

+

Guidebook Images

+ Please upload images to accompany {{ game.title }}'s entry on Guidebook.

+
+
+ +
+ + {% if game.guidebook_header %} + {{ game.guidebook_header.filename }} + {% endif %} +
+

+ A {{ c.GUIDEBOOK_HEADER_SIZE|format_image_size }} image to display on the schedule next to your game details.
+ {% if game.guidebook_header %}Uploading a file will replace the existing image.{% endif %} +

+
+ +
+ +
+ + {% if game.guidebook_thumbnail %} + {{ game.guidebook_thumbnail.filename }} + {% endif %} +
+

+ A {{ c.GUIDEBOOK_THUMBNAIL_SIZE|format_image_size }} image to display on the schedule next to your game name.
+ {% if game.guidebook_thumbnail %}Uploading a file will replace the existing image.{% endif %} +

+
+ {{ csrf_token() }} diff --git a/uber/utils.py b/uber/utils.py index c7008f9b2..2adde0e4e 100644 --- a/uber/utils.py +++ b/uber/utils.py @@ -637,10 +637,19 @@ def check_image_size(image, size_list): try: return Image.open(image).size == tuple(map(int, size_list)) except OSError: - # This probably isn't an image, so it's not a header image + # This probably isn't an image at all return +def check_guidebook_image_filetype(pic): + from uber.custom_tags import readable_join + + if pic.filename.split('.')[-1].lower() not in c.GUIDEBOOK_ALLOWED_IMAGE_TYPES: + return f'Image {pic.filename} is not one of the allowed extensions: '\ + f'{readable_join(c.GUIDEBOOK_ALLOWED_IMAGE_TYPES)}.' + return '' + + def validate_model(forms, model, preview_model=None, is_admin=False): from wtforms import validators