Browse Source

voting

master
Julio Biason 11 years ago
parent
commit
4730af454b
  1. 22
      luncho/blueprints/groups.py
  2. 148
      luncho/blueprints/voting.py
  3. 34
      luncho/exceptions.py
  4. 46
      luncho/server.py
  5. 57
      tests/vote_tests.py

22
luncho/blueprints/groups.py

@ -17,31 +17,11 @@ from luncho.server import User
from luncho.server import Group from luncho.server import Group
from luncho.server import Place from luncho.server import Place
from luncho.exceptions import LunchoException
from luncho.exceptions import ElementNotFoundException from luncho.exceptions import ElementNotFoundException
from luncho.exceptions import AccountNotVerifiedException from luncho.exceptions import AccountNotVerifiedException
from luncho.exceptions import NewMaintainerDoesNotExistException from luncho.exceptions import NewMaintainerDoesNotExistException
from luncho.exceptions import UserIsNotAdminException from luncho.exceptions import UserIsNotAdminException
from luncho.exceptions import UserIsNotMemberException
# ----------------------------------------------------------------------
# Exceptions
# ----------------------------------------------------------------------
class UserIsNotMemberException(LunchoException):
"""The user is not the admin of the group.
.. sourcecode:: http
HTTP/1.1 403 Forbidden
Content-Type: test/json
{ "status": "ERROR", "message": "User is not member of this group" }
"""
def __init__(self):
super(UserIsNotMemberException, self).__init__()
self.status = 403
self.message = 'User is not member of this group'
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------

148
luncho/blueprints/voting.py

@ -0,0 +1,148 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""Voting"""
import datetime
import logging
from flask import Blueprint
from flask import jsonify
from flask import request
from flask import current_app
from luncho.helpers import ForceJSON
from luncho.helpers import auth
from luncho.server import db
from luncho.server import Group
from luncho.server import Vote
from luncho.server import CastedVote
from luncho.server import Place
from luncho.exceptions import LunchoException
from luncho.exceptions import UserIsNotMemberException
from luncho.exceptions import ElementNotFoundException
LOG = logging.getLogger('luncho.blueprints.voting')
voting = Blueprint('voting', __name__)
# ----------------------------------------------------------------------
# Exceptions
# ----------------------------------------------------------------------
class VoteAlreadyCastException(LunchoException):
"""The user already voted today.
.. sourcecode:: http
HTTP/1.1 406 Not Acceptable
Content-Type: text/json
{ "status": "ERROR", "message": "User already voted today" }
"""
def __init__(self):
super(VoteAlreadyCastException, self).__init__()
self.status = 406
self.message = 'User already voted today'
class InvalidNumberOfPlacesCastedException(LunchoException):
"""The number of places in the vote casted is invalid.
.. sourcecode:: http
HTTP/1.1 406 Not Acceptable
Content-Type: text/json
{ "status": "ERROR",
"message": "The vote must register {places} places" }
"""
def __init__(self, places):
super(InvalidNumberOfPlacesCastedException, self).__init__()
self.status = 406
self.message = 'The must register {places} places'.format(places)
class PlaceDoesntBelongToGroupException(LunchoException):
"""The indicated places do not belong to the group.
.. sourcecode:: http
http/1.1 404 Not Found
Content-Type: text/json
{ "status": "ERROR",
"message": "Places are not part of this group",
"places": [<place>, <place>, ...]}
"""
def __init__(self, places):
super(PlaceDoesntBelongToGroupException, self).__init__()
self.status = 404
self.message = 'Places are not part of this group'
self.places = places
def _json(self):
super(PlaceDoesntBelongToGroupException, self)._json()
self.json['places'] = self.places
# ----------------------------------------------------------------------
# Voting
# ----------------------------------------------------------------------
@voting.route('<int:group_id>/', methods=['POST'])
@ForceJSON(required=['choices'])
@auth
def cast_vote(group_id):
"""Cast a vote for a group. A user can cast a vote in a single group
per day.
"""
# check if the group exists
group = Group.query.get(group_id)
if not group:
raise ElementNotFoundException('Group')
# check if the user belongs to the group
if request.user not in group.users:
LOG.debug('User is not member')
raise UserIsNotMemberException()
# check if the user voted today already, for any group
today = datetime.date.today()
today_vote = Vote.query.filter_by(user=request.user.username,
created_at=today).first()
if today_vote:
LOG.debug('User already voted today')
raise VoteAlreadyCastException()
# check the number of votes the user casted
choices = request.as_json.get('choices')
max_places = min(current_app.config['PLACES_IN_VOTE'],
len(group.places))
if len(choices) != max_places:
LOG.debug('Max places = {max_places}, voted for {choices}',
max_places=max_places, choices=len(choices))
raise InvalidNumberOfPlacesCastedException()
# check if the places exist and are part of the group
# (don't vote yet, so we can stop the whole thing if there is anything
# wrong)
for place_id in request.as_json.get('choices'):
place = Place.query.get(place_id)
if not place:
raise ElementNotFoundException('Place')
if not place in group.places:
raise PlaceDoesntBelongToGroupException(place_id)
# finally, cast the vote
vote = Vote(request.user, group_id)
db.session.add(vote)
db.session.commit() # so vote gets an id
for (pos, place_id) in enumerate(request.as_json.get('choices')):
place = CastedVote(vote, pos, place_id)
db.session.add(place)
db.session.commit()
return jsonify(status='OK')

34
luncho/exceptions.py

@ -10,14 +10,22 @@ class LunchoException(Exception):
self.status = 500 self.status = 500
self.message = 'Unknown error' self.message = 'Unknown error'
self.extra_fields = None self.extra_fields = None
self.json = {}
def _json(self):
"""Fill the json property. If you need to change something, just
extend this function."""
code = self.__class__.__name__[:-9]
self.json = {'status': 'ERROR', # always an error
'message': self.message,
'code': code}
if self.extra_fields:
self.json.update(self.extra_fields)
def response(self): def response(self):
"""Return a JSON representation of the exception.""" """Return a JSON representation of the exception."""
json = {'status': 'ERROR', self._json() # encode the error
'message': self.message} response = jsonify(self.json)
if self.extra_fields:
json.update(self.extra_fields)
response = jsonify(json)
response.status_code = self.status response.status_code = self.status
return response return response
@ -170,3 +178,19 @@ class UserIsNotAdminException(LunchoException):
super(UserIsNotAdminException, self).__init__() super(UserIsNotAdminException, self).__init__()
self.status = 403 self.status = 403
self.message = 'User is not admin' self.message = 'User is not admin'
class UserIsNotMemberException(LunchoException):
"""The user is not the admin of the group.
.. sourcecode:: http
HTTP/1.1 403 Forbidden
Content-Type: test/json
{ "status": "ERROR", "message": "User is not member of this group" }
"""
def __init__(self):
super(UserIsNotMemberException, self).__init__()
self.status = 403
self.message = 'User is not member of this group'

46
luncho/server.py

@ -20,6 +20,7 @@ from luncho.exceptions import LunchoException
class Settings(object): class Settings(object):
SQLALCHEMY_DATABASE_URI = 'sqlite://./luncho.db3' SQLALCHEMY_DATABASE_URI = 'sqlite://./luncho.db3'
DEBUG = True DEBUG = True
PLACES_IN_VOTE = 3 # number of places the user can vote
log = logging.getLogger('luncho.server') log = logging.getLogger('luncho.server')
@ -125,6 +126,49 @@ class Place(db.Model):
self.name = name self.name = name
self.owner = owner.username self.owner = owner.username
def __repr__(self):
return 'Place {id}-{name}-{owner}'.format(id=self.id,
name=self.name,
owner=self.owner)
class Vote(db.Model):
cast = db.Column(db.Integer, primary_key=True)
user = db.Column(db.String, db.ForeignKey('user.username'))
created_at = db.Column(db.Date, nullable=False)
group = db.Column(db.Integer, db.ForeignKey('group.id'))
def __init__(self, user, group):
self.user = user.username
self.created_at = datetime.date.today()
self.group = group
return
def __repr__(self):
values = {'cast': self.cast,
'user': self.user,
'created_at': self.created_at,
'group': self.group}
return 'Vote {cast}-{user}-{created_at}-{group}'.format(**values)
class CastedVote(db.Model):
vote = db.Column(db.Integer, db.ForeignKey('vote.cast'), primary_key=True)
order = db.Column(db.Integer, nullable=False, primary_key=True)
place = db.Column(db.Integer, db.ForeignKey('place.id'))
def __init__(self, vote, order, place):
self.vote = vote.cast
self.order = order
self.place = place
return
def __repr__(self):
return 'Cast {vote}-{order}-{place}'.format(vote=self.vote,
order=self.order,
place=self.place)
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# Blueprints # Blueprints
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
@ -134,6 +178,7 @@ from blueprints.groups import groups
from blueprints.groups import group_users from blueprints.groups import group_users
from blueprints.groups import group_places from blueprints.groups import group_places
from blueprints.places import places from blueprints.places import places
from blueprints.voting import voting
app.register_blueprint(token, url_prefix='/token/') app.register_blueprint(token, url_prefix='/token/')
app.register_blueprint(users, url_prefix='/user/') app.register_blueprint(users, url_prefix='/user/')
@ -141,6 +186,7 @@ app.register_blueprint(groups, url_prefix='/group/')
app.register_blueprint(group_users, url_prefix='/group/') app.register_blueprint(group_users, url_prefix='/group/')
app.register_blueprint(group_places, url_prefix='/group/') app.register_blueprint(group_places, url_prefix='/group/')
app.register_blueprint(places, url_prefix='/place/') app.register_blueprint(places, url_prefix='/place/')
app.register_blueprint(voting, url_prefix='/vote/')
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------

57
tests/vote_tests.py

@ -0,0 +1,57 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
import unittest
from luncho import server
from base import LunchoTests
from luncho.server import Group
from luncho.server import Place
class TestVote(LunchoTests):
def setUp(self):
super(TestVote, self).setUp()
self.default_user()
def tearDown(self):
super(TestVote, self).tearDown()
def _group(self):
"""Add a default group."""
group = Group(name='Test group',
owner=self.user)
server.db.session.add(group)
self.user.groups.append(group)
server.db.session.commit()
return group
def _place(self, user=None):
"""Add a default place, linked to the user."""
if not user:
user = self.user
place = Place(name='Place',
owner=user)
server.db.session.add(place)
server.db.session.commit()
return place
def test_cast_vote(self):
"""Try to cast a vote."""
group = self._group()
place = self._place()
group.places.append(place)
self.user.groups.append(group)
server.db.session.commit()
request = {'choices': [place.id]}
rv = self.post('/vote/{group_id}/'.format(group_id=group.id),
request,
token=self.user.token)
print rv.data
self.assertJsonOk(rv)
if __name__ == '__main__':
unittest.main()
Loading…
Cancel
Save