mirror of
https://github.com/godotengine/godot-showreel-voting.git
synced 2026-09-03 18:15:13 +03:00
Merge pull request 'Showreel entry submission functionality' (#1) from feature/add-submission-functionality into master
Reviewed-on: https://git.godot.foundation/website/showreel-voting/pulls/1
This commit is contained in:
@@ -19,6 +19,8 @@ services:
|
||||
image: docker.io/mariadb
|
||||
volumes:
|
||||
- mariadb_data:/var/lib/mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
MARIADB_DATABASE: showreel
|
||||
MARIADB_USER: showreel
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from urllib.parse import urlparse
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import IntegerField, StringField, ValidationError
|
||||
from wtforms import IntegerField, SelectField, StringField, ValidationError, EmailField
|
||||
from wtforms.validators import InputRequired
|
||||
|
||||
from gdshowreelvote.database import DB, Showreel, ShowreelStatus
|
||||
from gdshowreelvote.utils import downvote_video, skip_video, upvote_video
|
||||
|
||||
|
||||
@@ -15,6 +17,13 @@ def validate_action(form, field):
|
||||
if field.data:
|
||||
if VOTE_ACTIONS.get(field.data) is None:
|
||||
raise ValidationError(f"Action '{field.data}' is not supported.")
|
||||
|
||||
|
||||
def validate_urls(form, field):
|
||||
if field.data:
|
||||
parsed = urlparse(field.data)
|
||||
if not all([parsed.scheme, parsed.netloc]):
|
||||
raise ValidationError(f'Invalid URL: {field.data}')
|
||||
|
||||
|
||||
class CastVoteForm(FlaskForm):
|
||||
@@ -24,3 +33,39 @@ class CastVoteForm(FlaskForm):
|
||||
|
||||
class SelectVideoForm(FlaskForm):
|
||||
video_id = IntegerField('Video ID', validators=[InputRequired()])
|
||||
|
||||
|
||||
class VideoSubmissionForm(FlaskForm):
|
||||
game = StringField('Game Title', validators=[InputRequired()])
|
||||
author_name = StringField('Author Name', validators=[InputRequired()])
|
||||
contact_email = EmailField('Contact Email', validators=[InputRequired()])
|
||||
video_link = StringField('Video Link', validators=[InputRequired(), validate_urls]) # TODO: Check specific URL formats
|
||||
video_download_link = StringField('Video Download Link', validators=[InputRequired(), validate_urls])
|
||||
follow_me_link = StringField('Follow Me Link', validators=[InputRequired(), validate_urls])
|
||||
store_link = StringField('Store Link', validators=[InputRequired(), validate_urls])
|
||||
|
||||
|
||||
class ManageShowreelsForm(FlaskForm):
|
||||
showreel_id = SelectField('Showreel', validators=[InputRequired()], choices=[])
|
||||
showreel_status = SelectField('Showreel Status', validators=[InputRequired()],
|
||||
choices=[
|
||||
(ShowreelStatus.OPENED_TO_SUBMISSIONS.value, ShowreelStatus.OPENED_TO_SUBMISSIONS.value),
|
||||
(ShowreelStatus.VOTE.value, ShowreelStatus.VOTE.value),
|
||||
(ShowreelStatus.CLOSED.value, ShowreelStatus.CLOSED.value)
|
||||
])
|
||||
|
||||
def validate(self, extra_validators = None):
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
submissions = DB.session.query(Showreel).filter(Showreel.status == ShowreelStatus.OPENED_TO_SUBMISSIONS).first()
|
||||
if submissions and self.showreel_status.data == ShowreelStatus.OPENED_TO_SUBMISSIONS.value:
|
||||
self.showreel_status.errors.append("There is already a showreel open for submissions.")
|
||||
return False
|
||||
|
||||
vote = DB.session.query(Showreel).filter(Showreel.status == ShowreelStatus.VOTE).first()
|
||||
if vote and self.showreel_status.data == ShowreelStatus.VOTE.value:
|
||||
self.showreel_status.errors.append("There is already a showreel open for voting.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,22 +1,46 @@
|
||||
import csv
|
||||
from io import StringIO
|
||||
from sqlalchemy import case, func
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
Response,
|
||||
current_app,
|
||||
g,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
url_for,
|
||||
)
|
||||
from sqlalchemy import case, func, or_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from flask import Blueprint, Response, current_app, g, redirect, render_template, request, url_for
|
||||
|
||||
from gdshowreelvote import auth
|
||||
from gdshowreelvote.blueprints.forms import VOTE_ACTIONS, CastVoteForm, SelectVideoForm
|
||||
from gdshowreelvote.database import DB, User, Video, Vote
|
||||
from gdshowreelvote.utils import choose_random_video, get_total_votes, video_data, vote_data, voting_possible
|
||||
|
||||
from gdshowreelvote.blueprints.forms import (
|
||||
VOTE_ACTIONS,
|
||||
CastVoteForm,
|
||||
ManageShowreelsForm,
|
||||
SelectVideoForm,
|
||||
VideoSubmissionForm,
|
||||
)
|
||||
from gdshowreelvote.database import DB, Showreel, ShowreelStatus, User, Video, Vote
|
||||
from gdshowreelvote.utils import (
|
||||
choose_random_video,
|
||||
extract_steam_app_id,
|
||||
get_total_votes_for_showreel,
|
||||
video_data,
|
||||
vote_data,
|
||||
voting_possible,
|
||||
)
|
||||
|
||||
bp = Blueprint('votes', __name__)
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
def home():
|
||||
content = render_template('home.html', user=g.user)
|
||||
active_submissions = DB.session.query(Showreel).filter(Showreel.status == ShowreelStatus.OPENED_TO_SUBMISSIONS).first()
|
||||
active_vote = DB.session.query(Showreel).filter(Showreel.status == ShowreelStatus.VOTE).first()
|
||||
content = render_template('home.html', user=g.user, active_submissions=active_submissions, active_vote=active_vote)
|
||||
return render_template('default.html', content = content, user=g.user)
|
||||
|
||||
|
||||
@@ -99,13 +123,13 @@ def history():
|
||||
return redirect(url_for('votes.home'))
|
||||
limit = request.args.get('limit')
|
||||
page = int(request.args.get('page', 1))
|
||||
total_video_count = DB.session.query(Video).count()
|
||||
total_user_votes = DB.session.query(Vote).filter(Vote.user_id == g.user.id).count()
|
||||
total_video_count = DB.session.query(Video).join(Showreel).filter(Showreel.status == ShowreelStatus.VOTE).count()
|
||||
total_user_votes = DB.session.query(Vote).join(Video).join(Showreel).filter(Showreel.status == ShowreelStatus.VOTE).filter(Vote.user_id == g.user.id).count()
|
||||
progress = {
|
||||
'total': total_video_count,
|
||||
'current': total_user_votes,
|
||||
}
|
||||
query = DB.session.query(Vote).filter(Vote.user_id == g.user.id).order_by(Vote.created_at.desc())
|
||||
query = DB.session.query(Vote).join(Video).join(Showreel).filter(Showreel.status == ShowreelStatus.VOTE).filter(Vote.user_id == g.user.id).order_by(Vote.created_at.desc())
|
||||
|
||||
if limit == 'all':
|
||||
total_results = query.count()
|
||||
@@ -130,9 +154,19 @@ def history():
|
||||
@bp.route('/admin')
|
||||
@auth.admin_required
|
||||
def admin_view():
|
||||
total_votes, positive_votes, vote_tally = get_total_votes()
|
||||
form = ManageShowreelsForm()
|
||||
showreels = (
|
||||
DB.session.query(Showreel)
|
||||
.order_by(
|
||||
case(
|
||||
(Showreel.status == ShowreelStatus.VOTE, 0),
|
||||
(Showreel.status == ShowreelStatus.OPENED_TO_SUBMISSIONS, 1),
|
||||
else_=2))
|
||||
.all()
|
||||
)
|
||||
form.showreel_id.choices = [(showreel.id, showreel.title) for showreel in showreels]
|
||||
|
||||
content = render_template('admin.html', vote_tally=vote_tally, total_votes=total_votes, positive_votes=positive_votes)
|
||||
content = render_template('admin.html', form=form, showreels=showreels)
|
||||
if request.args.get('page'):
|
||||
return content
|
||||
return render_template('default.html', content = content, user=g.user)
|
||||
@@ -141,6 +175,10 @@ def admin_view():
|
||||
@bp.route('/results')
|
||||
@auth.admin_required
|
||||
def download_vote_results():
|
||||
showreel_id = request.args.get("showreel_id", type=int)
|
||||
showreel = DB.session.get(Showreel, showreel_id)
|
||||
if showreel is None:
|
||||
return render_template('error.html', title="Showreel Not Found", message="No showreel ID provided.")
|
||||
result = (
|
||||
DB.session.query(
|
||||
Video,
|
||||
@@ -151,13 +189,14 @@ def download_vote_results():
|
||||
)
|
||||
.outerjoin(Vote, Vote.video_id == Video.id)
|
||||
.outerjoin(User, User.id == Vote.user_id)
|
||||
.filter(Video.showreel_id == showreel_id)
|
||||
.group_by(Video.id)
|
||||
.order_by(func.coalesce(func.sum(Vote.rating), 0).desc()).all()
|
||||
)
|
||||
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
writer.writerow(['Author', 'Follow-me link', 'Game', 'Video link', 'Download link', 'Contact email', 'Store Link', 'Positive votes', 'Negative votes', 'staff', 'fund_member'])
|
||||
writer.writerow(['Author', 'Follow-me link', 'Game', 'Video link', 'Download link', 'Contact email', 'Store Link', 'Steam App ID', 'Positive votes', 'Negative votes', 'staff', 'fund_member'])
|
||||
|
||||
for video, plus_votes, minus_votes, staff_votes, fund_member_votes in result:
|
||||
writer.writerow([
|
||||
@@ -168,13 +207,14 @@ def download_vote_results():
|
||||
video.video_download_link,
|
||||
video.contact_email,
|
||||
video.store_link,
|
||||
extract_steam_app_id(video.store_link),
|
||||
plus_votes,
|
||||
minus_votes,
|
||||
staff_votes,
|
||||
fund_member_votes
|
||||
])
|
||||
response = Response(csv_file.getvalue(), mimetype='text/csv')
|
||||
response.headers["Content-Disposition"] = "attachment; filename=vote_results.csv"
|
||||
response.headers["Content-Disposition"] = f"attachment; filename=vote_results_{showreel.title}.csv"
|
||||
return response
|
||||
|
||||
|
||||
@@ -188,3 +228,159 @@ def video_view(video_id: int):
|
||||
data = video_data(video)
|
||||
content = render_template('video-view.html', data=data)
|
||||
return render_template('default.html', content = content, user=g.user, hide_nav=True)
|
||||
|
||||
|
||||
@bp.route('/submit', methods=['GET'])
|
||||
def submit():
|
||||
active_showreel = DB.session.query(Showreel).filter(Showreel.status == ShowreelStatus.OPENED_TO_SUBMISSIONS).count()
|
||||
if active_showreel != 1:
|
||||
current_app.logger.warning("No active showreel or multiple active showreels found.")
|
||||
error_template = render_template('error.html', title="Submissions Closed", message="Submissions are currently closed.")
|
||||
return render_template('default.html', content = error_template, user=g.user)
|
||||
|
||||
form = VideoSubmissionForm()
|
||||
content = render_template('submit.html', user=g.user, form=form)
|
||||
return render_template('default.html', content = content, user=g.user)
|
||||
|
||||
|
||||
@bp.route('/submit', methods=['POST'])
|
||||
@auth.login_required
|
||||
def post_submit():
|
||||
form = VideoSubmissionForm()
|
||||
if not form.validate():
|
||||
return render_template('submit.html', user=g.user, form=form)
|
||||
|
||||
active_showreel = DB.session.query(Showreel).filter(Showreel.status == ShowreelStatus.OPENED_TO_SUBMISSIONS).all()
|
||||
if not active_showreel or len(active_showreel) > 1:
|
||||
current_app.logger.warning("No active showreel or multiple active showreels found.")
|
||||
return render_template('error.html', title="Submissions Closed", message="Submissions are currently closed.")
|
||||
duplicate_video = DB.session.query(Video).filter(or_(Video.video_link == form.video_link.data, Video.video_download_link == form.video_download_link.data)).first()
|
||||
if duplicate_video:
|
||||
form.video_link.errors.append('A video with the same link or download link has already been submitted.')
|
||||
return render_template('submit.html', user=g.user, form=form)
|
||||
|
||||
active_showreel = active_showreel[0]
|
||||
new_video = Video(
|
||||
game=form.game.data,
|
||||
author_name=form.author_name.data,
|
||||
contact_email=form.contact_email.data,
|
||||
video_link=form.video_link.data,
|
||||
video_download_link=form.video_download_link.data,
|
||||
follow_me_link=form.follow_me_link.data,
|
||||
store_link=form.store_link.data,
|
||||
author=g.user,
|
||||
showreel=active_showreel
|
||||
)
|
||||
try:
|
||||
DB.session.add(new_video)
|
||||
DB.session.commit()
|
||||
except IntegrityError as e:
|
||||
current_app.logger.error(f"Database integrity error while submitting video: {e}")
|
||||
DB.session.rollback()
|
||||
return render_template('submit.html', user=g.user, form=form)
|
||||
return render_template('home.html', user=g.user, active_submissions=True, submission_success=True) # TODO: Add flag to show submission success message
|
||||
|
||||
|
||||
@bp.route('/showreel/update-status', methods=['POST'])
|
||||
@auth.admin_required
|
||||
def update_showreel_status():
|
||||
form = ManageShowreelsForm()
|
||||
showreels = DB.session.query(Showreel).all()
|
||||
form.showreel_id.choices = [(showreel.id, showreel.title) for showreel in showreels]
|
||||
if not form.validate():
|
||||
return redirect(url_for('votes.admin_view'))
|
||||
showreel = DB.session.query(Showreel).filter(Showreel.id == form.showreel_id.data).first()
|
||||
if not showreel:
|
||||
return render_template('error.html', title="Showreel Not Found", message="The requested showreel was not found.")
|
||||
|
||||
showreel.status = form.showreel_status.data
|
||||
DB.session.commit()
|
||||
return redirect(url_for('votes.admin_view'))
|
||||
|
||||
|
||||
@bp.route('/user/submissions', methods=['GET'])
|
||||
@auth.login_required
|
||||
def user_submissions():
|
||||
open_showreel = DB.session.query(Showreel).filter(Showreel.status == ShowreelStatus.OPENED_TO_SUBMISSIONS).first()
|
||||
open_submissions = DB.session.query(Video).filter(Video.author_id == g.user.id).filter(Video.showreel == open_showreel).all()
|
||||
closed_submissions = DB.session.query(Video).filter(Video.author_id == g.user.id).filter(Video.showreel != open_showreel).all()
|
||||
|
||||
content = render_template('user-submissions.html', user=g.user, open_submissions=open_submissions, closed_submissions=closed_submissions, open_showreel=open_showreel)
|
||||
if request.args.get('update'):
|
||||
return content
|
||||
return render_template('default.html', content = content, user=g.user)
|
||||
|
||||
|
||||
@bp.route('/user/submissions/<int:video_id>/manage', methods=['GET'])
|
||||
@auth.login_required
|
||||
def manage_submission(video_id: int):
|
||||
video = DB.session.query(Video).filter(Video.id == video_id).filter(Video.author == g.user).first()
|
||||
|
||||
if not video:
|
||||
return render_template('error.html', title="Video Not Found", message="The requested video submission was not found.")
|
||||
|
||||
if video.showreel.status != ShowreelStatus.OPENED_TO_SUBMISSIONS:
|
||||
return render_template('error.html', title="Cannot Manage Submission", message="Submissions can only be managed while the showreel is open to submissions.")
|
||||
|
||||
form = VideoSubmissionForm(obj=video)
|
||||
|
||||
content = render_template('manage-submission.html', user=g.user, form=form, video_id=video.id)
|
||||
return render_template('default.html', content = content, user=g.user)
|
||||
|
||||
|
||||
@bp.route('/user/submissions/<int:video_id>/delete', methods=['POST'])
|
||||
@auth.login_required
|
||||
def delete_submission(video_id: int):
|
||||
video = DB.session.query(Video).filter(Video.id == video_id).filter(Video.author == g.user).first()
|
||||
|
||||
if not video:
|
||||
return render_template('error.html', title="Video Not Found", message="The requested video submission was not found.")
|
||||
|
||||
if video.showreel.status != ShowreelStatus.OPENED_TO_SUBMISSIONS:
|
||||
return render_template('error.html', title="Cannot Delete Submission", message="Submissions can only be deleted while the showreel is open to submissions.")
|
||||
|
||||
DB.session.delete(video)
|
||||
DB.session.commit()
|
||||
|
||||
return redirect(url_for('votes.user_submissions'))
|
||||
|
||||
|
||||
@bp.route('/user/submissions/<int:video_id>/update', methods=['POST'])
|
||||
@auth.login_required
|
||||
def update_submission(video_id: int):
|
||||
video = DB.session.query(Video).filter(Video.id == video_id).filter(Video.author == g.user).first()
|
||||
|
||||
if not video:
|
||||
return render_template('error.html', title="Video Not Found", message="The requested video submission was not found.")
|
||||
|
||||
if video.showreel.status != ShowreelStatus.OPENED_TO_SUBMISSIONS:
|
||||
return render_template('error.html', title="Cannot Update Submission", message="Submissions can only be updated while the showreel is open to submissions.")
|
||||
|
||||
form = VideoSubmissionForm()
|
||||
if not form.validate():
|
||||
return render_template('update-submissions.html', user=g.user, submissions=[video], open_showreel=video.showreel, form=form)
|
||||
|
||||
video.game = form.game.data
|
||||
video.author_name = form.author_name.data
|
||||
video.contact_email = form.contact_email.data
|
||||
video.video_link = form.video_link.data
|
||||
video.video_download_link = form.video_download_link.data
|
||||
video.follow_me_link = form.follow_me_link.data
|
||||
video.store_link = form.store_link.data
|
||||
DB.session.commit()
|
||||
|
||||
return redirect(url_for('votes.user_submissions', update='1'))
|
||||
|
||||
|
||||
@bp.route("/showreel-results")
|
||||
@auth.admin_required
|
||||
def showreel_results():
|
||||
showreel_id = request.args.get("showreel_id", type=int)
|
||||
|
||||
showreel = DB.session.get(Showreel, showreel_id)
|
||||
if not showreel:
|
||||
return render_template('error.html', title="Showreel Not Found", message="The requested showreel was not found.")
|
||||
|
||||
vote_metrics = get_total_votes_for_showreel(showreel)
|
||||
|
||||
return render_template("partials/vote-results.html", metrics=vote_metrics, showreel=showreel)
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import List, Optional
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import Boolean, Column, DateTime, Enum, ForeignKey, Integer, MetaData, String, Table
|
||||
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, MetaData, String
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ migrate = Migrate()
|
||||
|
||||
|
||||
class ShowreelStatus(enum.Enum):
|
||||
OPENED_TO_SUBMISSIONS = 'OPEN'
|
||||
OPENED_TO_SUBMISSIONS = 'OPENED_TO_SUBMISSIONS'
|
||||
VOTE = 'VOTE'
|
||||
CLOSED = 'CLOSED'
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from urllib.parse import urlparse
|
||||
from sqlalchemy import and_, func
|
||||
from gdshowreelvote.database import DB, Showreel, ShowreelStatus, User, Video, Vote
|
||||
|
||||
@@ -88,8 +86,8 @@ def video_data(video: Video) -> Dict:
|
||||
|
||||
|
||||
def vote_data(user: User, video: Video) -> Tuple[Dict, Dict]:
|
||||
total_video_count = DB.session.query(Video).count()
|
||||
total_user_votes = DB.session.query(Vote).filter(Vote.user_id == user.id).count()
|
||||
total_video_count = DB.session.query(Video).join(Showreel).filter(Showreel.status == ShowreelStatus.VOTE).count()
|
||||
total_user_votes = DB.session.query(Vote).join(Video).join(Showreel).filter(Showreel.status == ShowreelStatus.VOTE).filter(Vote.user_id == user.id).count()
|
||||
|
||||
data = video_data(video) if video else None
|
||||
|
||||
@@ -101,9 +99,11 @@ def vote_data(user: User, video: Video) -> Tuple[Dict, Dict]:
|
||||
return data, progress
|
||||
|
||||
|
||||
def get_total_votes() -> Tuple[int, int, List[Tuple[Video, int, int]]]:
|
||||
total_votes = DB.session.query(func.count(Vote.id)).filter(Vote.rating != 0).scalar()
|
||||
positive_votes = DB.session.query(func.count(Vote.id)).filter(Vote.rating == 1).scalar()
|
||||
def get_total_votes_for_showreel(showreel: Showreel|None) -> Dict[str, Any]:
|
||||
selected_showreel = showreel if showreel else DB.session.query(Showreel).first()
|
||||
output = {}
|
||||
output['total_votes'] = DB.session.query(func.count(Vote.id)).join(Video).filter(Video.showreel == selected_showreel).filter(Vote.rating != 0).scalar()
|
||||
output['positive_votes'] = DB.session.query(func.count(Vote.id)).join(Video).filter(Video.showreel == selected_showreel).filter(Vote.rating == 1).scalar()
|
||||
results = (
|
||||
DB.session.query(
|
||||
Video,
|
||||
@@ -111,13 +111,43 @@ def get_total_votes() -> Tuple[int, int, List[Tuple[Video, int, int]]]:
|
||||
func.count(Vote.id).label("vote_count"),
|
||||
)
|
||||
.outerjoin(Vote, Vote.video_id == Video.id)
|
||||
.filter(Video.showreel == selected_showreel)
|
||||
.group_by(Video.id)
|
||||
.order_by(func.coalesce(func.sum(Vote.rating), 0).desc())
|
||||
.all()
|
||||
)
|
||||
output['results'] = []
|
||||
for video, vote_sum, vote_count in results:
|
||||
output['results'].append(
|
||||
{
|
||||
"id": video.id,
|
||||
"game": video.game,
|
||||
"author_name": video.author_name,
|
||||
"video_link": video.video_link,
|
||||
"vote_sum": vote_sum,
|
||||
"vote_count": vote_count,
|
||||
}
|
||||
)
|
||||
|
||||
return total_votes, positive_votes, results
|
||||
return output
|
||||
|
||||
|
||||
def voting_possible() -> bool:
|
||||
return DB.session.query(Showreel).where(Showreel.status==ShowreelStatus.VOTE).count() > 0
|
||||
|
||||
|
||||
def extract_steam_app_id(url: str) -> str:
|
||||
""" Extract the Steam App ID from a given URL. """
|
||||
if not url:
|
||||
return ""
|
||||
parsed = urlparse(url)
|
||||
|
||||
if parsed.netloc != 'store.steampowered.com':
|
||||
return ""
|
||||
path_parts = parsed.path.strip('/').split('/')
|
||||
if len(path_parts) < 2 or path_parts[0] != 'app':
|
||||
return ""
|
||||
app_id = path_parts[1]
|
||||
if not app_id.isdigit():
|
||||
return ""
|
||||
return app_id
|
||||
|
||||
11
main.py
11
main.py
@@ -61,6 +61,17 @@ def create_app(config=None):
|
||||
# Commands
|
||||
# ------------------------------------------------
|
||||
|
||||
@app.cli.command('create-showreel')
|
||||
@click.argument("name")
|
||||
def create_showreel(name):
|
||||
if not name:
|
||||
print('Showreel name is required.')
|
||||
return
|
||||
showreel = Showreel(status=ShowreelStatus.CLOSED, title=name)
|
||||
DB.session.add(showreel)
|
||||
DB.session.commit()
|
||||
print(f'Created showreel: {name} (ID: {showreel.id})')
|
||||
|
||||
@app.cli.command('create-sample-data')
|
||||
def create_sample_data():
|
||||
if current_app.config['ENV'] != 'dev':
|
||||
|
||||
@@ -237,3 +237,11 @@ textarea {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.error {
|
||||
color: #f65994;
|
||||
}
|
||||
|
||||
p.info {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -13,27 +13,68 @@
|
||||
}
|
||||
</style>
|
||||
<main id="admin">
|
||||
<h1 style="margin-bottom: 20px;">Vote results</h1>
|
||||
<div>
|
||||
<p>Total votes: <strong>{{ total_votes }}</strong></p>
|
||||
<p>Positive votes: <strong>{{ positive_votes }}</strong></p>
|
||||
<p>Negative votes: <strong>{{ total_votes - positive_votes }}</strong></p>
|
||||
</div>
|
||||
<br>
|
||||
<form action="{{ url_for('votes.download_vote_results') }}" method="get" style="display:inline;">
|
||||
<button type="submit" class="button primary small">Download results .csv</button>
|
||||
</form>
|
||||
<div>
|
||||
<div class="entries">
|
||||
{% for entry in vote_tally %}
|
||||
<div class="entry panel padded">
|
||||
<h2><a href="{{ url_for('votes.video_view', video_id=entry[0].id) }}">{{ entry[0].game }}</a></h2>
|
||||
<p>Category: <strong>{{ entry[0].showreel.title }}</strong></p>
|
||||
<p>Author: <strong>{{ entry[0].author_name }}</strong></p>
|
||||
<p>Vote sum: <strong>{{ entry[1] }}</strong></p>
|
||||
<p>Votes received: <strong>{{ entry[2] }}</strong></p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<h1>Manage Shorwreels</h1>
|
||||
<form action="{{ url_for('votes.update_showreel_status') }}" method="post" style="display:inline;">
|
||||
{{ form.csrf_token }}
|
||||
|
||||
{{ form.showreel_id.label }}
|
||||
<select id="showreel_id" name="showreel_id">
|
||||
{% for showreel in showreels %}
|
||||
<option value="{{ showreel.id }}"
|
||||
data-status="{{ showreel.status.name }}">
|
||||
{{ showreel.title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{{ form.showreel_status.label }}
|
||||
{{ form.showreel_status(id="showreel_status") }}
|
||||
|
||||
<button type="submit" class="button primary small">Save</button>
|
||||
</form>
|
||||
|
||||
<h1 style="margin-bottom: 20px;">Vote results</h1>
|
||||
<form action="{{ url_for('votes.download_vote_results') }}" method="get" style="display:inline;">
|
||||
<input type="hidden" id="download-showreel-id" name="showreel_id" value="">
|
||||
<button type="submit" class="button primary small">Download results .csv</button>
|
||||
</form>
|
||||
<div id="vote-results">
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const showreelSelect = document.getElementById('showreel_id');
|
||||
const statusSelect = document.getElementById('showreel_status');
|
||||
const voteResults = document.getElementById('vote-results');
|
||||
const downloadShowreelId = document.getElementById('download-showreel-id');
|
||||
|
||||
function updateStatus() {
|
||||
const selectedOption = showreelSelect.options[showreelSelect.selectedIndex];
|
||||
const status = selectedOption.dataset.status;
|
||||
|
||||
if (!status) return;
|
||||
|
||||
statusSelect.value = status;
|
||||
downloadShowreelId.value = showreelSelect.value;
|
||||
}
|
||||
|
||||
function updateResults() {
|
||||
const showreelId = showreelSelect.value;
|
||||
|
||||
fetch(`/showreel-results?showreel_id=${showreelId}`)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
voteResults.innerHTML = html;
|
||||
});
|
||||
}
|
||||
|
||||
showreelSelect.addEventListener('change', function () {
|
||||
updateStatus();
|
||||
updateResults();
|
||||
});
|
||||
|
||||
updateStatus();
|
||||
updateResults();
|
||||
});
|
||||
</script>
|
||||
4
templates/error.html
Normal file
4
templates/error.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<main>
|
||||
<h1>{{ title }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
</main>
|
||||
@@ -20,53 +20,73 @@
|
||||
}
|
||||
</style>
|
||||
<main>
|
||||
<div class="panel padded">
|
||||
{% comment %}
|
||||
<!-- During the voting period -->
|
||||
{% if user %}
|
||||
{% if user.can_vote() %}
|
||||
<p>Cast your votes until <strong>October 20th</strong> 📅</p>
|
||||
<ul>
|
||||
<li>Videos are presented in random order, and scores are hidden until the voting period ends</li>
|
||||
<li>Your votes are anonymous</li>
|
||||
<li>You can change your votes until the voting period ends</li>
|
||||
<li>We encourage you to vote for as many entries as you can</li>
|
||||
<li>If you found any issue or if you have a suggestion, please create an issue in our <a href="https://github.com/godotengine/godot-showreel-voting/issues">issue tracker</a></li>
|
||||
</ul>
|
||||
|
||||
<p style="margin: 20px 0;">Happy voting!</p>
|
||||
<a class="button primary" id="vote-link" href="/vote">Start Voting</a> <a class="button dim" href="/history">History</a>
|
||||
{% if active_submissions %}
|
||||
<div class="panel padded" style="margin-top: 20px;">
|
||||
<!-- During the submission period -->
|
||||
{% if user %}
|
||||
{% if submission_success %}
|
||||
<h2>Thank you for your submission!</h2>
|
||||
<p>Your entry has been received, voting will start on <strong>?????</strong> 📅</p>
|
||||
{% else %}
|
||||
<p>Submit your game for the Godot showreel until <strong>?????</strong> 📅</p>
|
||||
{% endif %}
|
||||
<a class="button primary" href="/submit">Submit entry</a>
|
||||
{% else %}
|
||||
<p>Only Godot Engine maintainers or members of the Development Fund can vote.</p>
|
||||
<p>If you would like to participate in the process, make sure to sign up to <a href="https://fund.godotengine.org">the Development Fund</a>.</p>
|
||||
<p style="margin-top: 20px;">
|
||||
<a href="https://fund.godotengine.org" class="button">Sign Up</a>
|
||||
</p>
|
||||
<p style="margin-top: 20px;">
|
||||
<strong>Already a fund member?</strong><br>
|
||||
If you became a member recently, try to <a href="{{ url_for('oidc.logout') }}">logout</a> and then log in again. Also make sure that you use the same account as you used on the fund page!
|
||||
</p>
|
||||
You need to be logged in to submit an entry:
|
||||
<a href="/login" class="button dim small">Log In</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif active_vote %}
|
||||
<div class="panel padded" style="margin-top: 20px;">
|
||||
<!-- During the voting period -->
|
||||
{% if user %}
|
||||
{% if user.can_vote() %}
|
||||
<p>Cast your votes until <strong>October 20th</strong> 📅</p>
|
||||
<ul>
|
||||
<li>Videos are presented in random order, and scores are hidden until the voting period ends</li>
|
||||
<li>Your votes are anonymous</li>
|
||||
<li>You can change your votes until the voting period ends</li>
|
||||
<li>We encourage you to vote for as many entries as you can</li>
|
||||
<li>If you found any issue or if you have a suggestion, please create an issue in our <a href="https://github.com/godotengine/godot-showreel-voting/issues">issue tracker</a></li>
|
||||
</ul>
|
||||
|
||||
<p style="margin: 20px 0;">Happy voting!</p>
|
||||
<a class="button primary" id="vote-link" href="/vote">Start Voting</a> <a class="button dim" href="/history">History</a>
|
||||
{% else %}
|
||||
<p>Only Godot Engine maintainers or members of the Development Fund can vote.</p>
|
||||
<p>If you would like to participate in the process, make sure to sign up to <a href="https://fund.godotengine.org">the Development Fund</a>.</p>
|
||||
<p style="margin-top: 20px;">
|
||||
<a href="https://fund.godotengine.org" class="button">Sign Up</a>
|
||||
</p>
|
||||
<p style="margin-top: 20px;">
|
||||
<strong>Already a fund member?</strong><br>
|
||||
If you became a member recently, try to <a href="{{ url_for('oidc.logout') }}">logout</a> and then log in again. Also make sure that you use the same account as you used on the fund page!
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<hr>
|
||||
You need to be logged in to vote:
|
||||
<a href="/login" class="button dim small">Log In</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<hr>
|
||||
You need to be logged in to vote:
|
||||
<a href="/login" class="button dim small">Log In</a>
|
||||
{% endif %}
|
||||
{% endcomment %}
|
||||
|
||||
<h1>Welcome!</h1>
|
||||
<p>The voting period for this years showreel videos has finished.</p>
|
||||
<p style="margin: 20px 0;">Thank you to everyone who participated!</p>
|
||||
</div>
|
||||
<h1>Welcome!</h1>
|
||||
<p>The voting period for this years showreel videos has finished.</p>
|
||||
<p style="margin: 20px 0;">Thank you to everyone who participated!</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel padded" style="margin-top: 20px;">
|
||||
<p>No showreels just yet!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="panel padded" style="margin-top: 20px;">
|
||||
<h2>Previous editions:<span><a href="https://www.youtube.com/playlist?list=PLeG_dAglpVo6EpaO9A1nkwJZOwrfiLdQ8">(View all)</a></span></h2>
|
||||
<div class="grid-2" style="margin-top: 10px;">
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/n1Lon_Q2T18" frameborder="0" allowfullscreen="" style="width:100%;aspect-ratio:16/9;height:auto; border-radius: 11px;"></iframe>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/W1_zKxYEP6Q" frameborder="0" allowfullscreen="" style="width:100%;aspect-ratio:16/9;height:auto; border-radius: 11px;"></iframe>
|
||||
<div class="panel padded" style="margin-top: 20px;">
|
||||
<h2>Previous editions:<span><a href="https://www.youtube.com/playlist?list=PLeG_dAglpVo6EpaO9A1nkwJZOwrfiLdQ8">(View all)</a></span></h2>
|
||||
<div class="grid-2" style="margin-top: 10px;">
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/n1Lon_Q2T18" frameborder="0" allowfullscreen="" style="width:100%;aspect-ratio:16/9;height:auto; border-radius: 11px;"></iframe>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/W1_zKxYEP6Q" frameborder="0" allowfullscreen="" style="width:100%;aspect-ratio:16/9;height:auto; border-radius: 11px;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
</a>
|
||||
<div id="dropdown-menu" class="dropdown-content">
|
||||
<a href="/about">About</a>
|
||||
{% if user %}
|
||||
<a href="/user/submissions">Submissions</a>
|
||||
{% endif %}
|
||||
{% if user.can_vote() %}
|
||||
<a href="/vote">Vote</a>
|
||||
<a href="/history">History</a>
|
||||
|
||||
101
templates/manage-submission.html
Normal file
101
templates/manage-submission.html
Normal file
@@ -0,0 +1,101 @@
|
||||
<main id="submission-form">
|
||||
<div>
|
||||
<h1>Showreel Submission</h1>
|
||||
<p>Fill in the form to submit your video for the Godot Showreel.</p>
|
||||
</div>
|
||||
<hr>
|
||||
<form method="POST" enctype="multipart/form-data" hx-post="{{ url_for('votes.update_submission', video_id=video_id, _method='POST') }}" hx-target='#submission-form' hx-swap='outerHTML'>
|
||||
{{ form.csrf_token }}
|
||||
|
||||
{{ form.game.label }}<br>
|
||||
{{ form.game(size=100) }}<br>
|
||||
{% for error in form.game.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}<br>
|
||||
|
||||
{{ form.author_name.label }}<br>
|
||||
{{ form.author_name(size=100) }}<br>
|
||||
{% for error in form.author_name.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}<br>
|
||||
|
||||
{{ form.contact_email.label }}<br>
|
||||
{{ form.contact_email(size=100) }}<br>
|
||||
{% for error in form.contact_email.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}<br>
|
||||
|
||||
{{ form.video_link.label }}<br>
|
||||
{{ form.video_link(size=100) }}
|
||||
<button type="button" class="button secondary" id="preview-btn">Preview</button><br>
|
||||
{% for error in form.video_link.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
|
||||
<div id="preview-container" style="display: none;">
|
||||
<iframe width="560" height="315" src="" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
{{ form.video_download_link.label }}<br>
|
||||
{{ form.video_download_link(size=100) }}<br>
|
||||
{% for error in form.video_download_link.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}<br>
|
||||
|
||||
{{ form.follow_me_link.label }}<br>
|
||||
{{ form.follow_me_link(size=100) }}<br>
|
||||
{% for error in form.follow_me_link.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}<br>
|
||||
|
||||
{{ form.store_link.label }}<br>
|
||||
{{ form.store_link(size=100) }}<br>
|
||||
{% for error in form.store_link.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}<br>
|
||||
|
||||
<button type="submit" class="button secondary">Update</button>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Render video preview
|
||||
function parseVideoID(videoLink) {
|
||||
const videoUrl = new URL(videoLink);
|
||||
console.log(videoUrl.hostname);
|
||||
const hostname = videoUrl.hostname
|
||||
if (hostname == 'youtu.be') {
|
||||
return videoUrl.pathname.slice(1);
|
||||
}
|
||||
else if (hostname === 'www.youtube.com' || hostname === 'youtube.com' || hostname === 'm.youtube.com') {
|
||||
if (videoUrl.pathname == '/watch') {
|
||||
return videoUrl.searchParams.get('v');
|
||||
}
|
||||
else if (videoUrl.pathname.startsWith('/embed/')) {
|
||||
return videoUrl.pathname.split('/')[2];
|
||||
}
|
||||
else if (videoUrl.pathname.startsWith('/v/')) {
|
||||
return videoUrl.pathname.split('/')[2];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('preview-btn').addEventListener('click', function() {
|
||||
const originalForm = document.querySelector("#submission-form form");
|
||||
const previewContainer = document.getElementById('preview-container');
|
||||
let videoID = parseVideoID(originalForm.querySelector("input[name='video_link']").value);
|
||||
if (!videoID) {
|
||||
previewContainer.innerHTML = '<p class="error">Invalid video link provided.</p>';
|
||||
previewContainer.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
videoID = 'https://www.youtube.com/embed/' + videoID + '?rel=0&autoplay=1';
|
||||
|
||||
previewContainer.querySelector('iframe').src = videoID;
|
||||
previewContainer.style.display = 'block';
|
||||
});
|
||||
})
|
||||
</script>
|
||||
28
templates/partials/vote-results.html
Normal file
28
templates/partials/vote-results.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% if metrics.total_votes == 0 %}
|
||||
<p>No voting data available for this showreel yet.</p>
|
||||
{% else %}
|
||||
<div>
|
||||
<p>Total votes: <strong>{{ metrics.total_votes }}</strong></p>
|
||||
<p>Positive votes: <strong>{{ metrics.positive_votes }}</strong></p>
|
||||
<p>Negative votes: <strong>{{ metrics.total_votes - metrics.positive_votes }}</strong></p>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
{% endif %}
|
||||
<div>
|
||||
<div class="entries">
|
||||
{% for entry in metrics.results %}
|
||||
<div class="entry panel padded">
|
||||
<h2>
|
||||
<a href="{{ url_for('votes.video_view', video_id=entry.id) }}">
|
||||
{{ entry.game }}
|
||||
</a>
|
||||
</h2>
|
||||
<p>Category: <strong>{{ showreel.title }}</strong></p>
|
||||
<p>Author: <strong>{{ entry.author_name }}</strong></p>
|
||||
<p>Vote sum: <strong>{{ entry.vote_sum }}</strong></p>
|
||||
<p>Votes received: <strong>{{ entry.vote_count }}</strong></p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
103
templates/submit.html
Normal file
103
templates/submit.html
Normal file
@@ -0,0 +1,103 @@
|
||||
<main id="submission-form">
|
||||
<div>
|
||||
<h1>Showreel Submission</h1>
|
||||
<p>Fill in the form to submit your video for the Godot Showreel.</p>
|
||||
</div>
|
||||
<hr>
|
||||
<form method="POST" enctype="multipart/form-data" hx-post="{{ url_for("votes.post_submit", _method='POST') }}" hx-target='#submission-form' hx-swap='outerHTML'>
|
||||
{{ form.csrf_token }}
|
||||
|
||||
{{ form.game.label }}<br>
|
||||
{{ form.game(size=100) }}<br>
|
||||
{% for error in form.game.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}<br>
|
||||
|
||||
{{ form.author_name.label }}<br>
|
||||
{{ form.author_name(size=100) }}<br>
|
||||
{% for error in form.author_name.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}<br>
|
||||
|
||||
{{ form.contact_email.label }}<br>
|
||||
{{ form.contact_email(size=100) }}<br>
|
||||
{% for error in form.contact_email.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}<br>
|
||||
|
||||
{{ form.video_link.label }}<br>
|
||||
{{ form.video_link(size=100) }}
|
||||
<button type="button" class="button secondary" id="preview-btn">Preview</button><br>
|
||||
{% for error in form.video_link.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
|
||||
<div id="preview-container" style="display: none;">
|
||||
<iframe width="560" height="315" src="" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
{{ form.video_download_link.label }}<br>
|
||||
{{ form.video_download_link(size=100) }}<br>
|
||||
{% for error in form.video_download_link.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}<br>
|
||||
|
||||
{{ form.follow_me_link.label }}<br>
|
||||
{{ form.follow_me_link(size=100) }}<br>
|
||||
{% for error in form.follow_me_link.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}<br>
|
||||
|
||||
{{ form.store_link.label }}
|
||||
<p class="info">We will use this to promote your game in future events and showcase pages.</p>
|
||||
{{ form.store_link(size=100) }}<br>
|
||||
{% for error in form.store_link.errors %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endfor %}<br>
|
||||
|
||||
<button type="submit" class="button secondary">Submit</button>
|
||||
</form>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Render video preview
|
||||
function parseVideoID(videoLink) {
|
||||
const videoUrl = new URL(videoLink);
|
||||
console.log(videoUrl.hostname);
|
||||
const hostname = videoUrl.hostname
|
||||
if (hostname == 'youtu.be') {
|
||||
return videoUrl.pathname.slice(1);
|
||||
}
|
||||
else if (hostname === 'www.youtube.com' || hostname === 'youtube.com' || hostname === 'm.youtube.com') {
|
||||
if (videoUrl.pathname == '/watch') {
|
||||
return videoUrl.searchParams.get('v');
|
||||
}
|
||||
else if (videoUrl.pathname.startsWith('/embed/')) {
|
||||
return videoUrl.pathname.split('/')[2];
|
||||
}
|
||||
else if (videoUrl.pathname.startsWith('/v/')) {
|
||||
return videoUrl.pathname.split('/')[2];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('preview-btn').addEventListener('click', function() {
|
||||
const originalForm = document.querySelector("#submission-form form");
|
||||
const previewContainer = document.getElementById('preview-container');
|
||||
let videoID = parseVideoID(originalForm.querySelector("input[name='video_link']").value);
|
||||
if (!videoID) {
|
||||
previewContainer.innerHTML = '<p class="error">Invalid video link provided.</p>';
|
||||
previewContainer.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
videoID = 'https://www.youtube.com/embed/' + videoID + '?rel=0&autoplay=1';
|
||||
|
||||
previewContainer.querySelector('iframe').src = videoID;
|
||||
previewContainer.style.display = 'block';
|
||||
});
|
||||
})
|
||||
</script>
|
||||
89
templates/user-submissions.html
Normal file
89
templates/user-submissions.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<style>
|
||||
.entry {
|
||||
.info {
|
||||
display: grid;
|
||||
margin-bottom: 5px;
|
||||
gap: 10px;
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: 120px 1fr;
|
||||
|
||||
}
|
||||
.category {
|
||||
font-size: 12px;
|
||||
margin: 4px 0px;
|
||||
}
|
||||
}
|
||||
.image-container {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
img {
|
||||
max-width: 100%;
|
||||
margin-bottom: -15px;
|
||||
margin-top: -11px;
|
||||
width: auto;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<main id="user-submissions">
|
||||
<h1>User Submissions</h1>
|
||||
<hr>
|
||||
{% if open_submissions %}
|
||||
<h2>Open Submissions</h2>
|
||||
<div>
|
||||
{% for entry in open_submissions %}
|
||||
<div class="entry panel padded">
|
||||
<div class="info">
|
||||
<div>
|
||||
<div class="image-container">
|
||||
<img src="https://img.youtube.com/vi/{{ entry.parse_youtube_video_id() }}/default.jpg" alt="Video thumbnail" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ entry.game }}</h2>
|
||||
<p><strong>{{ entry.author_name }}</strong></p>
|
||||
<p class="category">{{ entry.showreel.title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{url_for('votes.manage_submission', video_id=entry.id) }}" style="text-decoration: none;">
|
||||
<form action="{{ url_for('votes.manage_submission', video_id=entry.id) }}" method="get" style="display:inline-block; margin-top: 10px;">
|
||||
<button type="submit" class="button secondary">Edit entry</button>
|
||||
</form>
|
||||
</a>
|
||||
<form action="{{ url_for('votes.delete_submission', video_id=entry.id) }}" method="post" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this entry?');">
|
||||
<button type="submit" class="button tertiary">Delete entry</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<hr>
|
||||
{% endif %}
|
||||
{% if closed_submissions %}
|
||||
<h2>Closed Submissions</h2>
|
||||
<div>
|
||||
{% for entry in closed_submissions %}
|
||||
<div class="entry panel padded">
|
||||
<div class="info">
|
||||
<div>
|
||||
<div class="image-container">
|
||||
<img src="https://img.youtube.com/vi/{{ entry.parse_youtube_video_id() }}/default.jpg" alt="Video thumbnail" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ entry.game }}</h2>
|
||||
<p><strong>{{ entry.author_name }}</strong></p>
|
||||
<p class="category">{{ entry.showreel.title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not open_submissions and not closed_submissions %}
|
||||
<p>You haven't submitted any entries yet.</p>
|
||||
{% endif %}
|
||||
</main>
|
||||
Reference in New Issue
Block a user