class Member(db.Model): id = db.Column(db.Integer, primary_key=True) source = db.Column(db.String(20), default="manual") # ldap|manual type = db.Column(db.String(10)) polls = db.relationship("Poll", backref="owner", lazy="dynamic") __mapper_args__ = { 'polymorphic_identity': 'member', 'polymorphic_on': type }
class Group(Member): id = db.Column(db.Integer, db.ForeignKey("member.id"), primary_key=True) name = db.Column(db.String(80)) identifier = db.Column(db.String(80)) admin_id = db.Column(db.Integer, db.ForeignKey("user.id")) # relationships users = db.relationship("User", backref="groups", lazy="dynamic", secondary="group_users") __mapper_args__ = { 'polymorphic_identity': 'group', } @property def displayname(self): return self.name @property def changeable(self): return self.source == "manual"
class ChoiceValue(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(80)) icon = db.Column(db.String(64)) color = db.Column(db.String(6)) poll_id = db.Column(db.Integer, db.ForeignKey("poll.id")) deleted = db.Column(db.Boolean, default=False) weight = db.Column(db.Float, default=0.0) # relationships vote_choices = db.relationship("VoteChoice", backref="value", lazy="dynamic") def __init__(self, title="", icon="question", color="EEEEEE", weight=0.0): self.title = title self.icon = icon self.color = color self.weight = weight def to_dict(self): return dict(id=self.id, title=self.title, icon=self.title, color=self.color, deleted=self.deleted, weight=self.weight) def copy(self): n = ChoiceValue() n.title = self.title n.icon = self.icon n.color = self.color n.deleted = self.deleted n.weight = self.weight n.poll = self.poll return n
class Vote(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80)) poll_id = db.Column(db.Integer, db.ForeignKey("poll.id")) user_id = db.Column(db.Integer, db.ForeignKey("user.id")) anonymous = db.Column(db.Boolean, default=False) assigned_by_id = db.Column(db.Integer, db.ForeignKey("user.id")) created = db.Column(db.DateTime) comment = db.Column(db.Text) invitation = db.relationship("Invitation", backref="vote", lazy="dynamic") def __init__(self): self.created = datetime.utcnow() # relationship vote_choices = db.relationship("VoteChoice", backref="vote", cascade="all, delete-orphan", lazy="dynamic") @property def assigned(self): return self.assigned_by and self.assigned_by != self.user def user_can_delete(self, user): # disallow on deleted/expired polls if self.poll.deleted or self.poll.is_expired: return False # only if logged in if not user.is_authenticated(): return False # allow for poll author if self.poll.user_can_administrate(user): return True # allow for user if self.user and self.user == user: return True # allow for admin if user.is_admin: return True # disallow return False def user_can_edit(self, user): # disallow on deleted/expired polls if self.poll.deleted or self.poll.is_expired: return False # allow for poll author if self.poll.user_can_administrate(user): return True # allow for admin if user.is_authenticated() and user.is_admin: return True # allow for creator if self.user: return user == self.user # allow everyone, if no creator return True @property def displayname(self): return "anonymous" if self.anonymous else ( self.user.displayname if self.user else (self.name or "unknown")) def to_dict(self): return dict( id=self.id, displayname=self.displayname, name=self.name if not self.anonymous else None, user_id=self.user_id if not self.anonymous else 0, vote_choices={vc.id: vc.to_dict() for vc in self.vote_choices}, anonymous=self.anonymous)
class Poll(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(80)) description = db.Column(db.Text) slug = db.Column(db.String(80)) type = db.Column(db.String(20), default="normal") # PollType created = db.Column(db.DateTime) owner_id = db.Column(db.Integer, db.ForeignKey("member.id")) # === Extra settings === due_date = db.Column(db.DateTime) anonymous_allowed = db.Column(db.Boolean, default=True) public_listing = db.Column(db.Boolean, default=False) require_login = db.Column(db.Boolean, default=False) require_invitation = db.Column(db.Boolean, default=False) show_results = db.Column( db.String(20), default="complete" ) # summary|complete|never|summary_after_vote|complete_after_vote send_mail = db.Column(db.Boolean, default=False) one_vote_per_user = db.Column(db.Boolean, default=True) allow_comments = db.Column(db.Boolean, default=True) deleted = db.Column(db.Boolean, default=False) show_invitations = db.Column(db.Boolean, default=True) timezone_name = db.Column(db.String(40)) # Type: numeric amount_minimum = db.Column(db.Float, default=0) amount_maximum = db.Column(db.Float, default=None) amount_step = db.Column(db.Float, default=1) RESERVED_NAMES = [ "login", "logout", "index", "user", "admin", "api", "register", "static" ] # relationships choices = db.relationship("Choice", backref="poll", cascade="all, delete-orphan", lazy="dynamic") choice_values = db.relationship("ChoiceValue", backref="poll", lazy="dynamic") watchers = db.relationship("PollWatch", backref="poll", cascade="all, delete-orphan", lazy="dynamic") comments = db.relationship("Comment", backref="poll", cascade="all, delete-orphan", lazy="dynamic") votes = db.relationship("Vote", backref="poll", cascade="all, delete-orphan", lazy="dynamic") invitations = db.relationship("Invitation", backref="poll", cascade="all, delete-orphan", lazy="dynamic") _vote_choice_map = None _choices = None def __init__(self, title=None, slug=None, type=None, create_choice_values=True): if title: self.title = title if slug: self.slug = slug if type: self.type = type self.created = datetime.utcnow() # create yes/no/maybe default choice values if create_choice_values: self.choice_values.append(ChoiceValue("yes", "check", "9C6", 1.0)) self.choice_values.append(ChoiceValue("no", "ban", "F96", 0.0)) self.choice_values.append( ChoiceValue("maybe", "question", "FF6", 0.5)) @property def is_expired(self): return self.due_date and self.due_date < datetime.utcnow() def numeric_format(self, extra_digits=0): if not self.type == PollType.numeric: raise TypeError("Poll %s is not of type numeric." % self.slug) def get_decimal_places(val): decimals = str(val).rstrip("0").split(".") return len(decimals[1]) if (len(decimals) > 1) else 0 return "%%.%df" % (max( map(get_decimal_places, [self.amount_step, self.amount_maximum, self.amount_minimum])) + extra_digits) def invite(self, user): result = True invitation = Invitation.query.filter_by(poll_id=self.id, user_id=user.id).first() if invitation: return False invitation = Invitation() invitation.user = user invitation.poll = self if current_user.is_authenticated: invitation.creator = current_user vote = Vote.query.filter_by(poll_id=self.id, user_id=user.id).first() if vote: # create an invitation, just to track them invitation.vote = vote result = False db.session.add(invitation) invitation.send_mail() return result def invite_all(self, users): invited = [] failed = [] users = list(set(users)) for user in users: if not user: continue if self.invite(user): invited.append(user) else: failed.append(user) return invited, failed def show_votes(self, user): return self.user_can_administrate(user) \ or self.show_results == "complete" \ or (self.show_results == "complete_after_vote" and self.is_expired) def show_summary(self, user): return self.user_can_administrate(user) \ or self.show_votes(user) \ or self.show_results == "summary" \ or (self.show_results == "summary_after_vote" and self.is_expired) @property def has_choices(self): return len(self.get_choices()) > 0 def get_url(self): return url_for("poll", slug=self.slug) def get_vote_choice(self, vote, choice): if not self._vote_choice_map: self._vote_choice_map = { vote: { vote_choice.choice: vote_choice for vote_choice in vote.vote_choices } for vote in self.votes } try: return self._vote_choice_map[vote][choice] except KeyError: return None # return VoteChoice.query.filter_by(vote=vote, choice=choice).first() def get_choices(self): if self._choices is None: self._choices = Choice.query.filter_by(poll_id=self.id, deleted=False).all() self._choices.sort(key=Choice.get_hierarchy) return self._choices def get_choice_values(self): return ChoiceValue.query.filter_by(poll_id=self.id, deleted=False).all() def get_choice_by_id(self, id): return Choice.query.filter_by(poll_id=self.id, id=id).first() def get_choice_dates(self): return list(set(choice.date.date() for choice in self.get_choices())) def get_choice_times(self): return list(set(choice.date.time() for choice in self.get_choices())) def has_choice_date_time(self, date, time): dt = datetime.combine(date, time) return [ choice for choice in self.get_choices() if choice.date == dt and not choice.deleted ] def user_can_administrate(self, user): # Owners may administrate if self.owner and user.is_authenticated and user.is_member(self.owner): return True # Admins may administrate if (user.is_authenticated and user.is_admin): return True # Everyone else may not return False def user_can_edit(self, user): # If no owner is set, everyone may edit if not self.owner: return True # Owners may edit if user.is_authenticated and user.is_member(self.owner): return True # Admins may edit if user.is_authenticated and user.is_admin: return True # Everyone else may not return False def get_user_votes(self, user): return [] if user.is_anonymous else Vote.query.filter_by( poll=self, user=user).all() def check_expiry(self): if self.is_expired: raise PollExpiredException(self) def check_edit_permission(self): if not self.user_can_edit(current_user): raise PollActionException(self, lazy_gettext("edit")) # returns a list of groups # each group is sorted # the groups are sorted by first item def get_choice_groups(self): groups = {} for choice in self.get_choices(): hierarchy = choice.get_hierarchy() group = groups for part in hierarchy[:-1]: if not part in group: group[part] = {} group = group[part] group[hierarchy[-1]] = choice return groups def choice_groups_valid(self, left_hierarchy, ignore_id=None): for right in self.get_choices(): if ignore_id is not None and right.id == ignore_id: continue right_hierarchy = right.get_hierarchy() smaller = min([len(left_hierarchy), len(right_hierarchy)]) if left_hierarchy[:smaller] == right_hierarchy[:smaller]: return False return True # Weird algorithm. Required for poll.jade and vote.jade def get_choice_group_matrix(self): matrix = [choice.get_hierarchy() for choice in self.get_choices()] matrix = [[[item, 1, 1] for item in row] for row in matrix] width = max(len(row) for row in matrix) def fill(row, length): if len(row) >= length: return row.append([None, 1, 1]) fill(row, length) for row in matrix: fill(row, width) # Merge None left to determine depth for i in range(width - 1, 0, -1): for row in matrix: if row[i][0] is None: row[i - 1][1] = row[i][1] + 1 # Merge items up and replace by None for i in range(len(matrix) - 1, 0, -1): for j in range(width): if matrix[i][j][0] == matrix[ i - 1][j][0] and matrix[i][j][1] == matrix[i - 1][j][1]: matrix[i - 1][j][2] = matrix[i][j][2] + 1 matrix[i][j][0] = None # cut off time column in day mode, only use date field if self.type == PollType.date: matrix = [[row[0]] for row in matrix] return matrix def get_choices_by_group(self, group): return [ choice for choice in self.get_choices() if choice.group == group ] def get_comments(self): return Comment.query.filter_by(poll=self, deleted=False).order_by( db.asc(Comment.created)).all() def fill_vote_form(self, form): while form.vote_choices: form.vote_choices.pop_entry() for choice in self.get_choices(): form.vote_choices.append_entry(dict(choice_id=choice.id)) for subform in form.vote_choices: subform.value.choices = [(v.id, v.title) for v in self.get_choice_values()] def get_stats(self): if self.type == PollType.numeric: totals = {} counts = {} averages = {} for choice in self.choices: amounts = [ vote_choice.amount for vote_choice in choice.vote_choices ] counts[choice] = len(amounts) totals[choice] = sum(amounts) averages[ choice] = totals[choice] / counts[choice] if amounts else 0 return totals, counts, averages else: counts = {} for choice in self.choices: counts[choice] = choice.get_counts() scores = {} totals = {} for choice, choice_counts in counts.items(): totals[choice] = choice.vote_choices.count() scores[choice] = 0 for value, count in choice_counts.items(): scores[choice] += count * value.weight max_score = max(scores.values()) return scores, counts, totals, max_score def is_watching(self, user): return PollWatch.query.filter_by(poll=self, user=user).first() def send_watchers(self, subject, template, **kwargs): with mail.record_messages() as outbox: with mail.connect() as conn: for watcher in self.watchers: user = watcher.user msg = Message(recipients=[user.email], subject=subject, body=render_template(template, poll=self, user=user, **kwargs)) conn.send(msg) def to_dict(self): dictify = lambda l, f=(lambda x: True ): {i.id: i.to_dict() for i in l if f(i)} return dict(choices=dictify(self.choices, lambda c: not c.deleted), choice_values=dictify(self.choice_values, lambda c: not c.deleted), votes=dictify(self.votes), comments=dictify(self.comments, lambda c: not c.deleted), choice_groups=[[choice.id for choice in group] for group in self.get_choice_groups()]) @property def last_changed(self): dates = [] dates.append(self.created) if self.due_date: dates.append(self.due_date) for vote in self.votes: dates.append(vote.created) for comment in self.comments: dates.append(comment.created) dates = [date for date in dates if date] return max(dates) @property def timezone(self): return timezone(self.timezone_name) if self.timezone_name else None @property def localization_context(self): return LocalizationContext(current_user, self) @property def should_auto_delete(self): return self.last_changed + timedelta(days=60) < datetime.utcnow() def get_choice_range(self): values = [choice.value for choice in self.get_choices()] if not values: return None, None out = min(values), max(values) if self.type == PollType.datetime: out = [v.datetime for v in out] return out def get_mac(self, user_id=None): """ Generates a mac for actions on a poll :param user_id: The id from the user the mac is for. If it is None the current_user.id is used :return: String """ if not user_id: user_id = current_user.id to_sign = '{}/{}'.format(self.slug, user_id) return hmac.new(app.config['SECRET_KEY'], to_sign).hexdigest()
class Choice(db.Model): id = db.Column(db.Integer, primary_key=True) text = db.Column(db.String(80)) date = db.Column(db.DateTime) poll_id = db.Column(db.Integer, db.ForeignKey("poll.id")) deleted = db.Column(db.Boolean, default=False) # relationships vote_choices = db.relationship("VoteChoice", backref="choice", lazy="dynamic") def __init__(self, content=None, poll=None): if content: if isinstance(content, str): self.text = content elif isinstance(content, datetime): self.date = content else: raise ValueError("Invalid content type for choice: %s" % type(content)) if poll: self.poll = poll def __cmp__(self, other): return cmp(self.date, other.date) or cmp( self.deleted, other.deleted) or cmp(self.text, other.text) def get_counts(self): counts = {vc: 0 for vc in self.poll.choice_values} for vote_choice in self.vote_choices: if vote_choice.value: counts[vote_choice.value] += 1 return counts def get_hierarchy(self): if self.date: return [ PartialDateTime(self.date, DateTimePart.date, self.poll.localization_context), PartialDateTime(self.date, DateTimePart.time, self.poll.localization_context) ] else: return [part.strip() for part in self.text.split("/") if part] def to_dict(self): return dict(id=self.id, text=self.text, date=str(self.date), deleted=self.deleted) @property def title(self): from dudel.filters import date, datetime from dudel.models.poll import PollType if self.poll.type == PollType.date: return date(self.date, ref=self.poll) elif self.poll.type == PollType.datetime: return datetime(self.date, ref=self.poll.localization_context) else: return Markup( '<i class="fa fa-chevron-right choice-separator"></i>').join( self.get_hierarchy()) # return self.text @property def value(self): from dudel.models.poll import PollType if self.poll.type == PollType.date: return PartialDateTime(self.date, DateTimePart.date, self.poll.localization_context) elif self.poll.type == PollType.datetime: return self.date else: return self.text def copy(self): n = Choice() n.text = self.text n.date = self.date n.poll = self.poll n.deleted = self.deleted return n
class User(Member): id = db.Column(db.Integer, db.ForeignKey("member.id"), primary_key=True) firstname = db.Column(db.String(80)) lastname = db.Column(db.String(80)) username = db.Column(db.String(80)) password = db.Column(db.LargeBinary) _displayname = db.Column(db.String(80)) email = db.Column(db.String(80)) preferred_language = db.Column(db.String(80)) autowatch = db.Column(db.Boolean, default=False) allow_invitation_mails = db.Column(db.Boolean, default=True) timezone_name = db.Column(db.String(40)) # relationships watches = db.relationship("PollWatch", backref="user", cascade="all, delete-orphan", lazy="dynamic") groups_admin = db.relationship("Group", backref="admin", lazy="dynamic", foreign_keys=[Group.admin_id]) comments = db.relationship("Comment", backref="user", lazy="dynamic") invitations = db.relationship("Invitation", backref="user", lazy="dynamic", foreign_keys=[Invitation.user_id]) invitations_created = db.relationship("Invitation", backref="creator", lazy="dynamic", foreign_keys=[Invitation.creator_id]) votes = db.relationship("Vote", backref="user", lazy="dynamic", foreign_keys=[Vote.user_id]) votes_assigned = db.relationship("Vote", backref="assigned_by", lazy="dynamic", foreign_keys=[Vote.assigned_by_id]) __mapper_args__ = { 'polymorphic_identity': 'user', } def __init__(self, username=None, firstname=None, lastname=None, email=None, password=None): self.username = username self.firstname = firstname self.lastname = lastname self.email = email self.set_password(password) @property def displayname(self): return self._displayname or ( (app.config["NAME_FORMAT"] if "NAME_FORMAT" in app.config else "%(firstname)s (%(username)s)") % { "firstname": self.firstname, "lastname": self.lastname, "username": self.username, "email": self.email }) @property def timezone(self): return pytz.timezone( self.timezone_name) if self.timezone_name else default_timezone # login stuff def get_id(self): return self.username def is_active(self): return True def is_anonymous(self): return False def is_authenticated(self): return True @property def is_admin(self): return "ADMINS" in app.config and self.username in app.config["ADMINS"] def require_admin(self): if not self.is_authenticated() or not self.is_admin: abort(403) def set_password(self, password): self.password = hash_password(password.encode("ascii")) def get_avatar(self, size): return gravatar(self.email, size) def is_member(self, of): if not of: return False if of.type == "user": return of == self else: return self in of.users @property def poll_list(self): from dudel.models.poll import Poll from dudel.models.group import Group, group_users # Polls I watched watched = [watch.poll for watch in self.watches] # Owned by myself owned = self.polls.filter_by(deleted=False).all() # Owned by groups I am member of owned += Poll.query.filter_by( deleted=False).join(Group).join(group_users).filter_by( user_id=self.id).all() # Polls I voted on voted = [vote.poll for vote in self.votes] # Polls I am invited to invited = [invite.poll for invite in self.invitations] # All of them, filtered, without duplicates, sorted my_polls = watched + owned + voted + invited my_polls = [poll for poll in my_polls if not poll.deleted] my_polls = list(set(my_polls)) my_polls.sort(key=lambda x: x.created, reverse=True) return my_polls def __repr__(self): return "<User:%s>" % self.displayname def is_invited(self, poll): return self.invitations.filter_by(poll_id=poll.id).count() > 0