Skip to content

Commit

Permalink
Merge pull request #4391 from magfest/fix-magdev1316
Browse files Browse the repository at this point in the history
Update MITS for Super 2025
  • Loading branch information
kitsuta committed Aug 15, 2024
2 parents 5a6dd83 + 348250c commit 5eb0cbd
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Add is_header and is_thumbnail as columns for MITS pictures
Revision ID: 643d80417d4a
Revises: 0ba9060b0434
Create Date: 2024-08-15 01:12:39.387716
"""


# revision identifiers, used by Alembic.
revision = '643d80417d4a'
down_revision = '0ba9060b0434'
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():
op.add_column('mits_picture', sa.Column('is_header', sa.Boolean(), server_default='False', nullable=False))
op.add_column('mits_picture', sa.Column('is_thumbnail', sa.Boolean(), server_default='False', nullable=False))


def downgrade():
op.drop_column('mits_picture', 'is_thumbnail')
op.drop_column('mits_picture', 'is_header')
10 changes: 5 additions & 5 deletions uber/configspec.ini
Original file line number Diff line number Diff line change
Expand Up @@ -627,9 +627,6 @@ mivs_codes_required = integer(default=7)
# how the codes are used.
mivs_codes_requiring_instructions = string_list(default=list("password", "custom"))

# Uploaded screenshot files must have one of the following file extensions.
mivs_allowed_screenshot_types = string_list(default=list("gif", "gifv", "jpg", "jpeg", "png"))

# Config values which indicate a problem with the game. This list is checked
# in a few places to determine whether a status indicated by a judge indicates
# a problem which stopped them from being able to judge the game.
Expand Down Expand Up @@ -680,8 +677,11 @@ mits_email_signature = string(default=" - The MITS Team")

# We use these (width, height) measurements to mark uploaded images as
# either a header or thumbnail. The measurements below are for Guidebook.
mits_header_size = string_list(default=list('640','240'))
mits_thumbnail_size = string_list(default=list('240','240'))
guidebook_header_size = string_list(default=list('640','240'))
guidebook_thumbnail_size = string_list(default=list('240','240'))

# Uploaded guidebook images (e.g., MITS, MIVS) must have one of the following file extensions.
guidebook_allowed_image_types = string_list(default=list("gif", "jpg", "jpeg", "png"))


# =============================
Expand Down
8 changes: 8 additions & 0 deletions uber/custom_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,14 @@ def format_phone(val, country='US'):
return val


@JinjaEnv.jinja_filter
def format_image_size(val):
if not val or len(val) != 2:
return

return f"{val[0]}x{val[1]}px"


@JinjaEnv.jinja_filter
def jsonize(x):
is_empty = x is None or isinstance(x, jinja2.runtime.Undefined)
Expand Down
13 changes: 6 additions & 7 deletions uber/model_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from uber.badge_funcs import get_real_badge_type
from uber.config import c
from uber.custom_tags import format_currency
from uber.custom_tags import format_currency, readable_join
from uber.decorators import prereg_validation, validation
from uber.models import (AccessGroup, AdminAccount, ApiToken, Attendee, ArtShowApplication, ArtShowPiece,
AttendeeTournament, Attraction, AttractionFeature, Department, DeptRole, Event,
Expand Down Expand Up @@ -476,7 +476,7 @@ def mivs_description(image):

@validation.IndieGameImage
def mivs_valid_type(screenshot):
if screenshot.extension not in c.MIVS_ALLOWED_SCREENSHOT_TYPES:
if screenshot.extension not in c.GUIDEBOOK_ALLOWED_IMAGE_TYPES:
return 'Our server did not recognize your upload as a valid image'


Expand All @@ -488,21 +488,20 @@ def mivs_valid_type(screenshot):
('name', 'Production Team Name')
]


MITSApplicant.required = [
('first_name', 'First Name'),
('last_name', 'Last Name'),
('email', 'Email Address'),
('cellphone', 'Cellphone Number')
]


MITSGame.required = [
('name', 'Name'),
('description', 'Description')
]

MITSPicture.required = [
('description', 'Description')
]

MITSDocument.required = [
('description', 'Description')
Expand Down Expand Up @@ -533,8 +532,8 @@ def address_required_for_sellers(team):
def min_num_days_hours(team):
if team.days_available is not None and team.days_available < 3:
return 'You must be available at least 3 days to present at MITS.'
if team.hours_available is not None and team.hours_available < 4:
return 'You must be able to show at least 4 hours per day to present at MITS.'
if team.hours_available is not None and team.hours_available < 8:
return 'You must be able to show at least 8 hours per day to present at MITS.'


@validation.MITSTeam
Expand Down
35 changes: 9 additions & 26 deletions uber/models/mits.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import os
import cherrypy
from functools import wraps
from datetime import datetime

from PIL import Image
from pytz import UTC
from residue import CoerceUTF8 as UnicodeText, UTCDateTime, UUID
from sqlalchemy import and_
from sqlalchemy.schema import ForeignKey
Expand Down Expand Up @@ -202,22 +203,18 @@ def guidebook_location(self):
return ''

@property
def guidebook_image(self):
if not self.pictures:
return ''
def guidebook_header(self):
for image in self.pictures:
if image.is_header:
return image.filename
return self.pictures[0].filename
return image
return ''

@property
def guidebook_thumbnail(self):
if not self.pictures:
return ''
for image in self.pictures:
if image.is_thumbnail:
return image.filename
return self.pictures[1].filename if len(self.pictures) > 1 else self.pictures[0].filename
return image
return ''

@property
def guidebook_images(self):
Expand Down Expand Up @@ -249,6 +246,8 @@ class MITSPicture(MagModel):
content_type = Column(UnicodeText)
extension = Column(UnicodeText)
description = Column(UnicodeText)
is_header = Column(Boolean, default=False)
is_thumbnail = Column(Boolean, default=False)

@property
def url(self):
Expand All @@ -258,22 +257,6 @@ def url(self):
def filepath(self):
return os.path.join(c.MITS_PICTURE_DIR, str(self.id))

@property
def is_header(self):
try:
return Image.open(self.filepath).size == tuple(map(int, c.MITS_HEADER_SIZE))
except OSError:
# This probably isn't an image, so it's not a header image
return

@property
def is_thumbnail(self):
try:
return Image.open(self.filepath).size == tuple(map(int, c.MITS_THUMBNAIL_SIZE))
except OSError:
# This probably isn't an image, so it's not a thumbnail image
return


class MITSDocument(MagModel):
game_id = Column(UUID, ForeignKey('mits_game.id'))
Expand Down
81 changes: 70 additions & 11 deletions uber/site_sections/mits.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
import shutil
from datetime import datetime, timedelta
from PIL import Image

import cherrypy
from cherrypy.lib.static import serve_file
from pockets import listify
from pockets.autolog import log
from pytz import UTC

from uber.config import c
from uber.custom_tags import format_image_size, readable_join
from uber.decorators import ajax, all_renderable, csrf_protected, render
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, localized_now
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 ''


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())
with open(new_pic.filepath, 'wb') as f:
shutil.copyfileobj(pic.file, f)
return new_pic


@all_renderable(public=True)
Expand Down Expand Up @@ -194,36 +214,75 @@ def delete_document(self, session, id):
return "Document deleted"

def game(self, session, message='', **params):
header_pic, thumbnail_pic = None, None
game = session.mits_game(params, applicant=True)
header_image = params.get('header_image')
thumbnail_image = params.get('thumbnail_image')

if cherrypy.request.method == 'POST':
documents = params.get('upload_documents', [])
documents = documents if isinstance(documents, list) else [documents]
if not documents[0].filename and not game.documents:
message = "You must upload at least one rulesbook or other document."
else:

if not message:
# Once you access .file, it's gone, so we need to create
# MITSPicture objects BEFORE checking image size

if header_image and header_image.filename:
message = _check_pic_filetype(header_image)
if not message:
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_pic_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)

if not message:
pictures = params.get('upload_pictures', [])
pictures = pictures if isinstance(pictures, list) else [pictures]
for pic in [p for p in pictures if p.filename]:
if pic.filename.split('.')[-1].lower() not in c.GUIDEBOOK_ALLOWED_IMAGE_TYPES:
message = f'Image {pic.filename} is not one of the allowed extensions: '\
f'{readable_join(c.GUIDEBOOK_ALLOWED_IMAGE_TYPES)}.'

if not message:
for doc in [d for d in documents if d.filename]:
new_doc = MITSDocument(game_id=game.id, filename=doc.filename)
with open(new_doc.filepath, 'wb') as f:
shutil.copyfileobj(doc.file, f)
session.add(new_doc)

pictures = params.get('upload_pictures', [])
pictures = pictures if isinstance(pictures, list) else [pictures]

for pic in [p for p in pictures if p.filename]:
new_pic = MITSPicture(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)
new_pic = add_new_image(pic, game)
session.add(new_pic)
if header_pic:
session.delete(game.guidebook_header)
session.add(header_pic)
if thumbnail_pic:
session.delete(game.guidebook_thumbnail)
session.add(thumbnail_pic)
session.add(game)
raise HTTPRedirect('index?message={}', 'Game saved')

return {
'game': game,
'header_image': params.get('header_image', ''),
'message': message
}

Expand Down
Loading

0 comments on commit 5eb0cbd

Please sign in to comment.