class AddMultiVotesSchema(BaseMultiVotesSchema): confirm = colander.SchemaNode( colander.Bool(), title=_("I understand the consequences of adding this"), validator=colander.Function(_only_true_validator, _("Confirmation not clicked")), )
def includeme(config): config.add_view_action( control_panel_category, "control_panel", "multivotes", panel_group="control_panel_multivotes", title=_("Multiple votes"), description= _("Allow users to have multiple votes. This will change how you're able to invite users too." ), permission=security.MODERATE_MEETING, check_active=_is_active, ) config.add_view_action( only_when_inactive(control_panel_link), "control_panel_multivotes", "activate", title=_("Activate..."), view_name="_activate_multivotes", ) config.add_view_action( create_and_assign_link, "control_panel_multivotes", "create_assignments", title=_("Create and assign"), view_name="create_assignments", ) config.add_view_action(multivotes_nav, "nav_meeting", "multivotes") config.add_view_action(multivotes_nav, "control_panel_multivotes", "multivotes_page")
def validate_rows(node, value): i = 1 schema = VoteAssignmenRow() for row in value: if len(row) == 2: # To avoid trim row.append("") if len(row) == 3: try: schema.deserialize(row) except colander.Invalid as exc: request = get_current_request() for (k, v) in exc.asdict( translate=request.localizer.translate).items(): msg = _( "colander_csv_error", default="Row ${num} - item ${item} error: ${err_msg}", mapping={ "num": i, "item": k, "err_msg": v }, ) raise colander.Invalid(node, msg) elif len(row) == 1 and not row[0]: pass else: raise colander.Invalid( node, _("Row ${num} must have 3 elements", mapping={"num": i})) i += 1
class LockAssignmentWorkflow(Workflow): name = "lock_assignment" title = _("Lock assignment workflow") # description = _("") states = {"open": _("Open"), "locked": _("Locked")} transitions = {} initial_state = "open" @classmethod def init_acl(cls, registry): aclreg = registry.acl # Open open_name = "%s:open" % cls.name open_acl = aclreg.new_acl(open_name, title=_("Open")) open_acl.add(security.ROLE_ADMIN, security.ALL_PERMISSIONS) open_acl.add( security.ROLE_MODERATOR, [security.MODERATE_MEETING, security.EDIT, ADD_VOTE_ASSIGNMENT]) open_acl.add(security.ROLE_VIEWER, [security.VIEW]) # Locked locked_name = "%s:locked" % cls.name locked_acl = aclreg.new_acl(locked_name, title=_("Locked")) locked_acl.add(security.ROLE_ADMIN, [security.MODERATE_MEETING, security.VIEW]) locked_acl.add(security.ROLE_MODERATOR, [security.MODERATE_MEETING]) locked_acl.add(security.ROLE_VIEWER, [security.VIEW])
def __call__(self): block_during_ongoing_poll(self.context) if not self.request.has_permission(ADD_VOTE_ASSIGNMENT, self.context): if self.context.wf_state == 'locked': msg = _("Not allowed when assignment is locked. Open it to use this") self.flash_messages.add(msg, type="danger") raise HTTPFound(location=self.request.resource_url(self.context)) else: raise HTTPForbidden(_("Forbidden")) return super(CreateAndAssignForm, self).__call__()
class BaseMultiVotesSchema(colander.Schema): title = colander.SchemaNode( colander.String(), validator=colander.Length(max=20), title=_("Short title, used in navigation too"), description=_( "multivotes_schema_title_description", default= "Max 20 chars. Something like 'Organisations' or 'Vote power'. " "Basically the reason that someone has several votes.", ), )
def init_acl(cls, registry): aclreg = registry.acl # Open open_name = "%s:open" % cls.name open_acl = aclreg.new_acl(open_name, title=_("Open")) open_acl.add(security.ROLE_ADMIN, security.ALL_PERMISSIONS) open_acl.add( security.ROLE_MODERATOR, [security.MODERATE_MEETING, security.EDIT, ADD_VOTE_ASSIGNMENT]) open_acl.add(security.ROLE_VIEWER, [security.VIEW]) # Locked locked_name = "%s:locked" % cls.name locked_acl = aclreg.new_acl(locked_name, title=_("Locked")) locked_acl.add(security.ROLE_ADMIN, [security.MODERATE_MEETING, security.VIEW]) locked_acl.add(security.ROLE_MODERATOR, [security.MODERATE_MEETING]) locked_acl.add(security.ROLE_VIEWER, [security.VIEW])
class VoteAssignmentCreateCSV(colander.Schema): csv_items = colander.SchemaNode( colander.String(), title=_( "paste_spreadsheet_text_title", default= "Paste rows from Excel, Google docs, Libre-office or similar.", ), description=_( "paste_spreadsheet_text_description", default= "Columns must be: Title of organisation, Number of votes, user email (or blank).", ), widget=deform.widget.TextAreaWidget(rows=10), preparer=splitrows, validator=validate_rows, )
def block_starting_polls_with_multivotes_open(context, event): if event.new_state == "ongoing": meeting = find_interface(context, IMeeting) if MEETING_NAMESPACE in meeting: mv = meeting[MEETING_NAMESPACE] if mv.wf_state == "open": raise HTTPForbidden( _("You may not start polls unless you've locked the assignment of votes. See multivotes." ))
def block_during_ongoing_poll(context): if check_ongoing_poll(context): raise HTTPForbidden( _( "access_during_ongoing_not_allowed", default="During ongoing polls, this action isn't allowed. " "Try again when polls have closed.", ) )
class CreateAndAssignForm(BaseForm): title = _("Create and assign") type_name = "VoteAssignment" schema_name = "create_csv" def __call__(self): block_during_ongoing_poll(self.context) if not self.request.has_permission(ADD_VOTE_ASSIGNMENT, self.context): if self.context.wf_state == 'locked': msg = _("Not allowed when assignment is locked. Open it to use this") self.flash_messages.add(msg, type="danger") raise HTTPFound(location=self.request.resource_url(self.context)) else: raise HTTPForbidden(_("Forbidden")) return super(CreateAndAssignForm, self).__call__() def save_success(self, appstruct): factory = self.request.content_factories["VoteAssignment"] get_user_by_email = self.request.root["users"].get_user_by_email va_schema = VoteAssignmentSchema().bind( context=self.context, request=self.request ) obj_count = 0 assign_count = 0 not_found_count = 0 for row in appstruct["csv_items"]: userid = "" email = row[2] if email: # Only assign users that can be found via a validated email address # Blank assignment is ok user = get_user_by_email(email, only_validated=True) if user is None: not_found_count += 1 else: # email not found userid = user.userid assign_count += 1 payload = {"title": row[0], "votes": row[1], "userid_assigned": userid} kw = va_schema.deserialize(payload) obj = factory(**kw) self.context[obj.uid] = obj obj_count += 1 msg = _( "created_msg_summary", default="Created ${obj_count} objects. ${assign_count} users were assigned via email. " "${not_found_count} email addresses didn't match a registered user.", mapping={ "obj_count": obj_count, "assign_count": assign_count, "not_found_count": not_found_count, }, ) self.flash_messages.add(msg, type="info", auto_destruct=False) return HTTPFound(location=self.request.resource_url(self.context))
class VoteAssignment(Base): """ Votes are assigned with this item when using multi-vote system. A single user is assigned votes. """ type_name = "VoteAssignment" type_title = _("Vote Assignment") type_description = "" add_permission = ADD_VOTE_ASSIGNMENT naming_attr = "uid" title = "" votes = 1 userid_assigned = None
class VoteAssignmentSchema(colander.Schema): title = colander.SchemaNode( colander.String(), title= _("Name of voting asignment - like the organisation that this represents" ), ) votes = colander.SchemaNode( colander.Int(), title=_("Number of votes"), description=_("1-500 is a valid number"), default=1, validator=colander.Range(max=500, min=1), ) userid_assigned = colander.SchemaNode( colander.String(), title=_("The user assigned"), widget=MeetingUserReferenceWidget(multiple=False), validator=existing_userids, missing="", )
def save_success(self, appstruct): factory = self.request.content_factories["VoteAssignment"] get_user_by_email = self.request.root["users"].get_user_by_email va_schema = VoteAssignmentSchema().bind( context=self.context, request=self.request ) obj_count = 0 assign_count = 0 not_found_count = 0 for row in appstruct["csv_items"]: userid = "" email = row[2] if email: # Only assign users that can be found via a validated email address # Blank assignment is ok user = get_user_by_email(email, only_validated=True) if user is None: not_found_count += 1 else: # email not found userid = user.userid assign_count += 1 payload = {"title": row[0], "votes": row[1], "userid_assigned": userid} kw = va_schema.deserialize(payload) obj = factory(**kw) self.context[obj.uid] = obj obj_count += 1 msg = _( "created_msg_summary", default="Created ${obj_count} objects. ${assign_count} users were assigned via email. " "${not_found_count} email addresses didn't match a registered user.", mapping={ "obj_count": obj_count, "assign_count": assign_count, "not_found_count": not_found_count, }, ) self.flash_messages.add(msg, type="info", auto_destruct=False) return HTTPFound(location=self.request.resource_url(self.context))
class MultiVotes(Content, ContextACLMixin): """ A container for vote assignment when the multi vote system is activated. """ type_name = "MultiVotes" type_title = _("MultiVotes") type_description = "" add_permission = MODERATE_MEETING css_icon = "glyphicon glyphicon-folder-open" nav_visible = True listing_visible = True search_visible = False title = _("MultiVotes") total_votes = 0 total_voters = 0 assigned_votes = 0 def get_sorted_values(self): """ Return all contained Group object sorted on title. """ return sorted(self.values(), key=lambda x: x.title.lower()) def get_assignments(self, userid): """ Get the users assignments. """ for v in self.values(): if v.userid_assigned == userid: yield v def get_vote_power(self, userid): """ How many votes should this user have based on the number of assignments they have. """ votes = 0 for v in self.get_assignments(userid): votes += v.votes return votes def update_totals(self): total_votes = 0 assigned_votes = 0 voters = set() for v in self.values(): total_votes += v.votes if v.userid_assigned: assigned_votes += v.votes voters.add(v.userid_assigned) self.total_votes = total_votes self.total_voters = len(voters) self.assigned_votes = assigned_votes def get_voters(self): results = set() for v in self.values(): if v.userid_assigned: results.add(v.userid_assigned) return results def recheck_voters(self): """ Perform a check against the current voters and who's actually in a vote assignment. """ meeting = self.__parent__ lr = meeting.local_roles current_role_voters = frozenset(lr.get_any_local_with(ROLE_VOTER)) current_mv_voters = self.get_voters() # Make sure the voting role matches the users that should have a vote. # Invitations and similar doesn't matter in a multivote meeting remove_userids = current_role_voters - current_mv_voters add_userids = current_mv_voters - current_role_voters for userid in remove_userids: lr.remove(userid, ROLE_VOTER, event=False) for userid in add_userids: lr.add(userid, ROLE_VOTER, event=False) if remove_userids or add_userids: # Send one event afterwards to avoid clutter lr.send_event()
open_acl.add( security.ROLE_MODERATOR, [security.MODERATE_MEETING, security.EDIT, ADD_VOTE_ASSIGNMENT]) open_acl.add(security.ROLE_VIEWER, [security.VIEW]) # Locked locked_name = "%s:locked" % cls.name locked_acl = aclreg.new_acl(locked_name, title=_("Locked")) locked_acl.add(security.ROLE_ADMIN, [security.MODERATE_MEETING, security.VIEW]) locked_acl.add(security.ROLE_MODERATOR, [security.MODERATE_MEETING]) locked_acl.add(security.ROLE_VIEWER, [security.VIEW]) LockAssignmentWorkflow.add_transitions( from_states="open", to_states="locked", title=_("Lock vote changes"), permission=security.MODERATE_MEETING, ) LockAssignmentWorkflow.add_transitions( from_states="locked", to_states="open", title=_("Enable vote updates"), permission=security.MODERATE_MEETING, ) def includeme(config): config.add_workflow(LockAssignmentWorkflow)
def __call__(self): self.context.userid_assigned = None self.flash_messages.add(_("Cleared assigned user from ${title}", mapping={'title': self.context.title})) return redirect_parent(self.context, self.request)