mirror of
https://github.com/godotengine/godot-showreel-voting.git
synced 2026-01-05 22:09:47 +03:00
Update skip logic (#28)
* added link to video in admin view, truncated titles that are too long * added total vote counts to admin page, removed pagination on admin page * Track skipped videos in DB * suggest skipped videos if no others left
This commit is contained in:
@@ -3,13 +3,13 @@ from flask_wtf import FlaskForm
|
||||
from wtforms import IntegerField, StringField, ValidationError
|
||||
from wtforms.validators import InputRequired
|
||||
|
||||
from gdshowreelvote.utils import downvote_video, upvote_video
|
||||
from gdshowreelvote.utils import downvote_video, skip_video, upvote_video
|
||||
|
||||
|
||||
VOTE_ACTIONS = {
|
||||
'upvote': upvote_video,
|
||||
'downvote': downvote_video,
|
||||
'skip': lambda *args: None
|
||||
'skip': skip_video
|
||||
}
|
||||
|
||||
def validate_action(form, field):
|
||||
|
||||
@@ -8,7 +8,7 @@ from flask import Blueprint, Response, current_app, g, redirect, render_template
|
||||
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, vote_data
|
||||
from gdshowreelvote.utils import choose_random_video, get_total_votes, video_data, vote_data
|
||||
|
||||
|
||||
bp = Blueprint('votes', __name__)
|
||||
@@ -31,6 +31,7 @@ def before_you_vote():
|
||||
content = render_template('before-you-vote.html')
|
||||
return render_template('default.html', content = content, user=g.user)
|
||||
|
||||
|
||||
@bp.route('/vote', methods=['GET'])
|
||||
@bp.route('/vote/<int:video_id>', methods=['GET'])
|
||||
@auth.vote_role_required
|
||||
@@ -54,7 +55,6 @@ def vote_get(video_id=None):
|
||||
def vote():
|
||||
cast_vote_form = CastVoteForm()
|
||||
select_specific_video_form = SelectVideoForm()
|
||||
skip_videos = []
|
||||
if cast_vote_form.validate():
|
||||
action = cast_vote_form.action.data
|
||||
video = DB.session.query(Video).filter(Video.id == cast_vote_form.video_id.data).first()
|
||||
@@ -62,13 +62,11 @@ def vote():
|
||||
current_app.logger.warning(f"Video with ID {cast_vote_form.video_id.data} not found.")
|
||||
return "Video not found", 404
|
||||
VOTE_ACTIONS[action](g.user, video)
|
||||
if action == 'skip':
|
||||
skip_videos.append(video.id)
|
||||
else:
|
||||
current_app.logger.warning(f"Form validation failed: {cast_vote_form.errors} {select_specific_video_form.errors}")
|
||||
return "Invalid form submission", 400
|
||||
|
||||
video = choose_random_video(g.user, skip_videos)
|
||||
video = choose_random_video(g.user)
|
||||
data, progress = vote_data(g.user, video)
|
||||
|
||||
return render_template('vote.html', data=data, progress=progress, cast_vote_form=cast_vote_form, select_specific_video_form=select_specific_video_form)
|
||||
@@ -116,10 +114,9 @@ def history():
|
||||
@bp.route('/admin')
|
||||
@auth.admin_required
|
||||
def admin_view():
|
||||
page = int(request.args.get('page', 1))
|
||||
vote_tally = get_total_votes(page)
|
||||
total_votes, positive_votes, vote_tally = get_total_votes()
|
||||
|
||||
content = render_template('admin.html', vote_tally=vote_tally)
|
||||
content = render_template('admin.html', vote_tally=vote_tally, total_votes=total_votes, positive_votes=positive_votes)
|
||||
if request.args.get('page'):
|
||||
return content
|
||||
return render_template('default.html', content = content, user=g.user)
|
||||
@@ -162,4 +159,16 @@ def download_vote_results():
|
||||
])
|
||||
response = Response(csv_file.getvalue(), mimetype='text/csv')
|
||||
response.headers["Content-Disposition"] = "attachment; filename=vote_results.csv"
|
||||
return response
|
||||
return response
|
||||
|
||||
|
||||
@bp.route('/view/<int:video_id>', methods=['GET'])
|
||||
def video_view(video_id: int):
|
||||
video = DB.session.query(Video).filter(Video.id == video_id).first()
|
||||
if not video:
|
||||
current_app.logger.warning(f"Video with ID {video_id} not found.")
|
||||
return "Video not found", 404
|
||||
|
||||
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)
|
||||
|
||||
@@ -21,6 +21,19 @@ def choose_random_video(user: User, skip_videos: List[int]=[]) -> Video:
|
||||
.first()
|
||||
)
|
||||
|
||||
if random_video_without_votes is None:
|
||||
# If all videos have been voted on, return a random video that was skipped
|
||||
random_video_without_votes = (
|
||||
DB.session.query(Video)
|
||||
.join(Showreel, Video.showreel_id == Showreel.id) # join explicitly
|
||||
.outerjoin(Vote, and_(Video.id == Vote.video_id, Vote.user_id == user.id))
|
||||
.filter(Showreel.status == ShowreelStatus.VOTE) # filter on Showreel column
|
||||
.filter(Vote.rating == 0) # check votes with rating 0 (skipped)
|
||||
.filter(Video.id.notin_(skip_videos)) # exclude skipped videos
|
||||
.order_by(func.random())
|
||||
.first()
|
||||
)
|
||||
|
||||
return random_video_without_votes
|
||||
|
||||
|
||||
@@ -48,7 +61,18 @@ def downvote_video(user: User, video: Video):
|
||||
return vote
|
||||
|
||||
|
||||
def _video_data(video: Video) -> Dict:
|
||||
def skip_video(user: User, video: Video):
|
||||
vote = DB.session.query(Vote).filter(and_(Vote.user_id == user.id, Vote.video_id == video.id)).first()
|
||||
if vote:
|
||||
vote.rating = 0
|
||||
else:
|
||||
vote = Vote(user_id=user.id, video_id=video.id, rating=0)
|
||||
DB.session.add(vote)
|
||||
DB.session.commit()
|
||||
return vote
|
||||
|
||||
|
||||
def video_data(video: Video) -> Dict:
|
||||
data = {
|
||||
'id': video.id,
|
||||
'game': video.game,
|
||||
@@ -68,7 +92,7 @@ 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()
|
||||
|
||||
data = _video_data(video) if video else None
|
||||
data = video_data(video) if video else None
|
||||
|
||||
progress = {
|
||||
'total': total_video_count,
|
||||
@@ -78,8 +102,10 @@ def vote_data(user: User, video: Video) -> Tuple[Dict, Dict]:
|
||||
return data, progress
|
||||
|
||||
|
||||
def get_total_votes(page: int) -> List[Tuple[Video, int, int]]:
|
||||
query = (
|
||||
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()
|
||||
results = (
|
||||
DB.session.query(
|
||||
Video,
|
||||
func.coalesce(func.sum(Vote.rating), 0).label("vote_sum"),
|
||||
@@ -88,14 +114,10 @@ def get_total_votes(page: int) -> List[Tuple[Video, int, int]]:
|
||||
.outerjoin(Vote, Vote.video_id == Video.id)
|
||||
.group_by(Video.id)
|
||||
.order_by(func.coalesce(func.sum(Vote.rating), 0).desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
try:
|
||||
results = query.paginate(page=page, per_page=30)
|
||||
except NotFound:
|
||||
results = query.paginate(page=1, per_page=30)
|
||||
|
||||
return results
|
||||
return total_votes, positive_votes, results
|
||||
|
||||
|
||||
def parse_youtuvbe_video_id(yt_url: str) -> Optional[str]:
|
||||
|
||||
@@ -14,14 +14,20 @@
|
||||
</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.items %}
|
||||
{% for entry in vote_tally %}
|
||||
<div class="entry panel padded">
|
||||
<h2>{{ entry[0].game }}</h2>
|
||||
<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>
|
||||
@@ -29,8 +35,5 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% with elements=vote_tally, request_url="votes.admin_view", hx_target="#admin", strategy="outerHTML" %}
|
||||
{% include "pagination.html" with context %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</main>
|
||||
117
templates/video-view.html
Normal file
117
templates/video-view.html
Normal file
@@ -0,0 +1,117 @@
|
||||
<style>
|
||||
iframe {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
height: auto;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
div.main {
|
||||
max-width: 134vh;
|
||||
}
|
||||
.col-2 {
|
||||
text-align: left;
|
||||
display: grid;
|
||||
margin: 20px 0px;
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
.thank-you {
|
||||
font-size: 1.3em;
|
||||
padding: 41px 50px;
|
||||
text-align: left;
|
||||
margin: 0 auto;
|
||||
h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.progress {
|
||||
@media (min-width: 768px) {
|
||||
text-align: right;
|
||||
}
|
||||
a {
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
font-size: 18px;
|
||||
color: inherit;
|
||||
opacity: 0.8;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
progress {
|
||||
max-width: 190px;
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
position: relative;
|
||||
top: -8px;
|
||||
}
|
||||
}
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
div.main.title {
|
||||
margin-top: -20px;
|
||||
margin-bottom: -15px;
|
||||
padding-top: 10px;
|
||||
max-width: 100%;
|
||||
h1 {
|
||||
max-width: 40ch;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 auto;
|
||||
@media (min-width: 768px) {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
.col-2 {
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: 200px 1fr 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.truncate {
|
||||
max-width: 40ch;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
<div id="vote-view">
|
||||
<div style="background: black; color: white; margin-bottom: -20px; padding-bottom: 24px;">
|
||||
<div class="main title">
|
||||
<div class="col-2" style="margin-bottom: 0;">
|
||||
<div>
|
||||
<a href="/">
|
||||
<img class="logo" src="/static/images/nav-logo.svg" alt="Godot Showreel Voting">
|
||||
</a>
|
||||
</div>
|
||||
<h1 title="{{ data.game }}">{{ data.game }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style="background: black; margin: 20px 0;">
|
||||
<div class="main">
|
||||
<iframe width="560" height="315" src="{{ data.youtube_embed }}?rel=0&autoplay=1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="panel padded" style="margin-top: 10px;">
|
||||
<h2>Submission info:</h2>
|
||||
<div class="col-2">
|
||||
<p class="truncate" title="{{ data.game }}">Project: <strong>{{ data.game }}</strong></p>
|
||||
<p>Video Link: <a href="{{ data.video_link }}" target="_blank">{{ data.video_link }}</a></p>
|
||||
<p>Author: <strong>{{ data.author }}</strong></p>
|
||||
<p>Store Link: <a href="{{ data.store_link }}" target="_blank">{{ data.store_link }}</a></p>
|
||||
<p>Category: <strong>{{ data.category }}</strong></p>
|
||||
<p>Follow them: <a href="{{ data.follow_me_link }}" target="_blank">{{ data.follow_me_link }}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,6 +56,11 @@
|
||||
padding-top: 10px;
|
||||
max-width: 100%;
|
||||
h1 {
|
||||
max-width: 40ch;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 auto;
|
||||
@media (min-width: 768px) {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
@@ -68,6 +73,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.truncate {
|
||||
max-width: 40ch;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
<div id="vote-view">
|
||||
{% if not data %}
|
||||
@@ -88,7 +99,7 @@
|
||||
<img class="logo" src="/static/images/nav-logo.svg" alt="Godot Showreel Voting">
|
||||
</a>
|
||||
</div>
|
||||
<h1>{{ data.game }}</h1>
|
||||
<h1 title="{{ data.game }}">{{ data.game }}</h1>
|
||||
<div style="display: grid; align-content: end;">
|
||||
<div class="progress">
|
||||
<a href="/history">
|
||||
@@ -115,7 +126,7 @@
|
||||
{{ cast_vote_form.csrf_token }}
|
||||
{{ cast_vote_form.video_id(value=data.id, hidden=True) }}
|
||||
<button id="downvote" type="submit" name="action" value="downvote" class="button tertiary" title="Shortcut: 'A' or 'Q' keys">No 👎</button>
|
||||
<button id="skip" type="submit" name="action" value="skip" class="button dim" title="Shortcut: 'S' key">Skip ⏭️</button>
|
||||
<button id="skip" type="submit" name="action" value="skip" class="button dim" title="Shortcut: 'S' key. You need to wait 3 seconds before skipping." disabled>Skip ⏭️</button>
|
||||
<button id="upvote" type="submit" name="action" value="upvote" class="button primary" title="Shortcut: 'D' key">Yes 👍</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -124,7 +135,7 @@
|
||||
<div class="panel padded" style="margin-top: 10px;">
|
||||
<h2>Submission info:</h2>
|
||||
<div class="col-2">
|
||||
<p>Project: <strong>{{ data.game }}</strong></p>
|
||||
<p class="truncate" title="{{ data.game }}">Project: <strong>{{ data.game }}</strong></p>
|
||||
<p>Video Link: <a href="{{ data.video_link }}" target="_blank">{{ data.video_link }}</a></p>
|
||||
<p>Author: <strong>{{ data.author }}</strong></p>
|
||||
<p>Store Link: <a href="{{ data.store_link }}" target="_blank">{{ data.store_link }}</a></p>
|
||||
@@ -151,4 +162,11 @@
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// enable skip button after 5 seconds
|
||||
setTimeout(function() {
|
||||
document.getElementById("skip").disabled = false;
|
||||
}, 3000);
|
||||
</script>
|
||||
Reference in New Issue
Block a user