コード例 #1
0
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
    }
コード例 #2
0
ファイル: group.py プロジェクト: opatut/dudel
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"
コード例 #3
0
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
コード例 #4
0
ファイル: vote.py プロジェクト: RalfJung/dudel
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)
コード例 #5
0
ファイル: poll.py プロジェクト: opatut/dudel
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()
コード例 #6
0
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
コード例 #7
0
ファイル: user.py プロジェクト: RalfJung/dudel
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