diff --git a/gdshowreelvote/blueprints/forms.py b/gdshowreelvote/blueprints/forms.py
index 08d2a51..b17d4cf 100644
--- a/gdshowreelvote/blueprints/forms.py
+++ b/gdshowreelvote/blueprints/forms.py
@@ -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
diff --git a/gdshowreelvote/blueprints/votes.py b/gdshowreelvote/blueprints/votes.py
index 63f1ee6..4f2bb01 100644
--- a/gdshowreelvote/blueprints/votes.py
+++ b/gdshowreelvote/blueprints/votes.py
@@ -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)
diff --git a/gdshowreelvote/utils.py b/gdshowreelvote/utils.py
index 0c3a0c4..46d456d 100644
--- a/gdshowreelvote/utils.py
+++ b/gdshowreelvote/utils.py
@@ -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:
diff --git a/templates/admin.html b/templates/admin.html
index d71b971..7c9de4e 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -32,28 +32,13 @@
Save
+
Vote results
-
-
Total votes: {{ total_votes }}
-
Positive votes: {{ positive_votes }}
-
Negative votes: {{ total_votes - positive_votes }}
-
-
-
-
- {% for entry in vote_tally %}
-
-
-
Category: {{ entry[0].showreel.title }}
-
Author: {{ entry[0].author_name }}
-
Vote sum: {{ entry[1] }}
-
Votes received: {{ entry[2] }}
-
- {% endfor %}
-
+
@@ -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();
});
-
+
\ No newline at end of file
diff --git a/templates/home.html b/templates/home.html
index b4791dd..9e75327 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -20,64 +20,64 @@
}
- {% if submission_success %}
-
-
Thank you for your submission!
-
Your entry has been received, voting will start on ????? 📅
+ {% if active_submissions %}
+
+
+ {% if user %}
+ {% if submission_success %}
+
Thank you for your submission!
+
Your entry has been received, voting will start on ????? 📅
+ {% else %}
+
Submit your game for the Godot showreel until ????? 📅
+ {% endif %}
+
Submit entry
+ {% else %}
+ You need to be logged in to submit an entry:
+
Log In
+ {% endif %}
+
+ {% elif active_vote %}
+
+
+ {% if user %}
+ {% if user.can_vote() %}
+
Cast your votes until October 20th 📅
+
+ Videos are presented in random order, and scores are hidden until the voting period ends
+ Your votes are anonymous
+ You can change your votes until the voting period ends
+ We encourage you to vote for as many entries as you can
+ If you found any issue or if you have a suggestion, please create an issue in our issue tracker
+
+
+
Happy voting!
+
Start Voting History
+ {% else %}
+
Only Godot Engine maintainers or members of the Development Fund can vote.
+
If you would like to participate in the process, make sure to sign up to the Development Fund .
+
+ Sign Up
+
+
+ Already a fund member?
+ If you became a member recently, try to logout and then log in again. Also make sure that you use the same account as you used on the fund page!
+
+ {% endif %}
+ {% else %}
+
+ You need to be logged in to vote:
+
Log In
+ {% endif %}
+
+
Welcome!
+
The voting period for this years showreel videos has finished.
+
Thank you to everyone who participated!
+
+ {% else %}
+
{% endif %}
-
-
- {% if user %}
- {% if submission_success %}
-
You can submit multiple entries until ????? 📅
- {% else %}
-
Submit your game for the Godot showreel until ????? 📅
- {% endif %}
-
Submit entry
- {% else %}
- You need to be logged in to submit an entry:
-
Log In
- {% endif %}
-
-
-
-
-
- {% if user %}
- {% if user.can_vote() %}
-
Cast your votes until October 20th 📅
-
- Videos are presented in random order, and scores are hidden until the voting period ends
- Your votes are anonymous
- You can change your votes until the voting period ends
- We encourage you to vote for as many entries as you can
- If you found any issue or if you have a suggestion, please create an issue in our issue tracker
-
-
-
Happy voting!
-
Start Voting History
- {% else %}
-
Only Godot Engine maintainers or members of the Development Fund can vote.
-
If you would like to participate in the process, make sure to sign up to the Development Fund .
-
- Sign Up
-
-
- Already a fund member?
- If you became a member recently, try to logout and then log in again. Also make sure that you use the same account as you used on the fund page!
-
- {% endif %}
- {% else %}
-
- You need to be logged in to vote:
-
Log In
- {% endif %}
-
-
Welcome!
-
The voting period for this years showreel videos has finished.
-
Thank you to everyone who participated!
-
diff --git a/templates/partials/vote-results.html b/templates/partials/vote-results.html
new file mode 100644
index 0000000..122ead4
--- /dev/null
+++ b/templates/partials/vote-results.html
@@ -0,0 +1,28 @@
+{% if metrics.total_votes == 0 %}
+
No voting data available for this showreel yet.
+{% else %}
+
+
Total votes: {{ metrics.total_votes }}
+
Positive votes: {{ metrics.positive_votes }}
+
Negative votes: {{ metrics.total_votes - metrics.positive_votes }}
+
+
+
+{% endif %}
+
+
+ {% for entry in metrics.results %}
+
+
+
Category: {{ showreel.title }}
+
Author: {{ entry.author_name }}
+
Vote sum: {{ entry.vote_sum }}
+
Votes received: {{ entry.vote_count }}
+
+ {% endfor %}
+
+