fixed errors when handling multiple showreels

This commit is contained in:
InigoAllende
2026-08-04 12:44:57 +02:00
parent 0c66e71ec4
commit 476e190c92
6 changed files with 211 additions and 104 deletions

View File

@@ -3,7 +3,7 @@ from flask_wtf import FlaskForm
from wtforms import IntegerField, SelectField, StringField, ValidationError, EmailField
from wtforms.validators import InputRequired
from gdshowreelvote.database import ShowreelStatus
from gdshowreelvote.database import DB, Showreel, ShowreelStatus
from gdshowreelvote.utils import downvote_video, skip_video, upvote_video
@@ -53,3 +53,19 @@ class ManageShowreelsForm(FlaskForm):
(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

View File

@@ -1,24 +1,45 @@
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 sqlalchemy.exc import IntegrityError
from sqlalchemy import or_
from gdshowreelvote import auth
from gdshowreelvote.blueprints.forms import VOTE_ACTIONS, CastVoteForm, ManageShowreelsForm, SelectVideoForm, VideoSubmissionForm
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, get_total_votes, video_data, vote_data, voting_possible
from gdshowreelvote.utils import (
choose_random_video,
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)
@@ -101,13 +122,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()
@@ -133,12 +154,18 @@ def history():
@auth.admin_required
def admin_view():
form = ManageShowreelsForm()
showreels = DB.session.query(Showreel).all()
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]
total_votes, positive_votes, vote_tally = get_total_votes()
content = render_template('admin.html', form=form, vote_tally=vote_tally, total_votes=total_votes, positive_votes=positive_votes, showreels=showreels)
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)
@@ -147,6 +174,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,
@@ -157,6 +188,7 @@ 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()
)
@@ -180,7 +212,7 @@ def download_vote_results():
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
@@ -199,7 +231,7 @@ def video_view(video_id: int):
@bp.route('/submit', methods=['GET'])
def submit():
active_showreel = DB.session.query(Showreel).filter(Showreel.status == ShowreelStatus.OPENED_TO_SUBMISSIONS).count()
if not active_showreel == 1:
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)
@@ -222,7 +254,7 @@ def post_submit():
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.errors.setdefault('video_link', []).append('A video with the same link or download link has already been submitted.')
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]
@@ -244,7 +276,7 @@ def post_submit():
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, submission_success=True) # TODO: Add flag to show submission success message
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'])
@@ -336,3 +368,17 @@ def update_submission(video_id: int):
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)

View File

@@ -1,4 +1,4 @@
from typing import Dict, List, Tuple
from typing import Any, Dict, List, Tuple
from sqlalchemy import and_, func
from gdshowreelvote.database import DB, Showreel, ShowreelStatus, User, Video, Vote
@@ -85,8 +85,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
@@ -98,9 +98,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,
@@ -108,12 +110,25 @@ 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:

View File

@@ -32,28 +32,13 @@
<button type="submit" class="button primary small">Save</button>
</form>
<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;">
<input type="hidden" id="download-showreel-id" name="showreel_id" value="">
<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 %}
</div>
<div id="vote-results">
</div>
</main>
@@ -61,6 +46,8 @@
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];
@@ -69,10 +56,25 @@
if (!status) return;
statusSelect.value = status;
downloadShowreelId.value = showreelSelect.value;
}
showreelSelect.addEventListener('change', updateStatus);
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>
</script>

View File

@@ -20,64 +20,64 @@
}
</style>
<main>
{% if submission_success %}
<div class="panel padded">
<h2>Thank you for your submission!</h2>
<p>Your entry has been received, voting will start on <strong>?????</strong> 📅</p>
{% 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 %}
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 %}
<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;">
<!-- During the submission period -->
{% if user %}
{% if submission_success %}
<p>You can submit multiple entries until <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 %}
You need to be logged in to submit an entry:
<a href="/login" class="button dim small">Log In</a>
{% endif %}
</div>
<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 %}
<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>
<div class="panel padded" style="margin-top: 20px;">

View 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>