potion icon indicating copy to clipboard operation
potion copied to clipboard

Using Archive recipe and filtering

Open matdrapeau opened this issue 6 years ago • 4 comments

In order to use soft-deletes, I tried to used the recipe (https://potion.readthedocs.io/en/latest/recipes.html#archivingresource) but filtering resources are not working anymore.

from flask_potion import ModelResource, fields
from flask_potion.contrib.alchemy import SQLAlchemyManager
from flask_potion.exceptions import ItemNotFound
from flask_potion.routes import Route
from flask_potion.instances import Instances
from enum import Enum

class BaseResource(ModelResource):
    class Meta:
        read_only_fields = ['create_date', 'update_date']
    class Schema:
        create_date = fields.DateTimeString()
        update_date = fields.DateTimeString()



class Location(Enum):
    ARCHIVE_ONLY = 1
    INSTANCES_ONLY = 2
    BOTH = 3


class ArchiveManager(SQLAlchemyManager):
    def _query(self, source=Location.INSTANCES_ONLY):
        query = super(ArchiveManager, self)._query()

        if source == Location.BOTH:
            return query
        elif source == Location.ARCHIVE_ONLY:
            return query.filter(getattr(self.model, 'is_archived') == True)
        else:
            return query.filter(getattr(self.model, 'is_archived') == False)

    def instances(self, where=None, sort=None, source=Location.INSTANCES_ONLY):
        query = self._query(source)
        if where:
            expressions = [self._expression_for_condition(condition) for condition in where]
            query = self._query_filter(query, self._and_expression(expressions))
        if sort:
            query = self._query_order_by(query, sort)
        return query

    def archive_instances(self, page, per_page, where=None, sort=None):
        return self.instances(where=where, sort=sort, source=Location.ARCHIVE_ONLY).paginate(page=page, per_page=per_page)

    def read(self, id, source=Location.INSTANCES_ONLY):
        query = self._query(source)
        if query is None:
            raise ItemNotFound(self.resource, id=id)
        return self._query_filter_by_id(query, id)


class ArchivingResource(BaseResource):
    class Meta:
        manager = ArchiveManager
        exclude_routes = ['destroy'] # we're using rel="archive" instead.

    class Schema(BaseResource.Schema):
        is_archived = fields.Boolean(io='r')

    @Route.GET('/<int:id>', rel="self", attribute="instance")
    def read(self, id) -> fields.Inline('self'):
        return self.manager.read(id, source=Location.BOTH)

    @read.PATCH(rel="update")
    def update(self, properties, id):
        item = self.manager.read(id, source=Location.INSTANCES_ONLY)
        updated_item = self.manager.update(item, properties)
        return updated_item

    update.response_schema = update.request_schema = fields.Inline('self', patchable=True)

    @update.DELETE(rel="archive")
    def destroy(self, id):
        item = self.manager.read(id, source=Location.INSTANCES_ONLY)
        self.manager.update(item, {"is_archived": True})
        return None, 204

    @Route.GET("/archive")
    def archive_instances(self, **kwargs):
        return self.manager.archive_instances(**kwargs)

    archive_instances.request_schema = archive_instances.response_schema = Instances()

    @Route.GET('/archive/<int:id>', rel="readArchived")
    def read_archive(self, id) -> fields.Inline('self'):
        return self.manager.read(id, source=Location.ARCHIVE_ONLY)

    @Route.POST('/archive/<int:id>/restore', rel="restoreFromArchive")
    def restore_from_archive(self, id) -> fields.Inline('self'):
        item = self.manager.read(id, source=Location.ARCHIVE_ONLY)
        return self.manager.update(item, {"is_archived": False})


class Base(object):
    id = Column(Integer, primary_key=True, autoincrement=True)
    create_date = Column(DateTime(timezone=True), server_default=func.now())
    update_date = Column(DateTime(timezone=True), onupdate=func.now())
    is_archived = Column(Boolean, default=False)

Base = declarative_base(cls=Base)

class User(Base):
    __tablename__ = 'users'
    name = Column(String(), nullable=True)

class UserResource(ArchivingResource):
    class Meta(ArchivingResource.Meta):
        model = User
        natural_key = ('organization_id', 'foreign_id')
        #write_only_fields = ['organization_id']
    class Schema(ArchivingResource.Schema):
        name = fields.String()

app.api.add_resource(UserResource)

Doing such filters are raising errors: /users?where={"create_date": {"$gt": "2018-06-10T14:56:15-05:00"}} /users/archive?where={"create_date": {"$gt": "2018-06-10T14:56:15-05:00"}}

I get: ValidationError {'$gt': '2018-06-10T14:56:15-05:00'} is not valid under any of the given schemas

matdrapeau avatar Aug 17 '18 19:08 matdrapeau

Does /users?where={"create_date": {"$gt": "2018-06-10T19:56:15Z"}} work?

lyschoening avatar Aug 20 '18 11:08 lyschoening

@lyschoening Actually, that query doesn't work.

It seems the inheritance of the Meta is not picking parent attributes. create_date and update_date are not exposed for resources which inherit from ArchivingResource based on this definition:

class BaseResource(ModelResource):
    class Meta:
        read_only_fields = ['create_date', 'update_date']
    class Schema:
        create_date = fields.DateTimeString()
        update_date = fields.DateTimeString()


class ArchivingResource(BaseResource):
    class Meta(BaseResource.Meta):
        manager = ArchiveManager
        exclude_routes = ['destroy'] # we're using rel="archive" instead.

    class Schema(BaseResource.Schema):
        is_archived = fields.Boolean(io='r')

matdrapeau avatar Oct 15 '18 19:10 matdrapeau

@matdrapeau Have you tried just class Meta: and class Schema:? Sorry, I did not spot that before. There should be no need to subclass those classes in the way you did.

lyschoening avatar Oct 16 '18 06:10 lyschoening

@lyschoening no, same error as well without subclassing

matdrapeau avatar Oct 16 '18 16:10 matdrapeau