mirror of
https://github.com/godotengine/godot-showreel-voting.git
synced 2026-09-03 18:15:13 +03:00
fixed errors when handling multiple showreels
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
@@ -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;">
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user