Example #1
0
class Vote(db.Model, DeleteMixin):
    __tablename__ = 'vote'

    user_id = db.Column(UUID,
                        db.ForeignKey('user.id', ondelete='CASCADE'),
                        primary_key=True)
    revision_id = db.Column(db.Integer,
                            db.ForeignKey('revision.id', ondelete='CASCADE'),
                            primary_key=True)
    vote = db.Column(db.Boolean, nullable=False)
    rated_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)

    @classmethod
    def create(cls, user, review, vote):
        """Create new vote for the latest revision of a specified review."""
        # Deleting the vote from the last revision if it exists
        user.display_name, cls.query.filter_by(
            user=user, revision=review.last_revision).delete()
        # Creating a new vote for the last revision
        vote_obj = cls(user=user, revision=review.last_revision, vote=vote)
        db.session.add(vote_obj)
        db.session.commit()
        return vote_obj

    def to_dict(self):
        response = dict(vote=self.vote, voted_at=self.rated_at)
        return response
Example #2
0
class OAuthGrant(db.Model, DeleteMixin):
    __tablename__ = 'oauth_grant'

    id = db.Column(db.Integer, primary_key=True)
    client_id = db.Column(db.Unicode, db.ForeignKey('oauth_client.client_id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False)
    code = db.Column(db.Unicode, index=True, nullable=False)
    expires = db.Column(db.DateTime, nullable=False)
    redirect_uri = db.Column(db.UnicodeText, nullable=False)
    scopes = db.Column(db.UnicodeText)

    # Resource owner
    user_id = db.Column(UUID, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)

    def get_scopes(self):
        if hasattr(self, '_scopes') is False:
            self._scopes = self.scopes.split()
        return self._scopes
Example #3
0
class Revision(db.Model, DeleteMixin):
    __tablename__ = 'revision'

    id = db.Column(db.Integer, primary_key=True)
    review_id = db.Column(UUID,
                          db.ForeignKey('review.id', ondelete='CASCADE'),
                          index=True)
    timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    text = db.Column(db.Unicode, nullable=False)

    _votes = db.relationship('Vote',
                             cascade='delete',
                             lazy='dynamic',
                             backref='revision')
    _spam_reports = db.relationship('SpamReport',
                                    cascade='delete',
                                    lazy='dynamic',
                                    backref='revision')

    @property
    def votes_positive_count(self):
        if hasattr(self, '_votes_positive_count') is False:
            self._votes_positive_count = self._votes.filter_by(
                vote=True).count()
        return self._votes_positive_count

    @property
    def votes_negative_count(self):
        if hasattr(self, '_votes_negative_count') is False:
            self._votes_negative_count = self._votes.filter_by(
                vote=False).count()
        return self._votes_negative_count

    def to_dict(self):
        response = dict(id=self.id,
                        review_id=self.review_id,
                        timestamp=self.timestamp,
                        votes_positive=self.votes_positive_count,
                        votes_negative=self.votes_negative_count,
                        text=self.text)
        return response

    @classmethod
    def create(cls, review_id, text):
        revision = cls(review_id=review_id, text=text)
        db.session.add(revision)
        db.session.commit()
        return revision
Example #4
0
class SpamReport(db.Model, DeleteMixin):
    __tablename__ = 'spam_report'

    user_id = db.Column(UUID,
                        db.ForeignKey('user.id', ondelete='CASCADE'),
                        primary_key=True)
    reason = db.Column(db.Unicode)
    revision_id = db.Column(db.Integer,
                            db.ForeignKey('revision.id', ondelete='CASCADE'),
                            primary_key=True)
    reported_at = db.Column(db.DateTime,
                            default=datetime.utcnow,
                            nullable=False)
    is_archived = db.Column(db.Boolean, nullable=False, default=False)

    @classmethod
    def get(cls, **kwargs):
        return cls.query.filter_by(**kwargs).first()

    @property
    def review(self):
        return Review.get(id=self.revision.review_id)

    @classmethod
    def create(cls, revision_id, user, reason):
        report = cls(user=user, revision_id=revision_id, reason=reason)
        db.session.add(report)
        db.session.commit()
        return report

    def archive(self):
        self.is_archived = True
        db.session.commit()

    @classmethod
    def list(cls, **kwargs):
        """Get a list of reports.

        Args:
            review_id: UUID of the review that is associated with the report.
            user_id: UUID of the user who created the report.
            limit: Maximum number of reviews returned by this method.
            offset: Offset that can be used in conjunction with the limit.

        Returns:
            Pair of values: list of report that match applied filters and
            total number of reports.
        """

        query = SpamReport.query
        inc_archived = kwargs.pop('inc_archived', None)
        if not inc_archived:
            query = query.filter(SpamReport.is_archived == False)

        review_id = kwargs.pop('review_id', None)
        if review_id is not None:
            revision_ids = db.session.query(
                Revision.id).filter_by(review_id=review_id)
            query = SpamReport.query.filter(
                SpamReport.revision_id.in_(revision_ids))

        user_id = kwargs.pop('user_id', None)
        if user_id is not None:
            query = query.filter(SpamReport.user_id == user_id)

        count = query.count()

        query = query.order_by(desc(SpamReport.reported_at))

        limit = kwargs.pop('limit', None)
        if limit is not None:
            query = query.limit(limit)

        offset = kwargs.pop('offset', None)
        if offset is not None:
            query = query.offset(offset)

        if kwargs:
            raise TypeError('Unexpected **kwargs: %r' % kwargs)

        return query.all(), count
Example #5
0
class Review(db.Model, DeleteMixin):
    __tablename__ = 'review'
    CACHE_NAMESPACE = 'Review'

    id = db.Column(UUID,
                   primary_key=True,
                   server_default=db.text('uuid_generate_v4()'))
    entity_id = db.Column(UUID, index=True, nullable=False)
    entity_type = db.Column(db.Enum(*ENTITY_TYPES, name='entity_types'),
                            nullable=False)
    user_id = db.Column(UUID,
                        db.ForeignKey('user.id', ondelete='CASCADE'),
                        nullable=False)
    edits = db.Column(db.Integer, nullable=False, default=0)
    is_draft = db.Column(db.Boolean, nullable=False, default=False)
    is_hidden = db.Column(db.Boolean, nullable=False, default=False)
    license_id = db.Column(db.Unicode,
                           db.ForeignKey('license.id', ondelete='CASCADE'),
                           nullable=False)
    language = db.Column(db.String(3), default='en', nullable=False)
    source = db.Column(db.Unicode)
    source_url = db.Column(db.Unicode)

    revisions = db.relationship('Revision',
                                order_by='Revision.timestamp',
                                backref='review',
                                cascade='delete')

    __table_args__ = (db.UniqueConstraint('entity_id', 'user_id'), )

    def to_dict(self, confidential=False):
        return dict(
            id=self.id,
            entity_id=self.entity_id,
            entity_type=self.entity_type,
            user=self.user.to_dict(confidential=confidential),
            text=self.text,
            created=self.revisions[0].timestamp,
            last_updated=self.revisions[-1].timestamp,
            edits=self.edits,
            votes_positive=self.votes_positive_count,
            votes_negative=self.votes_negative_count,
            rating=self.rating,
            license=self.license.to_dict(),
            language=self.language,
            source=self.source,
            source_url=self.source_url,
        )

    @classmethod
    def get(cls, **kwargs):
        return cls.query.filter_by(**kwargs).first()

    @property
    def last_revision(self):
        """Returns latest revision of this review."""
        return self.revisions[-1]

    @property
    def text(self):
        """Returns text of the latest revision."""
        return self.last_revision.text  # latest revision

    @hybrid_property
    def created(self):
        """Returns creation time of this review (first revision)."""
        if self.revisions:
            return self.revisions[0].timestamp
        else:
            return None

    @created.expression
    def created(cls):
        return Revision.timestamp

    @property
    def votes_positive_count(self):
        return self.last_revision.votes_positive_count

    @property
    def votes_negative_count(self):
        return self.last_revision.votes_negative_count

    @property
    def rating(self):
        if hasattr(self, '_rating') is False:
            self._rating = self.votes_positive_count - self.votes_negative_count
        return self._rating

    def hide(self):
        self.is_hidden = True
        db.session.commit()

    def unhide(self):
        self.is_hidden = False
        db.session.commit()

    @classmethod
    def list(cls, **kwargs):
        """Get a list of reviews.

        This method provides several filters that can be used to select
        specific reviews. See argument description below for more info.

        Args:
            entity_id: MBID of the entity that is associated with a
                review.
            entity_type: One of the supported reviewable entities. 'release_group'
                or 'event' etc.
            user_id: UUID of the author.
            sort: Order of returned reviews. Can be either "rating" (order by
                rating), or "created" (order by creation time), or "random" (order randomly)
            limit: Maximum number of reviews returned by this method.
            offset: Offset that can be used in conjunction with the limit.
            language: Language (code) of returned reviews.
            licence_id: License of returned reviews.
            inc_drafts: True if reviews marked as drafts should be included,
                False if not.
            inc_hidden: True if reviews marked as hidden should be included,
                False if not.
            exclude: List of id of reviews to exclude.

        Returns:
            Pair of values: list of reviews that match applied filters and
            total number of reviews.
        """
        query = Review.query
        inc_drafts = kwargs.pop('inc_drafts', None)
        if not inc_drafts:
            query = query.filter(Review.is_draft == False)

        inc_hidden = kwargs.pop('inc_hidden', None)
        if not inc_hidden:
            query = query.filter(Review.is_hidden == False)

        # FILTERING:

        entity_id = kwargs.pop('entity_id', None)
        if entity_id is not None:
            query = query.filter(Review.entity_id == entity_id)

        entity_type = kwargs.pop('entity_type', None)
        if entity_type is not None:
            query = query.filter(Review.entity_type == entity_type)

        language = kwargs.pop('language', None)
        if language is not None:
            query = query.filter(Review.language == language)

        license_id = kwargs.pop('license_id', None)
        if license_id is not None:
            query = query.filter(Review.license_id == license_id)

        user_id = kwargs.pop('user_id', None)
        if user_id is not None:
            query = query.filter(Review.user_id == user_id)

        exclude = kwargs.pop('exclude', None)
        if exclude is not None:
            query = query.filter(Review.id.notin_(exclude))

        count = query.count(
        )  # Total count should be calculated before limits and sorting

        # SORTING:
        sort = kwargs.pop('sort', None)

        if sort == 'rating':  # order by rating (positive votes - negative votes)

            # TODO(roman): Simplify this part. It can probably be rewritten using
            # hybrid attributes (by making rating property a hybrid_property),
            # but I'm not sure how to do that.

            # Preparing base query for getting votes
            vote_query_base = db.session.query(
                Vote.revision_id,  # revision associated with a vote
                Vote.vote,  # vote itself (True if positive, False if negative)
                func.count().label('c')  # number of votes
            ).group_by(Vote.revision_id, Vote.vote)

            # Getting positive votes
            votes_pos = vote_query_base.subquery('votes_pos')
            query = query.outerjoin(Revision).outerjoin(
                votes_pos,
                and_(votes_pos.c.revision_id == Revision.id,
                     votes_pos.c.vote == True))

            # Getting negative votes
            votes_neg = vote_query_base.subquery('votes_neg')
            query = query.outerjoin(Revision).outerjoin(
                votes_neg,
                and_(votes_neg.c.revision_id == Revision.id,
                     votes_neg.c.vote == False))

            query = query.order_by(
                desc(
                    func.coalesce(votes_pos.c.c, 0) -
                    func.coalesce(votes_neg.c.c, 0)))

        elif sort == 'created':  # order by creation time
            # Getting publication times for all reviews
            pub_times = db.session.query(
                func.min(Revision.timestamp).label('published_on'),
                Revision.review_id,
            ).group_by(Revision.review_id).subquery('pub_times')

            # Joining and sorting by publication time
            query = query.outerjoin(pub_times).order_by(
                desc('pub_times.published_on'))

        elif sort == 'random':  # order randomly
            query = query.order_by(func.random())

        limit = kwargs.pop('limit', None)
        if limit is not None:
            query = query.limit(limit)

        offset = kwargs.pop('offset', None)
        if offset is not None:
            query = query.offset(offset)

        if kwargs:
            raise TypeError('Unexpected **kwargs: %r' % kwargs)

        return query.all(), count

    @classmethod
    def get_count(cls, **kwargs):
        review_count = 0
        if ('is_draft' in kwargs):
            is_drafted = kwargs.pop('is_draft', False)
            review_count += cls.query.filter(
                cls.is_draft == is_drafted).count()
        if ('is_hidden' in kwargs):
            is_hidden = kwargs.pop('is_hidden', False)
            review_count += cls.query.filter(
                cls.is_hidden == is_hidden).count()
        if (kwargs):
            raise TypeError('Unexpected **kwargs: %r' % kwargs)
        return review_count

    @classmethod
    def create(cls, **kwargs):
        if 'release_group' in kwargs:
            entity_id = kwargs.pop('release_group')
            entity_type = 'release_group'
        else:
            entity_id = kwargs.pop('entity_id')
            entity_type = kwargs.pop('entity_type')

        review = Review(entity_id=entity_id,
                        entity_type=entity_type,
                        user_id=kwargs.pop('user_id'),
                        language=kwargs.pop('language', None),
                        is_draft=kwargs.pop('is_draft', False),
                        license_id=kwargs.pop('license_id',
                                              DEFAULT_LICENSE_ID),
                        source=kwargs.pop('source', None),
                        source_url=kwargs.pop('source_url', None))
        db.session.add(review)
        db.session.flush()
        db.session.add(Revision(review_id=review.id, text=kwargs.pop('text')))

        if kwargs:
            db.session.rollback()
            raise TypeError('Unexpected **kwargs: %r' % kwargs)

        db.session.commit()
        cache.invalidate_namespace(Review.CACHE_NAMESPACE)

        return review

    def update(self, **kwargs):
        """Update contents of this review.

        Returns:
            New revision of this review.
        """
        license_id = kwargs.pop('license_id', None)
        if license_id is not None:
            if not self.is_draft:  # If trying to convert published review into draft.
                raise BadRequest(
                    lazy_gettext(
                        "Changing license of a published review is not allowed."
                    ))
            self.license_id = license_id

        language = kwargs.pop('language', None)
        if language is not None:
            self.language = language

        is_draft = kwargs.pop('is_draft', None)
        if is_draft is not None:  # This should be done after all changes that depend on review being a draft.
            if not self.is_draft and is_draft:  # If trying to convert published review into draft.
                raise BadRequest(
                    lazy_gettext(
                        "Converting published reviews back to drafts is not allowed."
                    ))
            self.is_draft = is_draft

        new_revision = Revision.create(self.id, kwargs.pop('text'))
        cache.invalidate_namespace(Review.CACHE_NAMESPACE)

        if kwargs:
            # FIXME: Revision creation and other changes need to be rolled back
            # there, but there's a `commit` in Revision.create.
            raise TypeError('Unexpected **kwargs: %r' % kwargs)

        return new_revision

    @classmethod
    def get_popular(cls, limit=None):
        """Get list of popular reviews.

        Popularity is determined by rating of a particular review. Rating is a
        difference between positive votes and negative. In this case only votes
        from the last month are used to calculate rating.

        Results are cached for 12 hours.

        Args:
            limit: Maximum number of reviews to return.

        Returns:
            Randomized list of popular reviews which are converted into
            dictionaries using to_dict method.
        """
        cache_key = cache.gen_key('popular_reviews', limit)
        reviews = cache.get(cache_key, Review.CACHE_NAMESPACE)

        if not reviews:
            # Selecting reviews for distinct release groups
            # TODO(roman): The is a problem with selecting popular reviews like
            # this: if there are multiple reviews for a release group we don't
            # choose the most popular.
            distinct_subquery = db.session.query(Review) \
                .filter(Review.is_draft == False) \
                .distinct(Review.entity_id).subquery()

            # Randomizing results to get some variety
            rand_subquery = db.session.query(aliased(Review, distinct_subquery)) \
                .order_by(func.random()).subquery()

            # Sorting reviews by rating
            query = db.session.query(aliased(Review, rand_subquery))

            # Preparing base query for getting votes
            vote_query_base = db.session.query(
                Vote.revision_id, Vote.vote, func.count().label('c')) \
                .group_by(Vote.revision_id, Vote.vote) \
                .filter(Vote.rated_at > datetime.now() - timedelta(weeks=4))

            # Getting positive votes
            votes_pos = vote_query_base.subquery('votes_pos')
            query = query.outerjoin(Revision).outerjoin(
                votes_pos,
                and_(votes_pos.c.revision_id == Revision.id,
                     votes_pos.c.vote == True))

            # Getting negative votes
            votes_neg = vote_query_base.subquery('votes_neg')
            query = query.outerjoin(Revision).outerjoin(
                votes_neg,
                and_(votes_neg.c.revision_id == Revision.id,
                     votes_neg.c.vote == False))

            query = query.order_by(
                desc(
                    func.coalesce(votes_pos.c.c, 0) -
                    func.coalesce(votes_neg.c.c, 0)))

            if limit is not None:
                # Selecting more reviews there so we'll have something
                # different to show (shuffling is done below).
                query = query.limit(limit * 4)

            reviews = query.all()
            reviews = [review.to_dict(confidential=True) for review in reviews]
            cache.set(cache_key, reviews, 1 * 60 * 60,
                      Review.CACHE_NAMESPACE)  # 1 hour

        shuffle(reviews)  # a bit more variety
        return reviews[:limit]
class ModerationLog(db.Model, DeleteMixin):
    __tablename__ = 'moderation_log'

    id = db.Column(db.Integer, primary_key=True)
    admin_id = db.Column(UUID,
                         db.ForeignKey('user.id', ondelete='CASCADE'),
                         nullable=False)
    user_id = db.Column(UUID, db.ForeignKey('user.id', ondelete='CASCADE'))
    review_id = db.Column(UUID, db.ForeignKey('review.id', ondelete='CASCADE'))
    action = db.Column(db.Enum(ACTION_HIDE_REVIEW,
                               ACTION_BLOCK_USER,
                               name='action_types'),
                       nullable=False)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
    reason = db.Column(db.Unicode, nullable=False)

    @property
    def user(self):
        return User.get(id=self.user_id)

    @property
    def admin(self):
        return User.get(id=self.admin_id)

    @property
    def review(self):
        return Review.get(id=self.review_id)

    @classmethod
    def get(cls, **kwargs):
        return cls.query.filter_by(**kwargs).first()

    @classmethod
    def create(cls, admin_id, action, reason, user_id=None, review_id=None):
        if user_id:
            log = cls(admin_id=str(admin_id),
                      action=action,
                      reason=reason,
                      user_id=str(user_id))
        elif review_id:
            log = cls(admin_id=str(admin_id),
                      action=action,
                      reason=reason,
                      review_id=str(review_id))
        db.session.add(log)
        db.session.commit()
        return log

    @classmethod
    def list(cls, **kwargs):
        """Get a list of log entries.

        Args:
            admin_id: UUID of the admin whose actions generated the log.
            limit: Maximum number of reviews returned by this method.
            offset: Offset that can be used in conjunction with the limit.

        Returns:
            Pair of values: list of log entries that match applied filters and
            total number of log entries.
        """

        query = ModerationLog.query

        admin_id = kwargs.pop('admin_id', None)
        if admin_id is not None:
            query = query.filter(ModerationLog.admin_id == admin_id)

        count = query.count()

        query = query.order_by(desc(ModerationLog.timestamp))

        limit = kwargs.pop('limit', None)
        if limit is not None:
            query = query.limit(limit)

        offset = kwargs.pop('offset', None)
        if offset is not None:
            query = query.offset(offset)

        if kwargs:
            raise TypeError('Unexpected **kwargs: %r' % kwargs)

        return query.all(), count
Example #7
0
class OAuthClient(db.Model, DeleteMixin):
    __tablename__ = 'oauth_client'

    client_id = db.Column(db.Unicode, primary_key=True)
    client_secret = db.Column(db.Unicode, nullable=False)
    redirect_uri = db.Column(db.UnicodeText, nullable=False)

    user_id = db.Column(UUID, db.ForeignKey('user.id', ondelete='CASCADE'))
    name = db.Column(db.Unicode, nullable=False)
    desc = db.Column(db.Unicode, nullable=False)
    website = db.Column(db.Unicode, nullable=False)

    grants = db.relationship('OAuthGrant', cascade='all', backref='client')
    tokens = db.relationship('OAuthToken', cascade='all', backref='client')

    allowed_includes = []

    @classmethod
    def create(cls, user, name, desc, website, redirect_uri):
        """Creates new OAuth client and generates a secret key for it.

        Args:
            user: User who manages the client.
            name: Name of the client.
            desc: Client description.
            website: Client web site.
            redirect_uri: URI where responses will be sent.

        Returns:
            New OAuth client.
        """
        client_id = generate_string(20)
        client_secret = generate_string(40)
        client = cls(client_id=client_id, client_secret=client_secret,
                     user=user, name=name, desc=desc, website=website,
                     redirect_uri=redirect_uri)
        db.session.add(client)
        db.session.commit()
        return client

    def to_dict(self):
        return dict(
            client_id=self.client_id,
            client_secret=self.client_secret,
            user_id=self.user_id,
            name=self.name,
            desc=self.desc,
            website=self.website,
            redirect_uri=self.redirect_uri
        )

    def update(self, name=None, desc=None, website=None, redirect_uri=None):
        if name is not None:
            self.name = name
        if desc is not None:
            self.desc = desc
        if website is not None:
            self.website = website
        if redirect_uri is not None:
            self.redirect_uri = redirect_uri
        db.session.commit()