diff --git a/README.md b/README.md index 9abc0d5..61b1008 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ Jay is a simple secret voting system for Jacobs University. Just in case you wer see [doc/](doc) for minimal developer documentation +## Setup + +1. Configure database and apply migrations +2. Create a Social Application pointing to `dreamjub` +3. update `DREAMJUB_CLIENT_ID` and `DREAMJUB_CLIENT_SECRET` variables + ## License Jay is © 2015-17 Leonhard Kuboschek, Tom Wiesing & Contributors. Licensed under MIT license. See [LICENSE.md](LICENSE.md) for details. diff --git a/core/templatetags/userflags.py b/core/templatetags/userflags.py index e69de29..f5cfbd3 100644 --- a/core/templatetags/userflags.py +++ b/core/templatetags/userflags.py @@ -0,0 +1,15 @@ +from django import template +from settings.models import VotingSystem +from jay import utils + +register = template.Library() + + +@register.filter() +def getAdminSystems(user): + return VotingSystem.getAdminSystems(user) + + +@register.filter() +def isSuperAdmin(user): + return utils.is_superadmin(user) diff --git a/core/views.py b/core/views.py index 1cfb20e..e9889f8 100644 --- a/core/views.py +++ b/core/views.py @@ -4,6 +4,7 @@ from settings.models import VotingSystem from votes.models import Vote, Status +from jay.utils import get_user_details # Create your views here. @@ -14,7 +15,8 @@ def home(request): systems = VotingSystem.objects.all() if request.user.is_authenticated(): - details = json.loads(request.user.profile.details) + details = get_user_details(request.user) + votes_shown = [v for v in votes if v.filter.matches(details)] ctx["vote_list_title"] = "Your votes" diff --git a/filters/forest.js b/filters/forest.js deleted file mode 100644 index 824add0..0000000 --- a/filters/forest.js +++ /dev/null @@ -1,26 +0,0 @@ - -/** Parses a string into a tree */ -var parse = function(obj){ - return logic.parse(obj); -}; - -/** Simplifies a parsed tree */ -var simplify = function( tree ){ - return logic.simplify(tree); -}; - -/** Checks if an object matches a filter tree */ -var matches = function(tree, obj){ - return logic.matches(tree, obj); -} - -/** Finds objects that match a filter tree */ -var map_match = function(tree, objs){ - var res = []; - - for(var i = 0; i < objs.length; i++){ - res.push(matches(tree, objs[i])); - } - - return res; -} diff --git a/filters/forest.py b/filters/forest.py deleted file mode 100644 index 6c2faa1..0000000 --- a/filters/forest.py +++ /dev/null @@ -1,79 +0,0 @@ -from django.contrib.staticfiles import finders -import os.path -import execjs - -from jay.utils import memoize - - -def init(): - """ Initialises the javascript context """ - - # find the path to the static files. - - files = [ - 'js/forest/jsep.js', - 'js/forest/logic.js', - 'js/forest/layouter.js', - 'js/forest/renderer.js' - ] - - src = '' - - for f in files: - with open(finders.find(f)) as g: - src += g.read() - - with open(os.path.join(os.path.dirname(__file__), 'forest.js')) as g: - src += g.read() - - # and eval them. - return execjs.compile(src) - - -# initialise the context -ctx = init() - - -@memoize -def parse(treestr): - return ctx.call('parse', treestr) - - -@memoize -def simplify(tree): - return ctx.call('simplify', tree) - - -def parse_and_simplify(treestr): - tree = parse(treestr) - return simplify(tree) - - -@memoize -def matches(tree, obj): - return ctx.call('matches', tree, obj) - - -def map_match(tree, objs): - return ctx.call('map_match', tree, objs) - - -@memoize -def layouter(tree, obj): - return ctx.call('layouter', tree, obj) - - -@memoize -def renderer(layout): - return ctx.call('renderer', layout) - - -@memoize -def renderer_box(contentNode, inp, out): - return ctx.call('renderer.box', contentNode, inp, out) - - -def parse_and_render(treestr, obj): - tree = parse(treestr) - layout = layouter(tree, obj) - return renderer(layout) diff --git a/filters/forest/__init__.py b/filters/forest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/filters/forest/layouter.py b/filters/forest/layouter.py new file mode 100644 index 0000000..6875ae0 --- /dev/null +++ b/filters/forest/layouter.py @@ -0,0 +1,242 @@ +from . import logic + + +def layouter(tree, obj): + op = tree["operation"] + + # we have a constant or a filter, there is only one thing to render + if ((op in logic.op_list["OP_TRUE"]) or ( + op in logic.op_list["OP_FALSE"]) or ( + op in logic.op_list["OPS_FILTERS"])): + return layout_const(op, tree, obj) + + # for a unary operation, we add a new node on top + if (op in logic.op_list["OPS_UNARY"]): + right = layouter(tree["right"], obj) + return layout_unary(op, right, tree, obj) + + # for a binary operation, we need to merge two existing parts + if (op in logic.op_list["OPS_BINARY"]): + left = layouter(tree["left"], obj) + right = layouter(tree["right"], obj) + return layout_binary(op, left, right, tree, obj) + + raise Exception("Unexpected operator during rendering") + + +def layout_const(op, tree, obj): + doesMatch = logic.matches(tree, obj) + is_filter = False + + # if it is a filter, include key, value + if (op in logic.op_list["OPS_FILTERS"]): + is_filter = True + + return { + 'size': [1, 1], # height x width + 'mainX': 0, + 'out': logic.matches(tree, obj), + 'grid': [[ + { + 'type': 'node', + 'prop': { + 'class': 'const', + 'op': op, + + 'is_filter': is_filter, + 'key': tree['key'] if 'key' in tree else '', + 'value': tree['value'] if 'value' in tree else '', + + 'input': [], + 'output': doesMatch + } + } + ]] + } + + +def layout_unary(op, right, tree, obj): + # get some properties from the right. + rightSize = right['size'] + rightGrid = right['grid'] + rightOut = right['out'] + rightX = right['mainX'] + + # check the value we should return + doesMatch = logic.matches(tree, obj) + + # these are two new lines to render + nodeline = [] + connline = [] + + # create them with a lot of empty space. + for i in range(rightSize[1]): + if i != rightX: + nodeline.append({ + "type": "empty" + }) + + connline.append({ + "type": "empty" + }) + else: + # push the node on top + nodeline.append({ + 'type': 'node', + 'prop': { + 'op': op, + 'input': [rightOut], + 'output': doesMatch + } + }) + + # push a connection line + connline.append({ + 'type': 'conn', + 'prop': { + 'class': 'tree_connect_ver', + 'active': [rightOut] + } + }) + + # add the connection line + rightGrid.insert(0, connline) + + # and the nodeline + rightGrid.insert(0, nodeline) + + # and assemble the thing to return + return { + 'size': [rightSize[0] + 2, right['size'][1]], + 'mainX': rightX, + 'out': doesMatch, + 'grid': rightGrid + } + + +def make_empty_line(size): + return [{"type": "empty"} for i in range(size)] + + +def layout_binary(op, left, right, tree, obj): + # get some properties from the left. + leftSize = left['size'] + leftGrid = left['grid'] + leftOut = left['out'] + leftX = left['mainX'] + + # get some properties from the right. + rightSize = right['size'] + rightGrid = right['grid'] + rightOut = right['out'] + rightX = right['mainX'] + + # the new size + newWidth = leftSize[1] + rightSize[1] + 1 + newHeight = max(leftSize[0], rightSize[0]) + 2 + + # check the value we should return + doesMatch = logic.matches(tree, obj) + + # these are two new lines to render + nodeline = [] + connline = [] + + # create the new top lines + for i in range(newWidth): + if (i != leftSize[1]): + nodeline.append({ + 'type': 'empty' + }) + + # for the left x, we need to connect towards the right + if (i == leftX): + connline.append({ + 'type': 'conn', + 'prop': { + 'active': [leftOut], + 'class': 'tree_connect_bot_right' + } + }) + elif i < leftX: + connline.append({ + 'type': 'empty' + }) + elif (i <= leftSize[1]): + connline.append({ + 'type': 'conn', + 'prop': { + 'active': [leftOut], + 'class': 'tree_connect_hor' + } + }) + elif (i <= leftSize[1] + rightX): + connline.append({ + 'type': 'conn', + 'prop': { + 'active': [rightOut], + 'class': 'tree_connect_hor' + } + }) + elif (i == leftSize[1] + rightX + 1): + connline.append({ + 'type': 'conn', + 'prop': { + 'active': [rightOut], + 'class': 'tree_connect_bot_left' + } + }) + else: + connline.append({ + 'type': 'empty' + }) + else: + # push the node on top + nodeline.append({ + 'type': 'node', + 'prop': { + 'class': 'binary', + 'op': op, + 'input': [leftOut, rightOut], + 'output': doesMatch + } + }) + + # push the connection line l / r + connline.append({ + 'type': 'conn', + 'prop': { + 'active': [leftOut, rightOut], + 'class': 'tree_connect_top_lr' + } + }) + + # create a new grid + newGrid = [] + + # push the top two lines + newGrid.append(nodeline) + newGrid.append(connline) + + for i in range(newHeight - 2): + newGrid.append( + [] + + + (leftGrid[i] if len(leftGrid) > i else make_empty_line( + leftSize[1])) + + + [{ + "type": "empty" + }] + + + (rightGrid[i] if len(rightGrid) > i else make_empty_line( + rightSize[1])) + ) + + # and assemble the thing to return + return { + 'size': [newHeight, newWidth], + 'mainX': leftSize[1], + 'out': doesMatch, + 'grid': newGrid + } diff --git a/filters/forest/logic.py b/filters/forest/logic.py new file mode 100644 index 0000000..01ec750 --- /dev/null +++ b/filters/forest/logic.py @@ -0,0 +1,415 @@ +import re +import PreJsPy + +OP_TRUE = ['true'] +OP_FALSE = ['false'] + +OP_NOT = ['not', '!'] + +OP_EQUALS = ['equals', '=', '==', '==='] + +OP_LESS = ['less than', '<'] +OP_LESS_EQUAL = ['less than or equal', '<=', '=<', ] + +OP_GREATER = ['greater than', '>'] +OP_GREATER_EQUAL = ['greater than or equal', '>=', '=>'] + +OP_CONTAINS = ['contains', '::'] + +OP_MATCHES = ['matches', 'unicorn', '@'] + +OP_AND = ['and', '&', '&&', '*'] + +OP_OR = ['or', '|', '||', '+'] + +OP_NAND = ['nand', '!&'] + +OP_XOR = ['xor', '^'] + +# ============================================================================= +# OPERATOR GROUPS CONFIG +# ============================================================================= + +# unary operators that expect one binary argument +OPS_UNARY = [] + OP_NOT + +# filters that expect a key and value argument +OPS_FILTERS = [] + OP_EQUALS + OP_LESS + OP_LESS_EQUAL + OP_GREATER + \ + OP_GREATER_EQUAL + OP_CONTAINS + OP_MATCHES + +# binary operators that expect to logical arguments +OPS_BINARY = [] + OP_AND + OP_OR + OP_NAND + OP_XOR + + +# ============================================================================= + + +def popu_jsep(jsep): + jsep.setUnaryOperators(OPS_UNARY) + + OPS_BIN_ALL = dict((o, 1) for o in OPS_BINARY) + OPS_BIN_ALL.update( + dict((o, 2) for o in OPS_FILTERS) + ) + jsep.setBinaryOperators(OPS_BIN_ALL) + + +# create a new parser for parsing all the things +parser = PreJsPy.PreJsPy() +popu_jsep(parser) + + +def parse_binary(tree): + """ + A binary expression is either: + + 1. A literal expression + 1a. the literal true ( ===> true ) + 1b. the literal false ( ===> false ) + 1c. the literal null ( ===> false ) + 2. A unary logical expression + 2a. a not expression + 3. A binary logical connetive + 3a. a logical And + 3b. a logical or + 3c. a logical nand + 3d. a logical xor + 4. A binary filter + 4a. an equals filter + 4b. a less filter + 4c. a less or equals filter + 4d. a greater filter + 4e. a greater than filter + 4f. a contains filter + 4g. a matches filter + 5. A primitive expression ( ==> is the string false-ish ? ) + """ + + # if there is nothing, return + if not tree: + raise Exception("Missing binary expression. ") + + # check for literals + if tree["type"] == "Literal": + if tree["raw"] == OP_TRUE[0]: + return {'operation': OP_TRUE[0]} + elif (tree["raw"] == OP_FALSE[0]): + return {'operation': OP_FALSE[0]} + elif (tree.raw == 'null'): + return {'operation': OP_FALSE[0]} + + # check for a unary expression + if (tree["type"] == 'UnaryExpression'): + # find the index inside the logical not + if tree["operator"] in OP_NOT: + return {'operation': OP_NOT[0], + 'right': parse_binary(tree["argument"])} + else: + raise Exception( + 'Expected a binary expression, but found unknown ' + 'UnaryExpression type in input. ') + + # check for binary logical connectives + if tree["type"] == 'BinaryExpression' \ + or tree["type"] == 'LogicalExpression': + + # check for a logical and + if tree["operator"] in OP_AND: + return {'operation': OP_AND[0], 'left': parse_binary(tree["left"]), + 'right': parse_binary(tree["right"])} + + # check for a logical or + if tree["operator"] in OP_OR: + return {'operation': OP_OR[0], 'left': parse_binary(tree["left"]), + 'right': parse_binary(tree["right"])} + + # check for a logical nand + if tree["operator"] in OP_NAND: + return {'operation': OP_NAND[0], + 'left': parse_binary(tree["left"]), + 'right': parse_binary(tree["right"])} + + # check for a logical xor + if tree["operator"] in OP_XOR: + return {'operation': OP_XOR[0], 'left': parse_binary(tree["left"]), + 'right': parse_binary(tree["right"])} + + # check for binary filter + if tree["type"] == 'BinaryExpression' \ + or tree["type"] == 'LogicalExpression': + # an equals filter + if tree["operator"] in OP_EQUALS: + return {'operation': OP_EQUALS[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + # a less filter + if tree["operator"] in OP_LESS: + return {'operation': OP_LESS[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + # a less or equals filter + if tree["operator"] in OP_LESS_EQUAL: + return {'operation': OP_LESS_EQUAL[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + # a greater filter + if tree["operator"] in OP_GREATER: + return {'operation': OP_GREATER[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + # a greater or equals filter + if tree["operator"] in OP_GREATER_EQUAL: + return {'operation': OP_GREATER_EQUAL[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + # a contains filter + if tree["operator"] in OP_CONTAINS: + return {'operation': OP_CONTAINS[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + # a matches filter + if tree["operator"] in OP_MATCHES: + return {'operation': OP_MATCHES[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + raise Exception( + 'Expected a binary expression, but found unknown ' + 'BinaryExpression / LogicalExpression in input. ') + + is_primitive = False + + try: + parse_primitive(tree) + is_primitive = True + except: + pass + + if is_primitive: + raise Exception( + "Expected a binary expression, but found a primitive instead. ") + else: + raise Exception( + "Expected a binary expression, but found unknown expression type " + "in input. ") + + +def parse_primitive(tree): + """ + A primitive expression is either: + + 1. A ThisExpression ( ==> this ) + 1. A literal expression ( ==> literal.raw ) + 2. A identifier expression ( ==> identifier.name ) + 3. A non-empty compound expression consisting of primitives + as above ( ==> join as strings ) + + """ + + # if there is nothing, return + if not tree: + raise Exception('Missing binary expression. ') + + # check for a literal expression + if (tree["type"] == 'Literal'): + if isinstance(tree["value"], str): + return tree["value"] + else: + return tree["raw"] + + # check for an identifier + if tree["type"] == "Identifier": + return tree["name"] + + # in case of a compound expression + if tree["type"] == "CompoundExpression": + return " ".join([parse_primitive(e) for e in tree["body"]]) + + raise Exception( + "Expected a primitive expression, but found unknown expression type " + "in input. ") + + +def parse(s): + """ + Parses a string into an expression + """ + + ast = parser.parse(s) + + return parse_binary(ast) + + +def matches_logical(tree, obj): + """ Check if an object matches a logical tree """ + + # operation of the tree + op = tree['operation'] + + # constants + if op == OP_TRUE[0]: + return True + + if op == OP_FALSE[0]: + return False + + # not + if op == OP_NOT[0]: + return not matches_logical(tree["right"], obj) + + # binary operations + if op == OP_AND[0]: + return matches_logical(tree['left'], obj) and matches_logical( + tree['right'], obj) + + if op == OP_NAND[0]: + return not ( + matches_logical(tree['left'], obj) and matches_logical( + tree['right'], + obj)) + + if op == OP_OR[0]: + return matches_logical(tree['left'], obj) or matches_logical( + tree['right'], obj) + + if (op == OP_XOR[0]): + return matches_logical(tree['left'], obj) != matches_logical( + tree['right'], obj) + + return matches_filter(tree, obj) + + +def match_toString(obj): + if obj is bool(obj): + return "true" if obj is True else "false" + return str(obj) + + +def match_toFloat(obj): + return float(obj) + + +def matches_filter(tree, obj): + """ Check if a tree matches a filter + + :param tree: + :param obj: + :return: + """ + + # find the operation + op = tree['operation'] + + # find the key and value to check. + key = tree['key'] + value = tree['value'] + + # if the object does not have the property, we can exit immediatly + if (key not in obj): + return False + + # read the key from the object + obj_value = obj[key] + + # equality: check if the objects are equal as strings. + if op in OP_EQUALS: + return match_toString(obj_value) == match_toString(value) + + # numeric comparisions + if op in OP_LESS: + try: + value = match_toFloat(value) + obj_value = match_toFloat(obj_value) + except: + return False + + return obj_value < value + + if op in OP_LESS_EQUAL: + try: + value = match_toFloat(value) + obj_value = match_toFloat(obj_value) + except: + return False + + return obj_value <= value + + if op in OP_GREATER: + try: + value = match_toFloat(value) + obj_value = match_toFloat(obj_value) + except: + return False + + return obj_value > value + + if (op == OP_GREATER_EQUAL[0]): + try: + value = match_toFloat(value) + obj_value = match_toFloat(obj_value) + except: + return False + + return obj_value >= value + + # check if we match a regular expression + if op in OP_MATCHES: + try: + value = re.compile(value) + except: + return False + + return bool(value.match(match_toString(obj_value))) + + # check if a value is contained in this array + if op in OP_CONTAINS: + return value in obj_value + + # and thats it + return False + + +def matches(tree, obj): + """Checks if a tree matches an object""" + + return matches_logical(tree, obj) + + +op_list = { + 'OP_TRUE': OP_TRUE, + 'OP_FALSE': OP_FALSE, + + 'OP_NOT': OP_NOT, + + 'OP_EQUALS': OP_EQUALS, + + 'OP_LESS': OP_LESS, + 'OP_LESS_EQUAL': OP_LESS_EQUAL, + + 'OP_GREATER': OP_GREATER, + 'OP_GREATER_EQUAL': OP_GREATER_EQUAL, + + 'OP_CONTAINS': OP_CONTAINS, + + 'OP_MATCHES': OP_MATCHES, + + 'OP_AND': OP_AND, + + 'OP_OR': OP_OR, + + 'OP_NAND': OP_NAND, + + 'OP_XOR': OP_XOR, + + 'OPS_UNARY': OPS_UNARY, + 'OPS_FILTERS': OPS_FILTERS, + 'OPS_BINARY': OPS_BINARY +} + +__all__ = ['parse', 'matches', 'op_list'] diff --git a/filters/forest/renderer.py b/filters/forest/renderer.py new file mode 100644 index 0000000..8ccf0d9 --- /dev/null +++ b/filters/forest/renderer.py @@ -0,0 +1,109 @@ +import re + +from django.utils.html import escape + + +def classescape(cls): + return re.sub(r"\s", "_", cls).upper() + + +def renderer(layout): + # the grid which contains the actual data + grid = layout["grid"] + + # dimensions + height = layout["size"][0] + width = layout["size"][1] + + table = "" + + for i in range(height): + tr = "" + + for j in range(width): + node = grid[i][j] + td = "" + tr += td + + # close the table row and add it to the table + tr += "" + table += tr + + # close the table + table += "
" + + # if the node is empty, do nothing. + if node["type"] == "empty": + pass + + # if the node is a connection, draw the right arrows in the + # right direction + elif node["type"] == "conn": + + # for a double connection we need to add two divs + if node["prop"]["class"] == "tree_connect_top_lr": + td += "
" + td += "
" + # for a single connection we need to add one div + else: + td += "
" + else: + if "is_filter" in node["prop"] and node["prop"]["is_filter"]: + key = escape(node["prop"]["key"]) + value = escape(node["prop"]["value"]) + + box = """
+
{}
+
+
{}
+
+ """.format(classescape(node["prop"]["op"]), key, value) + else: + box = """
+
+
+ """.format(classescape(node["prop"]["op"])) + + td += renderBox(box, node["prop"]["input"], + node["prop"]["output"]) + + # close the cell and add it to the row + td += "
" + return table + + +def renderStatusNode(addClass, status): + return "
" + + +def renderBox(contentNode, inStatus, outStatus): + # make a box to contain all the other items + box = "
" + + # render a box for the output + box += renderStatusNode("status_node_center", outStatus) + + # make a central box for the content + box += "
" + contentNode + "
" + + # make two nodes for input / output + if (len(inStatus) == 1): + box += renderStatusNode("status_node_center", inStatus[0]) + elif (len(inStatus) == 2): + box += renderStatusNode("status_node_left", inStatus[0]) + box += renderStatusNode("status_node_right", inStatus[1]) + + # close the box + box += "
" + + # and return it. + return box diff --git a/filters/models.py b/filters/models.py index dca5c8a..b706068 100644 --- a/filters/models.py +++ b/filters/models.py @@ -8,7 +8,8 @@ from settings.models import VotingSystem -import filters.forest as forest +from filters.forest import logic +from jay import utils # Create your models here. @@ -23,7 +24,7 @@ def __str__(self): def clean(self): try: - self.tree = json.dumps(forest.parse_and_simplify(self.value)) + self.tree = json.dumps(logic.parse(self.value)) except Exception as e: self.tree = None @@ -41,23 +42,29 @@ def matches(self, obj): """ try: - return forest.matches(json.loads(self.tree), obj) + return logic.matches(json.loads(self.tree), obj) except Exception as e: import sys sys.stderr.write(e) return False - def map_matches(self, objs): + def count_matches(self, objs): + """ Counts the number of objects matching this filter""" - try: - return forest.map_match(json.loads(self.tree), objs) - except Exception as e: - return False + tree = json.loads(self.tree) + c = 0 + + for obj in objs: + try: + if logic.matches(tree, obj): + c += 1 + except: + pass + + return c def canEdit(self, user): - """ - Checks if a user can edit this UserFilter. - """ + """ Checks if a user can edit this UserFilter. """ return self.system.isAdmin(user) diff --git a/filters/templatetags/filter.py b/filters/templatetags/filter.py index f794437..a02fdac 100644 --- a/filters/templatetags/filter.py +++ b/filters/templatetags/filter.py @@ -1,5 +1,7 @@ from django import template -import filters.forest as forest +from django.utils.safestring import mark_safe + +from filters.forest import logic, layouter, renderer import json register = template.Library() @@ -11,16 +13,16 @@ def render_full(src, inp): obj = json.loads(inp) # parse the source code - tree = forest.parse(src) + tree = logic.parse(src) # make a layout with the given object - layout = forest.layouter(tree, obj) + layout = layouter.layouter(tree, obj) # and finally render it - render = forest.renderer(layout) + render = renderer.renderer(layout) # that is what we return - return render + return mark_safe(render) @register.simple_tag(takes_context=False) @@ -31,4 +33,4 @@ def render_lbox(name, inp, out): inp = list(map(lambda x: x == '1', str(inp))) out = (out == '1') - return forest.renderer_box(box, inp, out) + return mark_safe(renderer.renderBox(box, inp, out)) diff --git a/filters/views.py b/filters/views.py index deec2a2..b6fcf2f 100644 --- a/filters/views.py +++ b/filters/views.py @@ -10,13 +10,14 @@ from filters.models import UserFilter from filters.forms import NewFilterForm, EditFilterForm, FilterTestForm, \ FilterTestUserForm -import filters.forest as forest -import json +from filters.forest import logic from votes.models import VotingSystem -from jay.utils import priviliged +from jay.utils import elevated, is_elevated, get_user_details + +import json FILTER_FOREST_TEMPLATE = "filters/filter_forest.html" FILTER_EDIT_TEMPLATE = "filters/filter_edit.html" @@ -24,10 +25,10 @@ @login_required -@priviliged +@elevated def Forest(request, alert_type=None, alert_head=None, alert_text=None): # if the user does not have enough priviliges, throw an exception - if not request.user.profile.isElevated(): + if not is_elevated(request.user): raise PermissionDenied # build a new context @@ -40,7 +41,7 @@ def Forest(request, alert_type=None, alert_head=None, alert_text=None): {'url': reverse('filters:forest'), 'text': 'Filters', 'active': True}) ctx['breadcrumbs'] = bc - (admin_systems, other_systems) = request.user.profile.getSystems() + (admin_systems, other_systems) = VotingSystem.splitSystemsFor(request.user) # give those to the view ctx['admin_systems'] = admin_systems @@ -79,7 +80,7 @@ def FilterNew(request): # check if the user can edit it. # if not, go back to the overview - if not system.isAdmin(request.user.profile): + if not system.isAdmin(request.user): return Forest(request, alert_head="Creation failed", alert_text="Nice try. You are not allowed to edit " "this VotingSystem. ") @@ -115,7 +116,7 @@ def FilterDelete(request, filter_id): # check if the user can edit it. # if not, go back to the overview - if not system.isAdmin(request.user.profile): + if not system.isAdmin(request.user): return Forest(request, alert_head="Deletion failed", alert_text="Nice try. You don't have permissions to " "delete this filter. ") @@ -139,7 +140,7 @@ def FilterDelete(request, filter_id): @login_required -@priviliged +@elevated def FilterEdit(request, filter_id): # make a context ctx = {} @@ -149,7 +150,7 @@ def FilterEdit(request, filter_id): ctx["filter"] = filter # check if the user can edit it - if not filter.canEdit(request.user.profile): + if not filter.canEdit(request.user): raise PermissionDenied # Set up the breadcrumbs @@ -175,7 +176,7 @@ def FilterEdit(request, filter_id): # check if we have a valid tree manually try: - tree = forest.parse(form.cleaned_data['value']) + tree = logic.parse(form.cleaned_data['value']) if not tree: raise Exception except Exception as e: @@ -207,7 +208,7 @@ def FilterEdit(request, filter_id): @login_required -@priviliged +@elevated def FilterTest(request, filter_id, obj=None): # try and grab the user filter filter = get_object_or_404(UserFilter, id=filter_id) @@ -244,7 +245,7 @@ def FilterTest(request, filter_id, obj=None): @login_required -@priviliged +@elevated def FilterTestUser(request, filter_id): # try and grab the user filter filter = get_object_or_404(UserFilter, id=filter_id) @@ -259,7 +260,8 @@ def FilterTestUser(request, filter_id): form = FilterTestUserForm(request.POST) if form.is_valid(): obj = form.cleaned_data["user"] - obj = User.objects.filter(username=obj)[0].profile.details + obj = json.dumps(get_user_details( + User.objects.filter(username=obj)[0])) except Exception as e: print(e) pass diff --git a/jay/settings.py b/jay/settings.py index 825a9c0..1cdc6eb 100644 --- a/jay/settings.py +++ b/jay/settings.py @@ -25,7 +25,15 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.sites', + 'django_forms_bootstrap', + + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'dreamjub.providers.oauth', + 'filters', 'settings', 'users', @@ -65,12 +73,21 @@ WSGI_APPLICATION = 'jay.wsgi.application' # OpenJUB auth -AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend', - 'users.ojub_auth.OjubBackend') +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', +) + +# the token and secret for oauth +DREAMJUB_CLIENT_URL = 'http://localhost:9000/' +DREAMJUB_CLIENT_ID = '7ZYpfROz1AEUDbsTQJJuy4RNL8LVSRTONOAXcjm4' +DREAMJUB_CLIENT_SECRET = 'EkeeT0GgOGOQRHbgFE4n1RERxVNSiOD1HZ80TRfERiWj3cK1hZ' \ + 'oTodH0kv8tz3gbqk53YMDuUFAsoaJkMEg1OM1RrZyd1xaYUGv5' \ + 'CtmHrpmJavc2JvRDNUAkFJgORpUW' # Default after login redirect # These are named URL routes -LOGIN_URL = "login" +LOGIN_URL = "accounts/dreamjub/login" LOGOUT_URL = "logout" LOGIN_REDIRECT_URL = "home" @@ -91,3 +108,6 @@ ) STATIC_URL = '/static/' +SITE_ID = 1 + +ACCOUNT_EMAIL_VERIFICATION = "none" diff --git a/jay/urls.py b/jay/urls.py index 34ffe79..6e77dd9 100644 --- a/jay/urls.py +++ b/jay/urls.py @@ -49,8 +49,7 @@ name="filter_help"), # Authentication - url(r'^login/', auth_views.login, {'template_name': 'auth/login.html'}, - name="login"), + url(r'^accounts/', include('allauth.urls'), name='login'), url(r'^logout/', auth_views.logout, {'template_name': 'auth/logout.html', 'next_page': 'home'}, name="logout"), diff --git a/jay/utils.py b/jay/utils.py index 698bda5..41ced99 100644 --- a/jay/utils.py +++ b/jay/utils.py @@ -13,24 +13,48 @@ def helper(*x): return helper +def is_superadmin(user): + """ Checks if a user is a superadmin. """ + return user.is_superuser + + +def is_elevated(user): + """ Checks if a user is an admin for some voting system """ + if not user.is_superuser: + if not user.admin_set.count() > 0: + return False + return True + + def superadmin(handler): - """ Checks if a user is a super admin. """ + """ Requires a user to be a superadmin. """ def helper(request, *args, **kwargs): - if not request.user.profile.isSuperAdmin(): + if not is_superadmin(request.user): raise PermissionDenied return handler(request, *args, **kwargs) return helper -def priviliged(handler): - """ Checks that a user has elevated privileges. """ +def elevated(handler): + """ Requires a user to be elevated user """ def helper(request, *args, **kwargs): - if not request.user.profile.isElevated(): + if not is_elevated(request.user): raise PermissionDenied return handler(request, *args, **kwargs) return helper + + +def get_user_details(user): + """ Gets a dict() object representing user data """ + + try: + data = user.socialaccount_set.get(provider="dreamjub").extra_data + return data + except: + # fallback to an in-active user with just a username flag + return {'username': user.username, 'active': False} diff --git a/requirements.txt b/requirements.txt index 8b63007..b002762 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,10 +8,14 @@ django-forms-bootstrap==3.0.1 Markdown==2.6.5 # For filters -PyExecJS==1.1.0 - -# For OpenJUB auth backend -requests==2.6.0 +pre-js-py==1.1.0 # For testing pep -pep8 >= 1.7.0 \ No newline at end of file +pep8 >= 1.7.0 + +# For authing against dreamjub +django-allauth==0.29.0 +django-allauth-dreamjub==0.1.3 + +# for counting users +requests-oauthlib==0.8.0 diff --git a/settings/models.py b/settings/models.py index cfeea0a..06de8a4 100644 --- a/settings/models.py +++ b/settings/models.py @@ -3,6 +3,8 @@ from django.contrib import admin from jay.restricted import is_restricted_word +from jay.utils import is_superadmin +from users.models import Admin class VotingSystem(models.Model): @@ -16,16 +18,47 @@ def clean(self): is_restricted_word('machine_name', self.machine_name) def canEdit(self, user): - """ - Checks if a user can edit this voting system. - """ - return user.isSuperAdmin() + """ Checks if a user can edit this voting system. """ + + return is_superadmin(user) def isAdmin(self, user): - """ - Checks if a user is an administrator for this voting system. - """ - return user.isAdminFor(self) + """ Checks if a user is an administrator for this voting system. """ + if user.is_anonymous: + return False + return self.canEdit(user) or \ + Admin.objects.filter(system=self, user=user).exists() + + @classmethod + def getAdminSystems(cls, user): + """ returns a pair (admin, others) of systems for a given user """ + + # if a user is a super admin all of them + if is_superadmin(user): + return VotingSystem.objects.all() + if user.is_anonymous: + return VotingSystem.objects.none() + + return VotingSystem.objects.filter(admin__user=user) + + @classmethod + def splitSystemsFor(cls, user): + """ returns a pair (admin, others) of systems for a given user """ + + # if a user is a super admin, return (all, none) + if is_superadmin(user): + return VotingSystem.objects.all(), VotingSystem.objects.none() + + # if are anonymous return (none, all) + if user.is_anonymous: + return VotingSystem.objects.none(), VotingSystem.objects.all() + + # else we need two quieries + administered = VotingSystem.objects.filter(admin__user=user) + others = VotingSystem.objects.exclude(admin__user=user) + + # and return both QuerySets + return administered, others admin.site.register(VotingSystem) diff --git a/settings/urls.py b/settings/urls.py index db501ca..a0de000 100644 --- a/settings/urls.py +++ b/settings/urls.py @@ -1,15 +1,15 @@ from django.conf.urls import url from django.views.generic import TemplateView -from settings.views import superadmins, systems +from settings.views import systems urlpatterns = [ # Superadmin management - url(r'^$', superadmins.settings, name="settings"), - url(r'^superadmins/add$', superadmins.superadmin_add, name="add"), - url(r'^superadmins/(?P[\w-]+)/remove$', - superadmins.superadmin_remove, name="remove"), + # url(r'^$', superadmins.settings, name="settings"), + # url(r'^superadmins/add$', superadmins.superadmin_add, name="add"), + # url(r'^superadmins/(?P[\w-]+)/remove$', + # superadmins.superadmin_remove, name="remove"), # System management url(r'^systems$', systems.systems, name='systems'), diff --git a/settings/views/systems.py b/settings/views/systems.py index 85f5f6c..5c1b580 100644 --- a/settings/views/systems.py +++ b/settings/views/systems.py @@ -52,7 +52,6 @@ def system_edit(request, system_id): except Exception as e: ctx['alert_head'] = 'Saving failed' ctx['alert_text'] = 'Invalid data submitted' - print(e) return render(request, SETTINGS_SYSTEMS_EDIT_TEMPLATE, ctx) diff --git a/static/js/forest/logic.js b/static/js/forest/logic.js index 104b0cc..5a45508 100644 --- a/static/js/forest/logic.js +++ b/static/js/forest/logic.js @@ -331,207 +331,6 @@ return parse_binary(ast); }; - // simplifies a boolean expression. - var simplify = function (obj) { - - var left, left_op; - var right, right_op; - - if (obj['operation'] == OP_AND[0]) { - // simplifiy on the left and on the right - left = simplify(obj['left']); - right = simplify(obj['right']); - - // find left and right operations - left_op = left['operation']; - right_op = right['operation']; - - // if the right operation is a constant - if (right_op == OP_TRUE[0]){ - return left; - } - - if (right_op == OP_FALSE[0]){ - return {'operation': OP_FALSE[0]}; - } - - if (left_op == OP_TRUE[0]){ - return right; - } - - if (left_op == OP_FALSE[0]){ - return {'operation': OP_FALSE[0]}; - } - - // else return the cleaned operation - return {'operation': OP_AND[0], 'left': left, 'right': right} - } - - if(obj['operation'] == OP_NAND[0]){ - // simplify on the left and on the right - left = simplify(obj['left']); - right = simplify(obj['right']); - - // find left and right operations - left_op = left['operation']; - right_op = right['operation']; - - // if the right operation is a constant - if(right_op == OP_TRUE[0]){ - return simplify({ - operation: OP_NOT[0], - 'right': left - }); - } - - if(right_op == OP_FALSE[0]){ - return {'operation': OP_TRUE[0]}; - } - - - // if the left operation is a constant - if (left_op == OP_TRUE[0]){ - return simplify({ - operation: OP_NOT[0], - 'right': right - }); - } - - if(left_op == OP_FALSE[0]){ - return {'operation': OP_TRUE[0]}; - } - - // else return the cleaned operation - return {'operation': OP_NAND[0], 'left': left, 'right': right} - } - - - if(obj['operation'] == OP_OR[0]){ - // simplify on the left and on the right - left = simplify(obj['left']); - right = simplify(obj['right']); - - // find left and right operations - left_op = left['operation']; - right_op = right['operation']; - - // if the right operation is a constant - if(right_op == OP_TRUE[0]){ - return {'operation': OP_TRUE[0]}; - } - - if(right_op == OP_FALSE[0]){ - return left; - } - - - // if the left operation is a constant - if(left_op == OP_TRUE[0]){ - return {'operation': OP_TRUE[0]}; - } - - if(left_op == OP_FALSE[0]){ - return right; - } - - - // else return the cleaned operation - return {'operation': OP_OR[0], 'left': left, 'right': right}; - } - - if(obj['operation'] == OP_XOR[0]){ - - // simplify on the left and on the right - left = simplify(obj['left']); - right = simplify(obj['right']); - - // find left and right operations - left_op = left['operation']; - right_op = right['operation']; - - // if the right operation is a constant - if(right_op == OP_TRUE[0]){ - return simplify({ - 'operation': OP_NOT[0], - 'right': left - }); - } - - if(right_op == OP_FALSE[0]){ - return left; - } - - - // if the left operation is a constant - if (left_op == OP_TRUE[0]) { - return simplify({ - 'operation': OP_NOT[0], - 'right': right - }); - } - - if (left_op == OP_FALSE[0]) { - return right; - } - - // else return the cleaned operation - return {'operation': OP_XOR[0], 'left': left, 'right': right}; - } - - if(obj['operation'] == OP_NOT[0]) { - // simplify the sub operation - right = simplify(obj['right']); - - // find the operation on the right - right_op = right['operation']; - - // remove true / false - if(right_op == OP_TRUE[0]) { - return {'operation': OP_FALSE[0]}; - } - - if(right_op == OP_FALSE[0]) { - return {'operation': OP_TRUE[0]}; - } - - // remove double nots - if(right_op == OP_NOT[0]){ - return simplify(right['right']); - } - - // ! nand => and - if( right_op == OP_NAND[0]){ - return { - 'operation': OP_AND[0], - 'left': right['left'], - 'right': right['right'] - }; - }; - - // ! and => nand - if(right_op == OP_AND[0]){ - return { - 'operation': OP_NAND[0], - 'left': right['left'], - 'right': right['right'] - }; - } - - // ! or(a, b) = and(!a, !b) - if (right_op == OP_OR[0]){ - return { - 'operation': OP_AND[0], - 'left': simplify({operation:OP_NOT[0], right: right['left']}), - 'right': simplify({operation:OP_NOT[0], right: right['right']}), - } - } - - return {'operation': OP_NOT[0], 'right': simplify(obj['right'])} - } - // otherwise it is a filter, so return as is. - return obj - }; - var matches_logical = function (tree, obj) { // operation of the tree @@ -685,7 +484,6 @@ // exports the entire thing in a namespace var logic = { 'parse': parse, - 'simplify': simplify, 'matches': matches, 'op_list': { diff --git a/templates/auth/login.html b/templates/auth/login.html deleted file mode 100644 index aadffe3..0000000 --- a/templates/auth/login.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base/base.html" %} - -{% block page_title %}Login{% endblock %} - -{% block content %} - -{% if form.errors %} - -{% endif %} - -{% if next %} - {% if user.is_authenticated %} - - {% else %} - - {% endif %} -{% endif %} - -
- -
-
- -
- -
-
- -

If you lost your password, reset it in CampusNet.

- -{% endblock %} diff --git a/templates/base/base.html b/templates/base/base.html index dfa528a..6762619 100644 --- a/templates/base/base.html +++ b/templates/base/base.html @@ -1,5 +1,6 @@ {% load staticfiles %} {% load bootstrap_tags %} +{% load userflags %} @@ -43,18 +44,20 @@ {{ user.username }} {% else %} - Login + Login {% endif %} diff --git a/templates/vote/fragments/option_edit.html b/templates/vote/fragments/option_edit.html index f130c4e..50c68d7 100644 --- a/templates/vote/fragments/option_edit.html +++ b/templates/vote/fragments/option_edit.html @@ -73,7 +73,7 @@

Option {{o.number}}

- +
diff --git a/templates/vote/fragments/vote_list.html b/templates/vote/fragments/vote_list.html index f73dc3b..6c7cf1f 100644 --- a/templates/vote/fragments/vote_list.html +++ b/templates/vote/fragments/vote_list.html @@ -12,13 +12,13 @@  {{ vote.name }} - {% if vote|can_delete:request.user.profile %} + {% if vote|can_delete:request.user %} {% endif %} - {% if vote|can_edit:request.user.profile %} + {% if vote|can_edit:request.user %} diff --git a/templates/vote/fragments/vote_stage.html b/templates/vote/fragments/vote_stage.html index bbd57a0..2f0a076 100644 --- a/templates/vote/fragments/vote_stage.html +++ b/templates/vote/fragments/vote_stage.html @@ -7,7 +7,7 @@
- + @@ -15,7 +15,8 @@
- Re-enter your password to stage the vote. This will prevent any further changes to be made. Staging the vote may take a few seconds. + Enter the name of this vote {{ vote.machine_name | escape }} to stage it.
+ This will prevent further edits from being made. Staging a vote may take a few seconds.
@@ -38,7 +39,8 @@
- Re-enter your password to update eligibility count. This may take a few seconds. + Enter the name of this vote, {{ vote.machine_name | escape }}, to update eligibility count. + This may take a few seconds.
diff --git a/users/models.py b/users/models.py index b8f9d93..f7e15ac 100644 --- a/users/models.py +++ b/users/models.py @@ -1,19 +1,13 @@ from django.db import models from django.contrib.auth.models import User -from django.core.exceptions import ValidationError - from django.contrib import admin -from settings.models import VotingSystem - -import json - # Create your models here. class Admin(models.Model): user = models.ForeignKey(User) - system = models.ForeignKey(VotingSystem) + system = models.ForeignKey("settings.VotingSystem") class Meta(): unique_together = (("system", "user")) @@ -22,87 +16,4 @@ def __str__(self): return u'[%s] %s' % (self.system.machine_name, self.user) -class SuperAdmin(models.Model): - user = models.ForeignKey(User) - - class Meta(): - unique_together = (("user",),) - - def __str__(self): - return u'%s' % (self.user) - - -class UserProfile(models.Model): - user = models.OneToOneField(User, related_name="profile") - details = models.TextField() - - def __str__(self): - return u'[Profile] %s' % (self.user.username) - - def clean(self): - # make sure that the details are a valid json object - try: - json.loads(self.details) - except: - raise ValidationError({ - 'details': ValidationError( - 'Details needs to be a valid JSON object', code='invalid') - }) - - def isSuperAdmin(self): - """ - Returns if this user is a SuperAdmin. - """ - return self.user.superadmin_set.count() > 0 - - def isAdminFor(self, system): - """ - Checks if this user can administer a certain voting system. - """ - return system in self.getAdministratedSystems() - - def getAdministratedSystems(self): - """ - Returns all voting systems this user can administer. - """ - # if we are a superadmin we can manage all systems - if self.isSuperAdmin(): - return VotingSystem.objects.all() - - # else return only the systems we are an admin for. - else: - return list( - map(lambda x: x.system, Admin.objects.filter(user=self.user))) - - def isElevated(self): - """ - Checks if this user is an elevated user. - - (i. e. if they are a superadmin or admin for some voting system) - """ - - # thy are a superadmin - if self.isSuperAdmin(): - return True - - # they administer some voting system - return self.user.admin_set.count() > 0 - - def getSystems(self): - """ - Gets the editable filters for this user. - """ - - # get all the voting systems for this user - admin_systems = self.getAdministratedSystems() - - # and all the other ones also - other_systems = list(filter(lambda a: a not in admin_systems, - VotingSystem.objects.all())) - - return (admin_systems, other_systems) - - admin.site.register(Admin) -admin.site.register(SuperAdmin) -admin.site.register(UserProfile) diff --git a/users/ojub_auth.py b/users/ojub_auth.py index af7a211..c126e69 100644 --- a/users/ojub_auth.py +++ b/users/ojub_auth.py @@ -1,113 +1,37 @@ from django.conf import settings -from django.contrib.auth.models import User -from users.models import UserProfile +from requests_oauthlib import OAuth2Session +from oauthlib.oauth2 import BackendApplicationClient -import requests +OPENJUB_BASE = "http://localhost:9000/" -OPENJUB_BASE = "https://api.jacobs.university/" +def get_all(): + client = BackendApplicationClient(client_id=settings.DREAMJUB_CLIENT_ID) + dreamjub = OAuth2Session(client=client) + dreamjub.fetch_token( + token_url=settings.DREAMJUB_CLIENT_URL + 'login/o/token/', + client_id=settings.DREAMJUB_CLIENT_ID, + client_secret=settings.DREAMJUB_CLIENT_SECRET) -class OjubBackend(object): - """ - Authenticates credentials against the OpenJUB database. + # iterate over the pages (while there is a next) + results = [] + next = settings.DREAMJUB_CLIENT_URL + 'api/v1/users/' - The URL for the server is configured by OPENJUB_BASE in the settings. + while next: + res = dreamjub.get(next) + if not res.ok: + raise Exception( + 'Unable to retrieve current list of students, ' + 'please try again later. ') - This class does not fill in user profiles, this has to be handled - in other places - """ + res = res.json() + results += res['results'] + next = res['next'] if 'next' in res else None - def authenticate(self, username=None, password=None): - r = requests.post(OPENJUB_BASE + "auth/signin", - data={'username': username, 'password': password}) + # replace http with https at most one + if next is not None and next.startswith('http://') \ + and settings.DREAMJUB_CLIENT_URL.startswith('https://'): + next = next.replace('http://', 'https://', 1) - if r.status_code != requests.codes.ok: - return None - - resp = r.json() - - uname = resp['user'] - token = resp['token'] - - details = requests.get(OPENJUB_BASE + "user/me", - params={'token': token}) - - if details.status_code != requests.codes.ok: - print("Could not get user details") - return None - - try: - user = User.objects.get(username=uname) - except User.DoesNotExist: - user = User(username=uname) - - user.set_unusable_password() - - # TODO Don't hardcode this - if user.username in ["lkuboschek", "twiesing", "jinzhang", - "rdeliallis"]: - user.is_staff = True - user.is_superuser = True - - data = details.json() - - user.first_name = data['firstName'] - user.last_name = data['lastName'] - user.email = data['email'] - - user.save() - - # Make a user profile if there isn't one already - try: - profile = UserProfile.objects.get(user=user) - except UserProfile.DoesNotExist: - profile = UserProfile(user=user) - - profile.details = details.text - profile.save() - - return user - - def get_user(self, user_id): - try: - return User.objects.get(pk=user_id) - except User.DoesNotExist: - return None - - -def get_all(username, password): - r = requests.post(OPENJUB_BASE + "auth/signin", - data={'username': username, 'password': password}) - - if r.status_code != requests.codes.ok: - return None - - resp = r.json() - - uname = resp['user'] - token = resp['token'] - - users = [] - - TIMEOUT = 60 - - request = requests.get(OPENJUB_BASE + "query", - params={'token': token, 'limit': 20000}, - timeout=TIMEOUT) - - while True: - if request.status_code != requests.codes.ok: - return None - else: - # read json - resjson = request.json() - - # load all the users - users += resjson["data"] - - # if there was no data or no next field, continue - if len(resjson["data"]) == 0 or not resjson["next"]: - return users - else: - request = requests.get(resjson["next"], timeout=TIMEOUT) + return results diff --git a/votes/models.py b/votes/models.py index 07c5af9..c4a2af1 100644 --- a/votes/models.py +++ b/votes/models.py @@ -1,5 +1,3 @@ -import datetime - from django.utils import timezone from django.db import models, transaction @@ -44,18 +42,16 @@ def clean(self): is_restricted_word('machine_name', self.machine_name) def canEdit(self, user): - """ - Checks if a user can edit this vote. - """ - return user.isAdminFor( - self.system) and self.status.stage != Status.PUBLIC + """ Checks if a user can edit this vote. """ + + return self.system.isAdmin(user) \ + and self.status.stage != Status.PUBLIC def canDelete(self, user): - """ - Check if a user can delete this vote. - """ - return user.isAdminFor( - self.system) and self.status.stage == Status.INIT + """ Checks if a user can delete this vote. """ + + return self.system.isAdmin(user) \ + and self.status.stage == Status.INIT def canBeModified(self): """ @@ -63,27 +59,24 @@ def canBeModified(self): """ return self.status.stage == Status.INIT - def update_eligibility(self, username, password): + def update_eligibility(self): + + # TODO: Retrieve the API key things PassiveVote.objects.get_or_create(vote=self, defaults={ 'num_voters': 0, 'num_eligible': 0}) - if self.filter is not None: + if self.filter is None: raise Exception("Missing filter. ") # this will take really long - everyone = get_all(username, password) + everyone = get_all() if not everyone: raise Exception("Invalid password or something went wrong. ") - check = self.filter.map_matches(everyone) - c = 0 - - for b in check: - if b: - c += 1 + c = self.filter.count_matches(everyone) # get or create the passive vote object (pv, _) = PassiveVote.objects.get_or_create(vote=self, defaults={ diff --git a/votes/views.py b/votes/views.py index fb8971e..9503357 100644 --- a/votes/views.py +++ b/votes/views.py @@ -26,9 +26,10 @@ from django.contrib.auth.models import User +from jay import utils from votes.forms import EditVoteForm, EditVoteFilterForm, \ - EditVoteOptionsForm, GetVoteOptionForm, EditVoteOptionForm, PasswordForm, \ - EditScheduleForm, AdminSelectForm + EditVoteOptionsForm, GetVoteOptionForm, EditVoteOptionForm, \ + PasswordForm, EditScheduleForm, AdminSelectForm VOTE_ERROR_TEMPLATE = "vote/vote_msg.html" VOTE_RESULT_TEMPLATE = "vote/vote_result.html" @@ -52,7 +53,7 @@ def system_home(request, system_name): all_votes = Vote.objects.filter(system=vs) - if request.user.is_authenticated() and vs.isAdmin(request.user.profile): + if request.user.is_authenticated() and vs.isAdmin(request.user): ctx['votes'] = all_votes ctx['results'] = Vote.objects.filter(system=vs, status__stage__in=[Status.PUBLIC, @@ -81,7 +82,7 @@ def admin(request, system_name, alert_type=None, alert_head=None, vs = get_object_or_404(VotingSystem, machine_name=system_name) # raise an error if the user trying to access is not an admin - if not vs.isAdmin(request.user.profile): + if not vs.isAdmin(request.user): raise PermissionDenied ctx['vs'] = vs @@ -112,7 +113,7 @@ def admin_add(request, system_name): vs = get_object_or_404(VotingSystem, machine_name=system_name) # raise an error if the user trying to access is not an admin - if not vs.isAdmin(request.user.profile): + if not vs.isAdmin(request.user): raise PermissionDenied try: @@ -151,7 +152,7 @@ def admin_remove(request, system_name): vs = get_object_or_404(VotingSystem, machine_name=system_name) # raise an error if the user trying to access is not an admin - if not vs.isAdmin(request.user.profile): + if not request.user.isAdmin(request.user): raise PermissionDenied try: @@ -228,6 +229,7 @@ def get_vote_props(ctx, vote): def vote_edit_context(request, system_name, vote_name): + from jay import utils """ Returns context and basic parameters for vote editing. """ @@ -237,14 +239,14 @@ def vote_edit_context(request, system_name, vote_name): vote.touch() # raise an error if the user trying to access is not an admin - if not system.isAdmin(request.user.profile): + if not system.isAdmin(request.user): raise PermissionDenied # make a context ctx = {} # get all the systems this user can edit - (admin_systems, other_systems) = request.user.profile.getSystems() + (admin_systems, other_systems) = VotingSystem.splitSystemsFor(request.user) # add the vote to the system ctx['vote'] = vote @@ -281,7 +283,7 @@ def vote_add(request, system_name): vs = get_object_or_404(VotingSystem, machine_name=system_name) # raise an error if the user trying to access is not an admin - if not vs.isAdmin(request.user.profile): + if not vs.isAdmin(request.user): raise PermissionDenied v = Vote() @@ -311,7 +313,7 @@ def vote_add(request, system_name): def vote_delete(request, system_name, vote_name): (system, vote, ctx) = vote_edit_context(request, system_name, vote_name) - if vote.canDelete(request.user.profile): + if vote.canDelete(request.user): vote.delete() return redirect('votes:system', system_name=system_name) @@ -440,18 +442,18 @@ def vote_stage(request, system_name, vote_name): if not form.is_valid(): raise Exception - # read username + password - username = request.user.username - password = form.cleaned_data['password'] + # make sure that the name of the vote has been submitted + if form.cleaned_data['password'] != vote_name: + raise Exception except: - ctx['alert_head'] = 'Saving failed' + ctx['alert_head'] = 'Staging vote failed' ctx['alert_text'] = 'Invalid data submitted' return render(request, VOTE_EDIT_TEMPLATE, ctx) # set the vote status to public try: - vote.update_eligibility(username, password) + vote.update_eligibility() vote.status.stage = Status.STAGED vote.status.save() @@ -499,7 +501,6 @@ def vote_time(request, system_name, vote_name): public_time = form.cleaned_data["public_time"] except Exception as e: - print(e, form.errors) ctx['alert_head'] = 'Saving failed' ctx['alert_text'] = 'Invalid data submitted' return render(request, VOTE_EDIT_TEMPLATE, ctx) @@ -547,18 +548,18 @@ def vote_update(request, system_name, vote_name): if not form.is_valid(): raise Exception - # read username + password - username = request.user.username - password = form.cleaned_data['password'] + # make sure that the name of the vote has been submitted + if form.cleaned_data['password'] != vote_name: + raise Exception except: ctx['alert_head'] = 'Saving failed' ctx['alert_text'] = 'Invalid data submitted' return render(request, VOTE_EDIT_TEMPLATE, ctx) - # set the vote status to public + # re-count if eligible try: - vote.update_eligibility(username, password) + vote.update_eligibility() except Exception as e: ctx['alert_head'] = 'Updating eligibility failed. ' ctx['alert_text'] = str(e) @@ -1013,7 +1014,7 @@ def results(request, system_name, vote_name): if vote.status.stage != Status.PUBLIC: if vote.status.stage == Status.CLOSE and \ request.user.is_authenticated(): - if vote.system.isAdmin(request.user.profile): + if vote.system.isAdmin(request.user): ctx['alert_type'] = 'info' ctx['alert_head'] = 'Non-public' ctx['alert_text'] = 'The results are not public yet. You ' \ @@ -1060,7 +1061,7 @@ def get(self, request, system_name, vote_name): # TODO Check status of vote try: - user_details = json.loads(request.user.profile.details) + user_details = utils.get_user_details(request.user) if not filter: ctx['alert_head'] = "No filter given." @@ -1157,7 +1158,7 @@ def post(self, request, system_name, vote_name): return self.render_error_response(ctx) try: - user_details = json.loads(request.user.profile.details) + user_details = utils.get_user_details(request.user) if not filter: ctx['alert_head'] = "No filter given."