Пример #1
0
class RegisterResource(Resource):
    @use_args({
        'username': fields.Str(required=True, location='form'),
        'password': fields.Str(required=True, location='form', validate=validate.Length(min=1)),
        'format': fields.Str(missing='json', location='query', validate=validate.Equal('json'))
    })
    def post(self, args):
        user = User(username=args['username'], password=args['password'])
        db.session.add(user)
        try:
            db.session.commit()
        except IntegrityError:
            db.session.rollback()
            return custom_error('username', ['Username already in use']), ErrorCode.BAD_REQUEST

        return {'message': 'OK'}
Пример #2
0
class LoginResource(Resource):
    @use_args({
        'username': fields.Str(required=True, location='form'),
        'password': fields.Str(required=True, location='form'),
        'format': fields.Str(missing='json', location='query', validate=validate.Equal('json'))
    })
    def post(self, args):
        user = User.query.filter(User.username == args['username']).first()
        if not user:
            return custom_error('username', ['Invalid username']), ErrorCode.BAD_REQUEST
        elif not user.verify_password(args['password']):
            return custom_error('password', ['Wrong password']), ErrorCode.BAD_REQUEST

        _login(user)

        return {'token': user.token}
Пример #3
0
class StatsResource(Resource):
    @requires_auth
    @use_args({
        'dateFrom':
        fields.Date(required=True, location='query'),
        'dateTo':
        fields.Date(required=True, location='query'),
        'product':
        fields.Int(required=True, location='query'),
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def get(self, args, **kwargs):
        query = db.session.query(Price.date, func.min(Price.price), func.avg(Price.price), func.max(Price.price)).\
            filter(Price.product_id == args['product'], Price.date.between(args['dateFrom'], args['dateTo'])).\
            group_by(Price.date).order_by(Price.date.asc())
        stats = [Stat(*x) for x in query.all()]
        return StatSchema(many=True).dump(stats).data
Пример #4
0
class ShopsResource(Resource):
    @use_args({
        'start':
        fields.Int(missing=0, location='query',
                   validate=validate.Range(min=0)),
        'count':
        fields.Int(missing=20,
                   location='query',
                   validate=validate.Range(min=0)),
        'sort':
        fields.List(
            fields.Str(validate=validate.OneOf(SORT_CHOICE)),
            missing=['id|DESC'],
            location='query'
            # default sort order not explicitly specified, maybe same as products?
        ),
        'status':
        fields.Str(missing='ACTIVE',
                   location='query',
                   validate=validate.OneOf(STATUS_CHOICE)),
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def get(self, args):
        query = Shop.query
        start = args['start']
        count = args['count']
        status = args['status']
        sorts = [x.split('|') for x in args['sort']]

        def to_sort_operator(field, order):
            sort_field = {'id': Shop.id, 'name': Shop.name}[field]

            sort_order = {'ASC': asc, 'DESC': desc}[order]

            return sort_order(sort_field)

        if status != 'ALL':
            query = query.filter_by(withdrawn=(status == 'WITHDRAWN'))
        query = query.order_by(
            *[to_sort_operator(field, order) for field, order in sorts])
        total = query.count()
        query = query.offset(start).limit(count)
        shops_page = query.all()
        shops = shop_schema.dump(shops_page, many=True).data
        return {'start': start, 'count': count, 'total': total, 'shops': shops}

    @requires_auth
    @use_args({
        'name':
        fields.Str(required=True, location='form'),
        'address':
        fields.Str(required=True, location='form'),
        'lng':
        fields.Float(required=True, location='form'),
        'lat':
        fields.Float(required=True, location='form'),
        'tags':
        fields.List(fields.String(), required=True, location='form'),
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def post(self, args, **_kwargs):
        position = from_shape(Point(args['lng'], args['lat']), srid=4326)
        new_shop = Shop(name=args['name'],
                        address=args['address'],
                        position=position,
                        withdrawn=False)
        new_shop.tags = [
            ShopTag(name=tag, shop=new_shop)
            for tag in unique_stripped(args['tags'])
        ]
        db.session.add(new_shop)
        try:
            db.session.commit()
        except IntegrityError as e:
            db.session.rollback()
            if "shop_pna_c" in e.orig:
                return custom_error('Address/Position/Name', ['Same address, position and name with existing shop']), \
                       ErrorCode.BAD_REQUEST
            else:
                return custom_error(
                    'tags',
                    ['Duplicate tags'
                     ]), ErrorCode.BAD_REQUEST  # we should never get here
        return shop_schema.dump(new_shop).data
Пример #5
0
class ShopResource(Resource):
    @use_args({
        'format':
        fields.Str(location='query', validate=validate.Equal('json'))
    })
    def get(self, _args, shop_id):
        shop = Shop.query.get_or_404(shop_id)
        return shop_schema.dump(shop).data

    @requires_auth
    @use_args({
        'name':
        fields.Str(required=True, location='form'),
        'address':
        fields.Str(required=True, location='form'),
        'lng':
        fields.Float(required=True, location='form'),
        'lat':
        fields.Float(required=True, location='form'),
        'tags':
        fields.List(fields.Str(), required=True, location='form'),
        'format':
        fields.Str(location='query', validate=validate.Equal('json'))
    })
    def put(self, args, shop_id, **_kwargs):
        shop = Shop.query.get_or_404(shop_id)
        shop.name = args['name']
        shop.address = args['address']
        shop.position = from_shape(Point(args['lng'], args['lat']), srid=4326)
        for tag in shop.tags:
            db.session.delete(tag)
        try:
            db.session.flush()
        except IntegrityError:
            db.session.rollback()
            return custom_error('Address/Position/Name', ['Same address, position and name with existing shop']), \
                   ErrorCode.BAD_REQUEST
        # flush delete's to db to ensure delete stmts precede insert stmt and avoid integrity error
        shop.tags = [
            ShopTag(name=tag, shop=shop)
            for tag in unique_stripped(args['tags'])
        ]
        try:
            db.session.commit()
        except IntegrityError:
            db.session.rollback()
            return custom_error(
                'tags', ['Duplicate tags'
                         ]), ErrorCode.BAD_REQUEST  # we should never get here
        return shop_schema.dump(shop).data

    @requires_auth
    @use_args({
        'name':
        fields.Str(location='form'),
        'address':
        fields.Str(location='form'),
        'lng':
        fields.Float(location='form'),
        'lat':
        fields.Float(location='form'),
        'tags':
        fields.List(fields.Str(), location='form'),
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def patch(self, args, shop_id, **_kwargs):
        del args['format']
        if len(args) != 1:
            return custom_error('patch', ['Specify exactly one of: name, address, lng, lat, tags']), \
                   ErrorCode.BAD_REQUEST
        shop = Shop.query.get_or_404(shop_id)
        changed = next(iter(args.keys()))
        if changed == 'tags':
            for tag in shop.tags:
                db.session.delete(tag)
            db.session.flush()
            shop.tags = [
                ShopTag(name=tag, shop=shop)
                for tag in unique_stripped(args['tags'])
            ]
        elif changed == 'lat':
            old = to_shape(shop.position)
            shop.position = from_shape(Point(old.x, args['lat']), srid=4326)
        elif changed == 'lng':
            old = to_shape(shop.position)
            shop.position = from_shape(Point(args['lng'], old.y), srid=4326)
        else:
            setattr(shop, changed, args[changed])

        try:
            db.session.commit()
        except IntegrityError:
            db.session.rollback()
            if changed == 'tags':
                return custom_error(
                    'tags',
                    ['Duplicate tags'
                     ]), ErrorCode.BAD_REQUEST  # we should never get here
            else:
                return custom_error('Address/Position/Name', ['Same address, position and name with existing shop']), \
                       ErrorCode.BAD_REQUEST

        return ShopSchema().dump(shop).data

    @requires_auth
    @use_args({
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def delete(self, _args, shop_id, is_admin, **_kwargs):
        shop = Shop.query.get(shop_id)
        if not shop:
            return custom_error('shop',
                                ['Invalid shop id']), ErrorCode.NOT_FOUND
        if is_admin:
            db.session.delete(shop)
        else:
            shop.withdrawn = True
        db.session.commit()
        return {'message': 'OK'}
Пример #6
0
class ProductsResource(Resource):
    _STATUS_CHOICE = ['ALL', 'WITHDRAWN', 'ACTIVE']
    _SORT_CHOICE = ['id|ASC', 'id|DESC', 'name|ASC', 'name|DESC']

    @use_args({
        'start':
        fields.Int(missing=0, location='query',
                   validate=validate.Range(min=0)),
        'count':
        fields.Int(missing=20,
                   location='query',
                   validate=validate.Range(min=0)),
        'status':
        fields.Str(missing='ACTIVE',
                   location='query',
                   validate=validate.OneOf(_STATUS_CHOICE)),
        'sort':
        fields.List(fields.Str(validate=validate.OneOf(_SORT_CHOICE)),
                    missing=['id|DESC'],
                    location='query'),
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def get(self, args, **_kwargs):
        start = args['start']
        count = args['count']
        status = args['status']
        sorts = [x.split('|') for x in args['sort']]

        def to_sort_operator(field, order):
            sort_field = {'id': Product.id, 'name': Product.name}[field]

            sort_order = {'ASC': asc, 'DESC': desc}[order]

            return sort_order(sort_field)

        query = Product.query
        if status != 'ALL':
            query = query.filter_by(withdrawn=(status == 'WITHDRAWN'))
        total = query.count()
        query = query.order_by(
            *[to_sort_operator(field, order)
              for field, order in sorts]).offset(start).limit(count)
        products = query.all()

        return {
            'start': start,
            'count': count,
            'total': total,
            'products': prod_schema.dump(products, many=True).data
        }

    @requires_auth
    @use_args({
        'name':
        fields.Str(location='form', required=True),
        'description':
        fields.Str(location='form', required=True),
        'category':
        fields.Str(location='form', required=True),
        'tags':
        fields.List(fields.Str(), location='form', required=True),
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def post(self, args, **_kwargs):
        product = Product(name=args['name'],
                          description=args['description'],
                          category=args['category'],
                          withdrawn=False)
        product.tags = [
            ProductTag(name=tag, product=product)
            for tag in unique_stripped(args['tags'])
        ]
        db.session.add(product)
        try:
            db.session.commit()
        except IntegrityError:
            db.session.rollback()
            return custom_error(
                'tags', ['Duplicate tags'
                         ]), ErrorCode.BAD_REQUEST  # we should never get here

        return prod_schema.dump(product).data
Пример #7
0
class ProductResource(Resource):
    @use_args({
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def get(self, _args, prod_id):
        product = Product.query.get_or_404(prod_id)

        return prod_schema.dump(product).data

    @requires_auth
    @use_args({
        'name':
        fields.Str(location='form', required=True),
        'description':
        fields.Str(location='form', required=True),
        'category':
        fields.Str(location='form', required=True),
        'tags':
        fields.List(fields.Str(), location='form', required=True),
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def put(self, args, prod_id, **_kwargs):
        product = Product.query.get_or_404(prod_id)
        product.name = args['name']
        product.description = args['description']
        product.category = args['category']
        for tag in product.tags:
            db.session.delete(tag)  # delete old product tags
        db.session.flush()
        # flush delete's to db to ensure delete stmts precedes insert stmt and avoid integrity error
        product.tags = [
            ProductTag(name=tag, product=product)
            for tag in unique_stripped(args['tags'])
        ]

        try:
            db.session.commit()
        except IntegrityError:
            db.session.rollback()
            return custom_error(
                'tags', ['Duplicate tags'
                         ]), ErrorCode.BAD_REQUEST  # we should never get here

        return prod_schema.dump(product).data

    @requires_auth
    @use_args({
        'name':
        fields.Str(location='form'),
        'description':
        fields.Str(location='form'),
        'category':
        fields.Str(location='form'),
        'tags':
        fields.List(fields.Str(), location='form'),
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def patch(self, args, prod_id, **_kwargs):
        del args['format']
        if len(args) != 1:
            return custom_error('patch', ['Specify exactly one of: name, description, category or tags']),\
                   ErrorCode.BAD_REQUEST

        product = Product.query.get_or_404(prod_id)
        changed = next(iter(args.keys()))
        if changed == 'tags':
            for tag in product.tags:
                db.session.delete(tag)  # delete old product tags
            db.session.flush()
            product.tags = [
                ProductTag(name=tag, product=product)
                for tag in unique_stripped(args['tags'])
            ]
        else:
            setattr(product, changed, args[changed])

        try:
            db.session.commit()
        except IntegrityError:
            db.session.rollback()
            custom_error('tags',
                         ['Duplicate tags'
                          ]), ErrorCode.BAD_REQUEST  # we should never get here

        return prod_schema.dump(product).data

    @requires_auth
    @use_args({
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def delete(self, _args, prod_id, is_admin, **_kwargs):
        product = Product.query.get_or_404(prod_id)

        if is_admin:
            db.session.delete(product)
        else:
            product.withdrawn = True

        db.session.commit()

        return {'message': 'OK'}
Пример #8
0
class Mods(RouteCog):
    @staticmethod
    def dict_all(models):
        return [m.to_dict() for m in models]

    @multiroute("/api/v1/mods", methods=["GET"], other_methods=["POST"])
    @json
    @use_kwargs(
        {
            "q": fields.Str(),
            "page": fields.Int(missing=0),
            "limit": fields.Int(missing=50),
            "category": EnumField(ModCategory),
            "rating": fields.Int(validate=validate.OneOf([1, 2, 3, 4, 5])),
            "status": EnumField(ModStatus),
            "sort": EnumField(ModSorting),
            "ascending": fields.Bool(missing=False),
        },
        locations=("query", ),
    )
    async def get_mods(
        self,
        q: str = None,
        page: int = None,
        limit: int = None,
        category: ModCategory = None,
        rating: int = None,
        status: ModStatus = None,
        sort: ModSorting = None,
        ascending: bool = None,
    ):
        if not 1 <= limit <= 100:
            limit = max(1, min(
                limit,
                100))  # Clamp `limit` to 1 or 100, whichever is appropriate

        page = page - 1 if page > 0 else 0
        query = Mod.query.where(Mod.verified)

        if q is not None:
            like = f"%{q}%"

            query = query.where(
                and_(
                    Mod.title.match(q),
                    Mod.tagline.match(q),
                    Mod.description.match(q),
                    Mod.title.ilike(like),
                    Mod.tagline.ilike(like),
                    Mod.description.ilike(like),
                ))

        if category is not None:
            query = query.where(Mod.status == category)

        if rating is not None:
            query = query.where(rating + 1 > db.select([
                func.avg(
                    Review.select("rating").where(Review.mod_id == Mod.id))
            ]) >= rating)

        if status is not None:
            query = query.where(Mod.status == status)

        if sort is not None:
            sort_by = mod_sorters[sort]
            query = query.order_by(
                sort_by.asc() if ascending else sort_by.desc())

        results = await paginate(query, page, limit).gino.all()
        total = await query.alias().count().gino.scalar()

        return jsonify(total=total,
                       page=page,
                       limit=limit,
                       results=self.dict_all(results))

    @multiroute("/api/v1/mods", methods=["POST"], other_methods=["GET"])
    @requires_login
    @json
    @use_kwargs(
        {
            "title":
            fields.Str(required=True, validate=validate.Length(max=64)),
            "tagline":
            fields.Str(required=True, validate=validate.Length(max=100)),
            "description":
            fields.Str(required=True,
                       validate=validate.Length(min=100, max=10000)),
            "website":
            fields.Url(required=True),
            "status":
            EnumField(ModStatus, required=True),
            "category":
            EnumField(ModCategory, required=True),
            "authors":
            fields.List(fields.Nested(AuthorSchema), required=True),
            "icon":
            b64img_field("icon"),
            "banner":
            b64img_field("banner"),
            "media":
            fields.List(b64img_field("media"), required=True),
            "is_private_beta":
            fields.Bool(missing=False),
            "mod_playtester":
            fields.List(fields.Str()),
            "color":
            EnumField(ModColor, missing=ModColor.default),
            "recaptcha":
            fields.Str(required=True),
        },
        locations=("json", ),
    )
    async def post_mods(
        self,
        title: str,
        tagline: str,
        description: str,
        website: str,
        authors: List[dict],
        status: ModStatus,
        category: ModCategory,
        icon: str,
        banner: str,
        media: List[str],
        recaptcha: str,
        color: ModColor,
        is_private_beta: bool = None,
        mod_playtester: List[str] = None,
    ):
        score = await verify_recaptcha(recaptcha, self.core.aioh_sess,
                                       "create_mod")

        if score < 0.5:
            # TODO: discuss what to do here
            abort(400, "Possibly a bot")

        user_id = await get_token_user()

        # Check if any mod with a similar enough name exists already.
        generalized_title = generalize_text(title)
        mods = await Mod.get_any(True,
                                 generalized_title=generalized_title).first()

        if mods is not None:
            abort(400, "A mod with that title already exists")

        if status is ModStatus.archived:
            abort(400, "Can't create a new archived mod")

        mod = Mod(
            title=title,
            tagline=tagline,
            description=description,
            website=website,
            status=status,
            category=category,
            theme_color=color,
        )

        icon_mimetype, icon_data = validate_img(icon, "icon")
        banner_mimetype, banner_data = validate_img(banner, "banner")
        media = [validate_img(x, "media") for x in media]

        for i, author in enumerate(authors):
            if author["id"] == user_id:
                authors.pop(i)
                continue
            elif not await User.exists(author["id"]):
                abort(400, f"Unknown user '{author['id']}'")

        authors.append({"id": user_id, "role": AuthorRole.owner})

        if is_private_beta is not None:
            mod.is_private_beta = is_private_beta

        if mod_playtester is not None:
            if not is_private_beta:
                abort(400, "No need for `ModPlaytester` if open beta")

            for playtester in mod_playtester:
                if not await User.exists(playtester):
                    abort(400, f"Unknown user '{playtester}'")

        # Decode images and add name for mimetypes
        icon_data = base64.b64decode(icon_data)
        banner_data = base64.b64decode(banner_data)
        icon_ext = icon_mimetype.split("/")[1]
        banner_ext = banner_mimetype.split("/")[1]

        icon_resp = await ipfs_upload(icon_data, f"icon.{icon_ext}",
                                      self.core.aioh_sess)
        banner_resp = await ipfs_upload(banner_data, f"banner.{banner_ext}",
                                        self.core.aioh_sess)

        mod.icon = icon_resp["Hash"]
        mod.banner = banner_resp["Hash"]

        media_hashes = []
        for mime, data in media:
            ext = mime.split("/")[1]
            resp = await ipfs_upload(data, f"media.{ext}", self.core.aioh_sess)

            media_hashes += [resp]

        await mod.create()
        await ModAuthor.insert().gino.all(*[
            dict(user_id=author["id"], mod_id=mod.id, role=author["role"])
            for author in authors
        ])
        await ModPlaytester.insert().gino.all(
            *[dict(user_id=user, mod_id=mod.id) for user in mod_playtester])

        if media_hashes:
            await Media.insert().gino.all(*[
                dict(type=MediaType.image, hash=hash_, mod_id=mod.id)
                for hash_ in media_hashes
            ])

        return jsonify(mod.to_dict())

    @route("/api/v1/mods/recent_releases")
    @json
    async def get_recent_releases(self):
        mods = (await Mod.query.where(
            and_(Mod.verified, Mod.status == ModStatus.released)
        ).order_by(Mod.released_at.desc()).limit(10).gino.all())

        return jsonify(self.dict_all(mods))

    @route("/api/v1/mods/most_loved")
    @json
    async def get_most_loved(self):
        love_counts = (select(
            [func.count()]).where(UserFavorite.mod_id == Mod.id).as_scalar())
        mods = await Mod.query.order_by(love_counts.desc()
                                        ).limit(10).gino.all()

        return jsonify(self.dict_all(mods))

    @route("/api/v1/mods/most_downloads")
    @json
    async def get_most_downloads(self):
        mods = (await
                Mod.query.where(and_(Mod.verified, Mod.released_at is not None)
                                ).order_by(Mod.downloads.desc()
                                           ).limit(10).gino.all())

        return jsonify(self.dict_all(mods))

    @route("/api/v1/mods/trending")
    @json
    async def get_trending(self):
        # TODO: implement
        return jsonify([])

    @route("/api/v1/mods/editors_choice")
    @json
    async def get_ec(self):
        mod_ids = [x.mod_id for x in await EditorsChoice.query.gino.all()]
        mods = (await Mod.query.where(Mod.id in mod_ids
                                      ).order_by(Mod.downloads.desc()
                                                 ).limit(10).gino.all())
        return jsonify(mods)

    @multiroute("/api/v1/mods/<mod_id>",
                methods=["GET"],
                other_methods=["PATCH", "DELETE"])
    @json
    async def get_mod(self, mod_id: str):
        # mod = await Mod.get(mod_id)
        mod = (await
               Mod.load(authors=ModAuthor,
                        media=Media).where(Mod.id == mod_id).gino.first())

        if mod is None:
            abort(404, "Unknown mod")

        return jsonify(mod.to_dict())

    @multiroute("/api/v1/mods/<mod_id>",
                methods=["PATCH"],
                other_methods=["GET", "DELETE"])
    @requires_login
    @json
    @use_kwargs(
        {
            "title":
            fields.Str(validate=validate.Length(max=64)),
            "tagline":
            fields.Str(validate=validate.Length(max=100)),
            "description":
            fields.Str(validate=validate.Length(min=100, max=10000)),
            "website":
            fields.Url(),
            "status":
            EnumField(ModStatus),
            "category":
            EnumField(ModCategory),
            "authors":
            fields.List(fields.Nested(AuthorSchema)),
            "icon":
            fields.Str(validate=validate.Regexp(
                DATA_URI_RE,
                error=
                ("`icon` should be a data uri like 'data:image/png;base64,<data>' or "
                 "'data:image/jpeg;base64,<data>'"),
            )),
            "banner":
            fields.Str(validate=validate.Regexp(
                DATA_URI_RE,
                error=
                ("`banner` should be a data uri like 'data:image/png;base64,<data>' or "
                 "'data:image/jpeg;base64,<data>'"),
            )),
            "color":
            EnumField(ModColor),
            "is_private_beta":
            fields.Bool(),
            "mod_playtester":
            fields.List(fields.Str()),
        },
        locations=("json", ),
    )
    async def patch_mod(
        self,
        mod_id: str = None,
        authors: List[dict] = None,
        mod_playtester: List[str] = None,
        icon: str = None,
        banner: str = None,
        **kwargs,
    ):
        if not await Mod.exists(mod_id):
            abort(404, "Unknown mod")

        mod = await Mod.get(mod_id)
        updates = mod.update(**kwargs)

        if authors is not None:
            authors = [
                author for author in authors if await User.exists(author["id"])
            ]
            # TODO: if user is owner or co-owner, allow them to change the role of others to ones below them.
            authors = [
                author for author in authors
                if not await ModAuthor.query.where(
                    and_(ModAuthor.user_id == author["id"], ModAuthor.mod_id ==
                         mod_id)).gino.first()
            ]

        if mod_playtester is not None:
            for playtester in mod_playtester:
                if not await User.exists(playtester):
                    abort(400, f"Unknown user '{playtester}'")
                elif await ModPlaytester.query.where(
                        and_(
                            ModPlaytester.user_id == playtester,
                            ModPlaytester.mod_id == mod.id,
                        )).gino.all():
                    abort(400, f"{playtester} is already enrolled.")

        if icon is not None:
            icon_mimetype, icon_data = validate_img(icon, "icon")
            icon_data = base64.b64decode(icon_data)

            icon_ext = icon_mimetype.split("/")[1]
            icon_resp = await ipfs_upload(icon_data, f"icon.{icon_ext}",
                                          self.core.aioh_sess)

            updates = updates.update(icon=icon_resp["Hash"])

        if banner is not None:
            banner_mimetype, banner_data = validate_img(banner, "banner")
            banner_data = base64.b64decode(banner_data)

            banner_ext = banner_mimetype.split("/")[1]
            banner_resp = await ipfs_upload(banner_data,
                                            f"banner.{banner_ext}",
                                            self.core.aioh_sess)

            updates = updates.update(banner=banner_resp["Hash"])

        await updates.apply()
        await ModAuthor.insert().gino.all(*[
            dict(user_id=author["id"], mod_id=mod.id, role=author["role"])
            for author in authors
        ])
        await ModPlaytester.insert().gino.all(
            *[dict(user_id=user, mod_id=mod.id) for user in ModPlaytester])

        return jsonify(mod.to_dict())

    # TODO: decline route with reason, maybe doesn't 100% delete it? idk
    @multiroute("/api/v1/mods/<mod_id>",
                methods=["DELETE"],
                other_methods=["GET", "PATCH"])
    @requires_login
    @json
    async def delete_mod(self, mod_id: str):
        await Mod.delete.where(Mod.id == mod_id).gino.status()

        return jsonify(True)

    @route("/api/v1/mods/<mod_id>/download")
    @json
    async def get_download(self, mod_id: str):
        user_id = await get_token_user()

        mod = await Mod.get(mod_id)

        if mod is None:
            abort(404, "Unknown mod")
        if user_id is None and mod.is_private_beta:
            abort(403, "Private beta mods requires authentication.")
        if not await ModPlaytester.query.where(
                and_(ModPlaytester.user_id == user_id, ModPlaytester.mod_id
                     == mod.id)).gino.all():
            abort(403, "You are not enrolled to the private beta.")
        elif not mod.zip_url:
            abort(404, "Mod has no download")

        return jsonify(url=mod.zip_url)

    @multiroute("/api/v1/mods/<mod_id>/reviews",
                methods=["GET"],
                other_methods=["POST"])
    @json
    @use_kwargs(
        {
            "page":
            fields.Int(missing=0),
            "limit":
            fields.Int(missing=10),
            "rating":
            UnionField(
                [
                    fields.Int(validate=validate.OneOf([1, 2, 3, 4, 5])),
                    fields.Str(validate=validate.Equal("all")),
                ],
                missing="all",
            ),
            "sort":
            EnumField(ReviewSorting, missing=ReviewSorting.best),
        },
        locations=("query", ),
    )
    async def get_reviews(
        self,
        mod_id: str,
        page: int,
        limit: int,
        rating: Union[int, str],
        sort: ReviewSorting,
    ):
        if not await Mod.exists(mod_id):
            abort(404, "Unknown mod")

        if not 1 <= limit <= 25:
            limit = max(1, min(
                limit,
                25))  # Clamp `limit` to 1 or 100, whichever is appropriate

        page = page - 1 if page > 0 else 0

        upvote_aggregate = (select([func.count()]).where(
            and_(
                ReviewReaction.review_id == Review.id,
                ReviewReaction.reaction == ReactionType.upvote,
            )).as_scalar())
        downvote_aggregate = (select([func.count()]).where(
            and_(
                ReviewReaction.review_id == Review.id,
                ReviewReaction.reaction == ReactionType.downvote,
            )).as_scalar())
        funny_aggregate = (select([func.count()]).where(
            and_(
                ReviewReaction.review_id == Review.id,
                ReviewReaction.reaction == ReactionType.funny,
            )).as_scalar())

        query = Review.load(
            author=User,
            upvotes=upvote_aggregate,
            downvotes=downvote_aggregate,
            funnys=funny_aggregate,
        ).where(Review.mod_id == mod_id)

        user_id = await get_token_user()

        if user_id:
            did_upvote_q = (select([func.count()]).where(
                and_(
                    ReviewReaction.review_id == Review.id,
                    ReviewReaction.user_id == user_id,
                    ReviewReaction.reaction == ReactionType.upvote,
                )).limit(1).as_scalar())
            did_downvote_q = (select([func.count()]).where(
                and_(
                    ReviewReaction.review_id == Review.id,
                    ReviewReaction.user_id == user_id,
                    ReviewReaction.reaction == ReactionType.downvote,
                )).limit(1).as_scalar())
            did_funny_q = (select([func.count()]).where(
                and_(
                    ReviewReaction.review_id == Review.id,
                    ReviewReaction.user_id == user_id,
                    ReviewReaction.reaction == ReactionType.funny,
                )).limit(1).as_scalar())

            query = query.gino.load(
                user_reacted_upvote=did_upvote_q,
                user_reacted_downvote=did_downvote_q,
                user_reacted_funny=did_funny_q,
            )

        if review_sorters[sort]:
            query = query.order_by(review_sorters[sort])
        elif sort == ReviewSorting.best:
            query = query.order_by(upvote_aggregate - downvote_aggregate)
        elif sort == ReviewSorting.funniest:
            query = query.order_by(funny_aggregate.desc())

        if isinstance(rating, int):
            values = [rating, rating + 0.5]

            if rating == 1:
                # Also get reviews with a 0.5 star rating, otherwise they'll never appear.
                values.append(0.5)

            query = query.where(Review.rating.in_(values))

        reviews = await paginate(query, page, limit).gino.all()
        total = await query.alias().count().gino.scalar()

        return jsonify(total=total,
                       page=page,
                       limit=limit,
                       results=self.dict_all(reviews))

    @multiroute("/api/v1/mods/<mod_id>/reviews",
                methods=["POST"],
                other_methods=["GET"])
    @requires_login
    @json
    @use_kwargs({
        "rating":
        fields.Int(
            required=True,
            validate=[
                # Only allow increments of 0.5, up to 5.
                lambda x: 5 >= x >= 1,
                lambda x: x % 0.5 == 0,
            ],
        ),
        "content":
        fields.Str(required=True, validate=validate.Length(max=2000)),
        "title":
        fields.Str(required=True, validate=validate.Length(max=32)),
    })
    async def post_review(self, mod_id: str, rating: int, content: str,
                          title: str):
        if not await Mod.exists(mod_id):
            abort(404, "Unknown mod")

        user_id = await get_token_user()

        if await Review.query.where(
                and_(Review.author_id == user_id,
                     Review.mod_id == mod_id)).gino.first():
            abort(400, "Review already exists")

        review = await Review.create(
            title=title,
            content=content,
            rating=rating,
            author_id=user_id,
            mod_id=mod_id,
        )

        return jsonify(review.to_json())

    @route("/api/v1/reviews/<review_id>/react", methods=["POST"])
    @requires_login
    @json
    @use_kwargs({
        "undo":
        fields.Bool(missing=False),
        "type":
        EnumField(ReactionType, data_key="type", required=True),
    })
    async def react_review(self, review_id: str, undo: bool, type_: EnumField):
        user_id = await get_token_user()
        where_opts = [
            ReviewReaction.review_id == review_id,
            ReviewReaction.user_id == user_id,
            ReviewReaction.reaction == type_,
        ]
        create_opts = {
            "review_id": review_id,
            "user_id": user_id,
            "reaction": type_
        }

        exists = bool(await ReviewReaction.select("id").where(and_(*where_opts)
                                                              ).gino.scalar())

        if (exists and not undo) or (not exists and undo):
            pass
        elif not undo:
            if type_ == ReactionType.upvote:
                # Negate any downvotes by the user as upvoting and downvoting at the same time is stupid
                await ReviewReaction.delete.where(
                    and_(*where_opts[:-1],
                         ReviewReaction.type == ReactionType.downvote)
                ).gino.status()
            elif type_ == ReactionType.downvote:
                # Ditto for any upvotes if trying to downvote
                await ReviewReaction.delete.where(
                    and_(*where_opts[:-1],
                         ReviewReaction.type == ReactionType.downvote)
                ).gino.status()

            await ReviewReaction.create(**create_opts)
        else:
            await ReviewReaction.delete.where(and_(*where_opts)).gino.status()

        # Return user's current reaction results
        results = await ReviewReaction.query.where(and_(*where_opts[:-1])
                                                   ).gino.all()
        results = [x.reaction for x in results]

        return jsonify(
            upvote=ReactionType.upvote in results,
            downvote=ReactionType.downvote in results,
            funny=ReactionType.funny in results,
        )

    # This handles POST requests to add zip_url.
    # Usually this would be done via a whole entry but this
    # is designed for existing content.
    @route("/api/v1/mods/<mod_id>/upload_content", methods=["POST"])
    @json
    @requires_supporter
    @requires_login
    async def upload(self, mod_id: str):
        if not await Mod.exists(mod_id):
            abort(404, "Unknown mod")

        abort(501, "Coming soon")

    @route("/api/v1/mods/<mod_id>/report", methods=["POST"])
    @json
    @use_kwargs(
        {
            "content":
            fields.Str(required=True,
                       validate=validate.Length(min=100, max=1000)),
            "type_":
            EnumField(ReportType, data_key="type", required=True),
            "recaptcha":
            fields.Str(required=True),
        },
        locations=("json", ),
    )
    @requires_login
    @limiter.limit("2 per hour")
    async def report_mod(self, mod_id: str, content: str, type_: ReportType,
                         recaptcha: str):
        score = await verify_recaptcha(recaptcha, self.core.aioh_sess)

        if score < 0.5:
            # TODO: send email/other 2FA when below 0.5
            abort(400, "Possibly a bot")

        user_id = await get_token_user()
        report = await Report.create(content=content,
                                     author_id=user_id,
                                     mod_id=mod_id,
                                     type=type_)

        return jsonify(report.to_dict())
Пример #9
0
class PricesResource(Resource):
    class PriceSchema(ma.ModelSchema):
        class ProductSchema(ma.ModelSchema):
            class ProductTagSchema(ma.ModelSchema):
                name = fields.String()

                @post_dump
                def flatten(self, data):
                    return data['name']

            productId = fields.Int(attribute="id")
            productName = fields.String(attribute="name")
            productTags = fields.Nested(ProductTagSchema,
                                        many=True,
                                        attribute='tags')

        class ShopSchema(ma.ModelSchema):
            class ShopTagSchema(ma.ModelSchema):
                name = fields.String()

                @post_dump
                def flatten(self, data):
                    return data['name']

            shopId = fields.Int(attribute='id')
            shopName = fields.Str(attribute='name')
            shopTags = fields.Nested(ShopTagSchema,
                                     many=True,
                                     attribute='tags')
            shopAddress = fields.Str(attribute='address')

        price = fields.Float()
        date = fields.Date()
        product = fields.Nested(ProductSchema)
        shop = fields.Nested(ShopSchema)
        shopDist = fields.Float(attribute='dist')

        @pre_dump
        def handle_tuple(self, data):
            # if with_geo: query result = list of tuple(product, distance)
            if not isinstance(data, tuple):
                return data
            data[0].dist = data[1]
            return data[0]

        @post_dump
        def refactor(self, data):
            # flatten shop, product entries of data dict
            t_shop = data['shop']
            t_prod = data['product']
            del data['shop']
            del data['product']
            data.update(t_shop)
            data.update(t_prod)
            return data

    @use_args({
        'start':
        fields.Int(missing=0, location='query',
                   validate=validate.Range(min=0)),
        'count':
        fields.Int(missing=20,
                   location='query',
                   validate=validate.Range(min=0)),  # zero count is ok ?
        'geoDist':
        fields.Float(missing=None, location='query'),
        'geoLng':
        fields.Float(missing=None, location='query'),
        'geoLat':
        fields.Float(missing=None, location='query'),
        'dateFrom':
        fields.Date(missing=None, location='query'),
        'dateTo':
        fields.Date(missing=None, location='query'),
        'sort':
        fields.List(fields.Str(validate=validate.OneOf(SORT_CHOICE)),
                    missing=['price|ASC'],
                    location='query'),
        'shops':
        fields.List(fields.Int(), missing=None, location='query'),
        'products':
        fields.List(fields.Int(), missing=None, location='query'),
        'tags':
        fields.List(fields.Str(), missing=None, location='query'),
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def get(self, args):
        shop_ids = args['shops']
        product_ids = args['products']
        tags = args['tags']

        with_geo = args['geoDist'] is not None and args[
            'geoLng'] is not None and args['geoLat'] is not None
        no_geo = args['geoDist'] is None and args['geoLng'] is None and args[
            'geoLat'] is None
        if not (with_geo or no_geo):
            return custom_error(
                'geo',
                ['Invalid geo parameters combination']), ErrorCode.BAD_REQUEST

        with_date = (args['dateFrom'] is not None and args['dateTo'] is not None) and \
                    (args['dateFrom'] <= args['dateTo'])
        no_date = args['dateFrom'] is None and args['dateTo'] is None
        if not (with_date or no_date):
            return custom_error(
                'date',
                ['Invalid date parameters combination']), ErrorCode.BAD_REQUEST

        sorts = [x.split('|') for x in args['sort']]
        for sort in sorts:
            if (sort[0] == 'geoDist' and no_geo) or (sort[0] == 'date'
                                                     and no_date):
                return custom_error(
                    'sort', ['Invalid sort parameter']), ErrorCode.BAD_REQUEST

        # extra validation, some combinations are illegal

        dist_col = None
        if with_geo:
            dist = func.ST_Distance(Shop.position, from_shape(Point(args['geoLng'], args['geoLat']), srid=4326), True).\
                label('dist')
            query = db.session.query(Price, dist)
        else:
            query = Price.query

        query = query.join(Price.product, Price.shop)
        today = date.today()
        date_from = args['dateFrom'] if args['dateFrom'] else today
        date_to = args['dateTo'] if args['dateTo'] else today
        query = query.filter(Price.date.between(date_from, date_to))

        if shop_ids:
            query = query.filter(Shop.id.in_(shop_ids))
        if product_ids:
            query = query.filter(Product.id.in_(product_ids))

        if tags:
            query = query.filter(
                or_(
                    Product.tags.any(
                        func.lower(ProductTag.name).in_(map(str.lower, tags))),
                    Shop.tags.any(
                        func.lower(ShopTag.name).in_(map(str.lower, tags)))))

        if with_geo:
            subq = query.subquery()
            dist_col = subq.c.dist
            query = db.session.query(Price,
                                     dist_col).select_entity_from(subq).filter(
                                         dist_col < 1000 * args['geoDist'])
            # nested query to avoid recalculating distance expr
            # WHERE and HAVING clauses can't refer to column names (dist)

        def to_sort_operator(field, order):
            sort_field = {
                'geoDist': dist_col,
                'price': Price.price,
                'date': Price.date
            }[field]

            sort_order = {'ASC': asc, 'DESC': desc}[order]

            return sort_order(sort_field)

        query = query.order_by(
            *[to_sort_operator(field, order) for field, order in sorts])

        start = args['start']
        count = args['count']
        total = query.count()
        query = query.offset(start).limit(count)
        prices_page = query.all()
        prices = PricesResource.PriceSchema(many=True).dump(prices_page).data
        return {
            'start': start,
            'count': count,
            'total': total,
            'prices': prices
        }

    @requires_auth
    @use_args({
        'price':
        fields.Float(required=True, location='form'),
        'dateFrom':
        fields.Date(required=True, location='form'),
        'dateTo':
        fields.Date(required=True, location='form'),
        'productId':
        fields.Int(required=True, attribute='product_id', location='form'),
        'shopId':
        fields.Int(required=True, attribute='shop_id', location='form'),
        'format':
        fields.Str(missing='json',
                   location='query',
                   validate=validate.Equal('json'))
    })
    def post(self, args, **_kwargs):
        date1 = args['dateFrom']
        date2 = args['dateTo']
        if not (date1 <= date2):
            return custom_error(
                'date',
                ['date(From) must be before date(To)']), ErrorCode.BAD_REQUEST

        shop = Shop.query.filter_by(id=args['shop_id']).first()
        product = Product.query.filter_by(id=args['product_id']).first()
        if not shop:
            return custom_error('shop',
                                ['Invalid shop id']), ErrorCode.NOT_FOUND
        elif not product:
            return custom_error('product',
                                ['Invalid product id']), ErrorCode.NOT_FOUND

        del args['format']
        del args['dateFrom']
        del args['dateTo']

        for d in [
                date1 + timedelta(days=x)
                for x in range((date2 - date1).days + 1)
        ]:
            new_price = dict(**args, date=d)
            upsert = insert(Price).values(new_price).on_conflict_do_update(
                constraint="price_psd_c", set_=new_price)
            db.session.execute(upsert)
        db.session.commit()
        new_prices = Price.query.filter(Price.product_id == args['product_id'],
                                        Price.shop_id == args['shop_id'],
                                        Price.date.between(date1,
                                                           date2)).all()
        return {
            'start': 0,
            'total': (date2 - date1).days + 1,
            'count': (date2 - date1).days + 1,
            # why you ask ? no idea, field must be present, can't find any sensible value
            'prices':
            PricesResource.PriceSchema(many=True).dump(new_prices).data
        }