Skip to content

Commit

Permalink
Add guidebook images to MIVS show info step
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kitsuta committed Nov 5, 2024
1 parent 77699b4 commit 8199304
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -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 ###
40 changes: 30 additions & 10 deletions uber/models/mivs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
14 changes: 14 additions & 0 deletions uber/site_sections/guests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
19 changes: 6 additions & 13 deletions uber/site_sections/mits.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
63 changes: 50 additions & 13 deletions uber/site_sections/mivs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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')
Expand Down
37 changes: 33 additions & 4 deletions uber/templates/mivs/show_info.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ <h2>Show Information for MIVS</h2>
<h4>Gameplay Images</h4>
Please mark your two best gameplay images, or upload new ones.
<table>
{% for screenshot in game.screenshots %}
{% for screenshot in game.screenshots|rejectattr('is_header')|rejectattr('is_thumbnail') %}
<tr>
<td><ul><li></li></ul></td>
<td><a target="_blank" href="{{ screenshot.url }}">{{ screenshot.filename }}</a></td>
Expand All @@ -49,10 +49,39 @@ <h4>Gameplay Images</h4>
{% endfor %}
</table>
<a class="btn btn-primary" href="screenshot?game_id={{ game.id }}&use_in_promo=True">Upload a Screenshot</a>

{# TODO: Add Guidebook image upload!! #}

<br/><br/>
<h4>Guidebook Images</h4>
Please upload images to accompany {{ game.title }}'s entry on Guidebook.<br/><br/>

<form method="post" enctype="multipart/form-data" class="form-horizontal" action="show_info">
<div class="form-group">
<label class="col-sm-3 control-label">Header Image</label>
<div class="col-sm-6">
<input type="file" name="header_image" />
{% if game.guidebook_header %}
<a target="_blank" href="{{ game.guidebook_header.url }}">{{ game.guidebook_header.filename }}</a>
{% endif %}
</div>
<p class="help-block col-sm-9 col-sm-offset-3">
A {{ c.GUIDEBOOK_HEADER_SIZE|format_image_size }} image to display on the schedule next to your game details.<br/>
{% if game.guidebook_header %}Uploading a file will replace the existing image.{% endif %}
</p>
</div>

<div class="form-group">
<label class="col-sm-3 control-label">Thumbnail Image</label>
<div class="col-sm-6">
<input type="file" name="thumbnail_image" />
{% if game.guidebook_thumbnail %}
<a target="_blank" href="{{ game.guidebook_thumbnail.url }}">{{ game.guidebook_thumbnail.filename }}</a>
{% endif %}
</div>
<p class="help-block col-sm-9 col-sm-offset-3">
A {{ c.GUIDEBOOK_THUMBNAIL_SIZE|format_image_size }} image to display on the schedule next to your game name.<br/>
{% if game.guidebook_thumbnail %}Uploading a file will replace the existing image.{% endif %}
</p>
</div>

{{ csrf_token() }}
<input type="hidden" name="id" value="{{ game.id }}" />

Expand Down
11 changes: 10 additions & 1 deletion uber/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 8199304

Please sign in to comment.