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 @@ +

Vote results

-
-

Total votes: {{ total_votes }}

-

Positive votes: {{ positive_votes }}

-

Negative votes: {{ total_votes - positive_votes }}

-
-
+
-
-
- {% for entry in vote_tally %} -
-

{{ entry[0].game }}

-

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 %} +
+

No showreels just yet!

{% 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 %} +
+

+ + {{ entry.game }} + +

+

Category: {{ showreel.title }}

+

Author: {{ entry.author_name }}

+

Vote sum: {{ entry.vote_sum }}

+

Votes received: {{ entry.vote_count }}

+
+ {% endfor %} +
+