diff --git a/quotefault/__init__.py b/quotefault/__init__.py index cca2d84..feafb9a 100644 --- a/quotefault/__init__.py +++ b/quotefault/__init__.py @@ -8,6 +8,7 @@ from flask import Flask, render_template, request, flash, session, make_response from flask_pyoidc.flask_pyoidc import OIDCAuthentication from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import func app = Flask(__name__) # look for a config file to associate with a db/port/ip/servername @@ -52,6 +53,24 @@ def __init__(self, submitter, quote, speaker): self.speaker = speaker +class Vote(db.Model): + id = db.Column(db.Integer, primary_key=True, nullable=False) + quote_id = db.Column(db.ForeignKey("quote.id")) + voter = db.Column(db.String(200), nullable=False) + direction = db.Column(db.Integer, nullable=False) + updated_time = db.Column(db.DateTime, nullable=False) + + quote = db.relationship(Quote) + test = db.UniqueConstraint("quote_id", "voter") + + # initialize a row for the Vote table + def __init__(self, quote_id, voter, direction): + self.quote_id = quote_id + self.voter = voter + self.direction = direction + self.updated_time = datetime.now() + + def get_metadata(): uuid = str(session["userinfo"].get("sub", "")) uid = str(session["userinfo"].get("preferred_username", "")) @@ -89,6 +108,30 @@ def settings(): return render_template('bootstrap/settings.html', metadata=metadata) +@app.route('/vote', methods=['POST']) +@auth.oidc_auth +def make_vote(): + # submitter will grab UN from OIDC when linked to it + submitter = session['userinfo'].get('preferred_username', '') + + quote = request.form['quote_id'] + direction = request.form['direction'] + + existing_vote = Vote.query.filter_by(voter=submitter, quote_id=quote).first() + if existing_vote is None: + vote = Vote(quote, submitter, direction) + db.session.add(vote) + db.session.commit() + return '200' + elif existing_vote.direction != direction: + existing_vote.direction = direction + existing_vote.updated_time = datetime.now() + db.session.commit() + return '200' + else: + return '201' + + @app.route('/settings', methods=['POST']) @auth.oidc_auth def update_settings(): @@ -164,7 +207,7 @@ def submit(): def get(): metadata = get_metadata() metadata['submitter'] = request.args.get('submitter') # get submitter from url query string - metadata['speaker'] = request.args.get('speaker') # get submitter from url query string + metadata['speaker'] = request.args.get('speaker') # get speaker from url query string if metadata['speaker'] is not None and metadata['submitter'] is not None: quotes = Quote.query.order_by(Quote.quote_time.desc()).filter(Quote.submitter == metadata['submitter'], @@ -174,19 +217,27 @@ def get(): elif metadata['speaker'] is not None: quotes = Quote.query.order_by(Quote.quote_time.desc()).filter(Quote.speaker == metadata['speaker']).all() else: - quotes = Quote.query.order_by(Quote.quote_time.desc()).limit(20).all() # collect all quote rows in the Quote db + # quotes = Quote.query.order_by(Quote.quote_time.desc()).limit(20).all() + + # returns tuples with a quote and its net vote value + quotes = db.session.query(Quote, func.sum(Vote.direction).label('votes')).outerjoin(Vote).group_by(Quote).order_by( + Quote.quote_time.desc()).limit(20).all() + + user_votes = db.session.query(Vote).filter(Vote.voter == metadata['submitter']).all() if request.cookies.get('flag'): return render_template( 'flag/storage.html', quotes=quotes, - metadata=metadata + metadata=metadata, + user_votes=user_votes ) else: return render_template( 'bootstrap/storage.html', quotes=quotes, - metadata=metadata + metadata=metadata, + user_votes=user_votes ) diff --git a/quotefault/static/css/quotefaultcss.css b/quotefault/static/css/quotefaultcss.css index f57df2b..cd8a3ed 100644 --- a/quotefault/static/css/quotefaultcss.css +++ b/quotefault/static/css/quotefaultcss.css @@ -36,3 +36,5 @@ body{ .contentContainer a:hover{ color: #d1d6d6; } + + diff --git a/quotefault/static/css/styles.css b/quotefault/static/css/styles.css index a636bb1..22868b3 100644 --- a/quotefault/static/css/styles.css +++ b/quotefault/static/css/styles.css @@ -313,3 +313,9 @@ div.version{ #get_more { width: 100%; } + +.upvote-meta-stackoverflow{ + text-align: right; + float: right; + justify-content: right; +} \ No newline at end of file diff --git a/quotefault/static/css/votes/upvote.css b/quotefault/static/css/votes/upvote.css new file mode 100644 index 0000000..38e37b7 --- /dev/null +++ b/quotefault/static/css/votes/upvote.css @@ -0,0 +1,150 @@ +div.upvote { + text-align: center; +} + +div.upvote a.upvote-enabled { + cursor: pointer; +} + +div.upvote a { + color: transparent; + background-image: url('images/sprites-stackoverflow.png?v=1'); + background-repeat: no-repeat; + overflow: hidden; + display: block; + margin: 0 auto; + width: 41px; + height: 25px; +} + +div.upvote span.count { + display: block; + font-size: 24px; + font-family: Arial, Liberation, Sans, DejaVu Sans, sans-serif; + color: #555; + text-align: center; + line-height: 1; +} + +div.upvote a.upvote { + background-position: 0px -265px; +} + +div.upvote a.upvote.upvote-on { + background-position: 0px -230px; +} + +div.upvote a.downvote { + background-position: 0px -300px; +} + +div.upvote a.downvote.downvote-on { + background-position: 0px -330px; +} + +div.upvote a.star { + width: 33px; + height: 31px; + background-position: 0px -150px; +} + +div.upvote a.star.star-on { + background-position: 0px -190px; +} + +div.upvote-serverfault a { + background-image: url('images/sprites-serverfault.png?v=1'); +} + +div.upvote-meta-stackoverflow a { + background-image: url('images/sprites-meta-stackoverflow.png?v=1'); +} + +div.upvote-superuser a { + background-image: url('images/sprites-superuser.png?v=1'); +} + +div.upvote-unix a { + background-image: url('images/sprites-unix.png?v=1'); + width: 42px; + height: 27px; +} + +div.upvote-unix a.upvote { + background-position: 0px -237px; +} + +div.upvote-unix a.upvote.upvote-on { + background-position: 0px -198px; +} + +div.upvote-unix a.downvote { + background-position: 0px -281px; +} + +div.upvote-unix a.downvote.downvote-on { + background-position: 0px -319px; +} + +div.upvote-unix a.star { + width: 42px; + height: 30px; + background-position: 0px -126px; +} + +div.upvote-unix a.star.star-on { + background-position: 0px -164px; +} + +div.upvote-unix span.count { + color: #333; + line-height: 15px; + padding: 9px 0; + font-family: "DejaVu Sans Mono","Bitstream Vera Sans Mono","Courier New",Courier,Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter",monospace; + text-shadow: 1px 1px 0 #ffffff; + font-weight: bold; + margin: 0; + border: 0; + vertical-align: baseline; +} + +div.upvote-programmers a { + background-image: url('images/sprites-programmers.png?v=1'); + width: 42px; + height: 20px; +} + +div.upvote-programmers a.upvote { + background-position: 5px -248px; +} + +div.upvote-programmers a.upvote.upvote-on { + background-position: 5px -211px; +} + +div.upvote-programmers a.downvote { + background-position: 5px -282px; +} + +div.upvote-programmers a.downvote.downvote-on { + background-position: 5px -320px; +} + +div.upvote-programmers a.star { + width: 42px; + height: 30px; + background-position: 6px -128px; +} + +div.upvote-programmers a.star.star-on { + background-position: 6px -166px; +} + +div.upvote-programmers span.count { + color: #333; + line-height: 15px; + padding: 5px 0 7px; + font-size: 20px; + font-weight: bold; + font-family: Tahoma,Geneva,Arial,sans-serif; +} \ No newline at end of file diff --git a/quotefault/static/css/votes/upvote.js b/quotefault/static/css/votes/upvote.js new file mode 100644 index 0000000..557632b --- /dev/null +++ b/quotefault/static/css/votes/upvote.js @@ -0,0 +1,227 @@ +/*! + * jQuery Upvote - a voting plugin + * ------------------------------------------------------------------ + * + * jQuery Upvote is a plugin that generates a voting widget like + * the one used on Stack Exchange sites. + * + * Licensed under Creative Commons Attribution 3.0 Unported + * http://creativecommons.org/licenses/by/3.0/ + * + * @version 1.0.2 + * @since 2013.06.19 + * @author Janos Gyerik + * @homepage https://janosgyerik.github.io/jquery-upvote + * @twitter twitter.com/janosgyerik + * + * ------------------------------------------------------------------ + * + *
+ * + * + * + * + *
+ * + * $('#topic').upvote(); + * $('#topic').upvote({count: 5, upvoted: true}); + * + */ + +;(function($) { + "use strict"; + var namespace = 'upvote'; + var dot_namespace = '.' + namespace; + var upvote_css = 'upvote'; + var dot_upvote_css = '.' + upvote_css; + var upvoted_css = 'upvote-on'; + var dot_upvoted_css = '.' + upvoted_css; + var downvote_css = 'downvote'; + var dot_downvote_css = '.' + downvote_css; + var downvoted_css = 'downvote-on'; + var dot_downvoted_css = '.' + downvoted_css; + var star_css = 'star'; + var dot_star_css = '.' + star_css; + var starred_css = 'star-on'; + var dot_starred_css = '.' + starred_css; + var count_css = 'count'; + var dot_count_css = '.' + count_css; + var enabled_css = 'upvote-enabled'; + + function init(dom, options) { + return dom.each(function() { + var jqdom = $(this); + methods.destroy(jqdom); + + var count = parseInt(jqdom.find(dot_count_css).text(), 10); + count = isNaN(count) ? 0 : count; + var initial = { + id: jqdom.attr('data-id'), + count: count, + upvoted: jqdom.find(dot_upvoted_css).length, + downvoted: jqdom.find(dot_downvoted_css).length, + starred: jqdom.find(dot_starred_css).length, + callback: function() {} + }; + + var data = $.extend(initial, options); + if (data.upvoted && data.downvoted) { + data.downvoted = false; + } + + jqdom.data(namespace, data); + render(jqdom); + setupUI(jqdom); + }); + } + + function setupUI(jqdom) { + jqdom.find(dot_upvote_css).addClass(enabled_css); + jqdom.find(dot_downvote_css).addClass(enabled_css); + jqdom.find(dot_star_css).addClass(enabled_css); + jqdom.find(dot_upvote_css).on('click.' + namespace, function() { + jqdom.upvote('upvote'); + }); + jqdom.find('.downvote').on('click.' + namespace, function() { + jqdom.upvote('downvote'); + }); + jqdom.find('.star').on('click.' + namespace, function() { + jqdom.upvote('star'); + }); + } + + function _click_upvote(jqdom) { + jqdom.find(dot_upvote_css).click(); + } + + function _click_downvote(jqdom) { + jqdom.find(dot_downvote_css).click(); + } + + function _click_star(jqdom) { + jqdom.find(dot_star_css).click(); + } + + function render(jqdom) { + var data = jqdom.data(namespace); + jqdom.find(dot_count_css).text(data.count); + if (data.upvoted) { + jqdom.find(dot_upvote_css).addClass(upvoted_css); + jqdom.find(dot_downvote_css).removeClass(downvoted_css); + } else if (data.downvoted) { + jqdom.find(dot_upvote_css).removeClass(upvoted_css); + jqdom.find(dot_downvote_css).addClass(downvoted_css); + } else { + jqdom.find(dot_upvote_css).removeClass(upvoted_css); + jqdom.find(dot_downvote_css).removeClass(downvoted_css); + } + if (data.starred) { + jqdom.find(dot_star_css).addClass(starred_css); + } else { + jqdom.find(dot_star_css).removeClass(starred_css); + } + } + + function callback(jqdom) { + var data = jqdom.data(namespace); + data.callback(data); + } + + function upvote(jqdom) { + var data = jqdom.data(namespace); + if (data.upvoted) { + data.upvoted = false; + --data.count; + } else { + data.upvoted = true; + ++data.count; + if (data.downvoted) { + data.downvoted = false; + ++data.count; + } + } + render(jqdom); + callback(jqdom); + return jqdom; + } + + function downvote(jqdom) { + var data = jqdom.data(namespace); + if (data.downvoted) { + data.downvoted = false; + ++data.count; + } else { + data.downvoted = true; + --data.count; + if (data.upvoted) { + data.upvoted = false; + --data.count; + } + } + render(jqdom); + callback(jqdom); + return jqdom; + } + + function star(jqdom) { + var data = jqdom.data(namespace); + data.starred = ! data.starred; + render(jqdom); + callback(jqdom); + return jqdom; + } + + function count(jqdom) { + return jqdom.data(namespace).count; + } + + function upvoted(jqdom) { + return jqdom.data(namespace).upvoted; + } + + function downvoted(jqdom) { + return jqdom.data(namespace).downvoted; + } + + function starred(jqdom) { + return jqdom.data(namespace).starred; + } + + var methods = { + init: init, + count: count, + upvote: upvote, + upvoted: upvoted, + downvote: downvote, + downvoted: downvoted, + starred: starred, + star: star, + _click_upvote: _click_upvote, + _click_downvote: _click_downvote, + _click_star: _click_star, + destroy: destroy + }; + + function destroy(jqdom) { + return jqdom.each(function() { + $(window).unbind(dot_namespace); + $(this).removeClass(enabled_css); + $(this).removeData(namespace); + }); + } + + $.fn.upvote = function(method) { + var args; + if (methods[method]) { + args = Array.prototype.slice.call(arguments, 1); + args.unshift(this); + return methods[method].apply(this, args); + } + if (typeof method === 'object' || ! method) { + args = Array.prototype.slice.call(arguments); + args.unshift(this); + return methods.init.apply(this, args); + } + $.error('Method ' + method + ' does not exist on jQuery.upvote'); + }; +})(jQuery); \ No newline at end of file diff --git a/quotefault/static/js/vote.js b/quotefault/static/js/vote.js new file mode 100644 index 0000000..4224e62 --- /dev/null +++ b/quotefault/static/js/vote.js @@ -0,0 +1,20 @@ + +function makeVote(quote_id, direction){ + $.ajax({ + url: "/vote", + data: { + "quote_id" : quote_id, + "direction": direction + }, + method: "POST" + }); + $("#votes-" + quote_id).upvote(); + + if(direction === 1){ + $("#votes-" + quote_id).upvote('upvote'); + } else { + $("#votes-" + quote_id).upvote('downvote'); + } +} + + diff --git a/quotefault/templates/bootstrap/base.html b/quotefault/templates/bootstrap/base.html index e73ad1a..5c3d877 100644 --- a/quotefault/templates/bootstrap/base.html +++ b/quotefault/templates/bootstrap/base.html @@ -17,6 +17,9 @@ + {% block styles %} + {% endblock %} + + + {% endblock %} diff --git a/requirements.txt b/requirements.txt index 77d0a48..c938ec6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ click csh_ldap>=2.0.2 Flask Flask-Migrate -Flask-pyoidc +Flask-pyoidc==1.3.0 Flask-Script Flask-SQLAlchemy gunicorn