adnanmuttaleb
adnanmuttaleb

Reputation: 3624

How to use flask-restplus's fields.Url for mongoengine Document queryset?

I have the following Mongoengine Document:

class Post(mongo_db.Document):
    id = mongo_db.UUIDField(max_length=300, required=True, primary_key=True)
    content = mongo_db.StringField(max_length=300, required=False,)
    notes = mongo_db.ListField(mongo_db.StringField(max_length=2000), required=False)
    category = mongo_db.ReferenceField('Category', required=True)
    creation_date = mongo_db.DateTimeField()

And the following model, Resource defined for it:

post_fields = ns.model(
    'Post', 
    {
        'content': fields.String,
        'creation_date': fields.DateTime,
        'notes': fields.List(fields.String),
        'category': fields.Nested(category_fields),
        'URI': fields.Url('my_endpoint')
    }
)


class PostResource(Resource):

    @ns.marshal_with(post_fields)
    def get(self):
        queryset = Post.objects
        return list(queryset)

Everything works OK for all fields, except the fields.Url, and the following error is raised:

flask_restplus.fields.MarshallingError: url_for() argument after ** must be a mapping, not Post

I tried to use flask's jsonify function:

return jsonify(queryset)

but the following error occur:

werkzeug.routing.BuildError: Could not build url for endpoint 'my_endpoint' with values ['_on_close', '_status', '_status_code', 'direct_passthrough', 'headers', 'response']. Did you forget to specify values ['id']?

Please inform me if you want any other details, and thanks in advance.

Upvotes: 1

Views: 724

Answers (1)

Dinko Pehar
Dinko Pehar

Reputation: 6061

I tried solving your problem by simplifying Document and model. The problem lies in response of your resource:

def get(self):
        queryset = Post.objects
        return list(queryset)

queryset becomes list of posts as [<Post: Post object>, <Post: Post object>]. When marshal_with decorator is applied to response, it expects single object, dicts, or lists of objects. Although it should work with Post object, I'm not directly sure what causes error when applying URI to Post object. It seems somehow that url_for method is called with it's proper arguments internally, and tries to unpack Post object as **post.

Small example:

def url_for(endpoint, *args, **kwargs):
    print(endpoint, args, kwargs)

class Post:

    def __init__(self):
        self.content = "Dummy content"

post1 = Post()  # Instance of Post class
post2 = {"content": "This dummy content works !"} # Dictionary

# This won't work, returns TypeError: url_for() argument after ** must be a mapping, not Post
url_for("my_endpoint", 1, 2, **post1)

# This works since it's able to unpack dictionary.
url_for("my_endpoint", 1, 2, **post2)

The solution is to convert every Post object to dict by using .to_mongo().to_dict() on it. Also, to represent URI of an object Post with ID, we must create resource route for it.

Full example:

from flask import Flask
from flask_restplus import Api, fields, Resource, marshal_with
from flask_mongoengine import MongoEngine

import uuid

app = Flask(__name__)
api = Api(app)

app.config['MONGODB_SETTINGS'] = {
    'db': 'test',
    'host': 'localhost',
    'port': 27017
}

mongo_db = MongoEngine(app)

class Post(mongo_db.Document):
    id = mongo_db.UUIDField(max_length=300, required=True, primary_key=True)
    content = mongo_db.StringField(max_length=300, required=False)
    notes = mongo_db.ListField(mongo_db.StringField(max_length=2000), required=False)

#post1 = Post(id=uuid.uuid4(), content="abacdcad", notes=["a", "b"])
#post2 = Post(id=uuid.uuid4(), content="aaaaa", notes=["a", "bc"])

#post1.save()
#post2.save()

post_fields = {
    'content': fields.String,
    'notes': fields.List(fields.String),
    'URI': fields.Url('my_endpoint')
}

class PostResource(Resource):
    @marshal_with(post_fields)
    def get(self):
        queryset = [obj.to_mongo().to_dict() for obj in Post.objects]
        return queryset


class PostEndpoint(Resource):

    def get(self, _id):
        # Query db for this entry.
        return None


api.add_resource(PostResource, '/posts')
api.add_resource(PostEndpoint, '/post/<string:_id>', endpoint='my_endpoint')

if __name__ == '__main__':
    app.run(debug=True)

Note above that _id represents id of entry in document. Mongoengine returns it like that, not sure why. Maybe primary key is represented with underscore in front of it.

Response of http://127.0.0.1:5000/posts should be (URI is from my example here):

[
  {
    "content": "Dummy content 1",
    "notes": [
      "a",
      "b"
    ],
    "URI": "/post/0b689467-41cc-4bb7-a606-881f6554a6b7"
  },
  {
    "content": "Dummy content 2",
    "notes": [
      "c",
      "d"
    ],
    "URI": "/post/8e8c1837-fdd2-4891-90cf-72edc0f4c19a"
  }
]

I hope this clears out the problem.

Upvotes: 1

Related Questions