Search code examples
pythonmongodbpostmanpymongo

WriteError in pymongo through using test in Postman


I am getting this error in my application:

this error

after trying to do a test case like this:

test case in Postman

Here is my application code, I am quite unsure as to why python is throwing this error at me. It seems to not like me trying to add a new review through Postman which is in this format here:

enter image description here

from flask import Flask, request, jsonify, make_response
from pymongo import MongoClient
from bson import ObjectId
import json
import jwt
import datetime
from functools import wraps 
import bcrypt

def jwt_required(func):
    @wraps(func)
    def jwt_required_wrapper(*args, **kwargs):
        token = None
        if 'x-access-token' in request.headers:
            token = request.headers['x-access-token']
        if not token:
            return jsonify( { 'message' : 'Token is missing' }), 401
        try:
            data = jwt.decode(token, app.config['SECRET_KEY'])
        except:
            return jsonify({ 'message' : 'Token is invalid' }), 401
        bl_token = blacklist.find_one( { "token" : token })
        if bl_token is not None:
            return make_response( jsonify ( { "message" : "Token has been cancelled "}), 401)
        return func(*args, **kwargs)
    return jwt_required_wrapper

def admin_required(func):
    @wraps(func)
    def admin_required_wrapper(*args, **kwargs):
        token = request.headers['x-access-token']
        data = jwt.decode(token, app.config['SECRET_KEY'])
        if data["admin"]:
            return func(*args, **kwargs)
        else:
            return make_response( jsonify ( { "message" : "Admin access required"}), 401)
    return admin_required_wrapper

class MyEncoder(json.JSONEncoder):

    def default(self, obj):
        if isinstance(obj, ObjectId):
            return str(obj)
        return super(MyEncoder, self).default(obj)

app = Flask(__name__)

app.config['SECRET_KEY'] = 'password123'

client = MongoClient("mongodb://127.0.0.1:27017")
db = client.bizDB
businesses = db.biz
users = db.users
blacklist = db.blacklist
app.json_encoder = MyEncoder


with app.test_request_context():
    db.mycollection.insert_one({'a': 1})
    doc = db.mycollection.find_one({'a': 1})

    print(jsonify(doc).response)

@app.route("/api/v1.0/businesses", methods=["GET"])
def show_all_businesses():
    page_num, page_size = 1, 10
    if request.args.get("pn"):
        page_num = int(request.args.get("pn"))
    if request.args.get("ps"):
        page_size = int(request.args.get("ps"))
    page_start = page_size * (page_num - 1)

    data_to_return = []
    for business in businesses.find().skip(page_start).limit(page_size):
        business["_id"] = str(business["_id"])
        for review in business["reviews"]:
            review["_id"] = str(review["_id"])
        data_to_return.append(business)

    return make_response( jsonify( data_to_return ), 200 )


@app.route("/api/v1.0/businesses/<string:id>", methods=["GET"])
@jwt_required
def show_one_business(id):
    business = businesses.find_one({"_id":ObjectId(id)})
    if business is not None:
        business["_id"] = str(business["_id"])
        for review in business["reviews"]:
            review["_id"] = str(review["_id"])
        return make_response ( jsonify ( business ), 200)
    else:
        return make_response (jsonify({ "error" : "Invalid Business ID" } ), 404)

@app.route("/api/v1.0/businesses", methods=["POST"])
@jwt_required
def add_business():
    if "name" in request.form and "town" in request.form and "rating" in request.form:
        new_business = { "name" : request.form["name"],
                        "town" : request.form["town"],
                        "rating" : request.form["rating"],
                        "reviews" : {} 
                        }
        new_business_id = businesses.insert_one(new_business)
        new_business_link = "http://localhost:5000/api/v1.0/businesses/" + str(new_business_id.inserted_id)

        return make_response( jsonify( { "url" : new_business_link } ), 201 )
    else:
        return make_response ( jsonify( { "error" : "Missing form data" } ), 404 )


@app.route("/api/v1.0/businesses/<string:id>", methods=["PUT"])
def edit_business(id):
    if "name" in request.form and "town" in request.form and "rating" in request.form:
        result = businesses.update_one(
            {"_id" : ObjectId(id)},
            {
                "$set": { 
                    "name" : request.form["name"],
                    "town" : request.form["town"],
                    "rating" : request.form["rating"]
                }
            }
        )
        if result.matched_count == 1:
            edited_business_link = "http://localhost:5000/api/v1.0/businesses/" + id 
            return make_response( jsonify( { "url": edited_business_link } ), 200 )
        else:     
            return make_response ( jsonify( { "error" : "Invalid business ID" } ), 404 )
    else:
        return make_response ( jsonify( { "error" : "Missing form data" } ), 404 )

        
@app.route("/api/v1.0/businesses/<string:id>", methods=["DELETE"])
@jwt_required
@admin_required
def delete_business(id):
    result = businesses.delete_one( { "_id" : ObjectId(id) })
    if result.deleted_count == 1:
        return make_response( jsonify( {} ), 204)
    else:
        return make_response ( jsonify( { "error" : "Invalid business ID" } ), 404 )



@app.route("/api/v1.0/businesses/<string:id>/reviews", methods=["POST"])
@jwt_required
def add_new_review(id):
    new_review = { 
        "_id" : ObjectId(),
        "username" : request.form["username"],
        "comment" : request.form["comment"],
        "stars" : request.form["stars"]
    }
    businesses.update_one( { "_id" : ObjectId(id)} , {"$push" : { "reviews" : new_review}})
    new_review_link = "http://localhost:5000/api/v1.0/businesses/" + id + "/reviews/" + str(new_review["_id"])
    return make_response( jsonify( { "url" : new_review_link  } ), 201 )


@app.route("/api/v1.0/businesses/<string:id>/reviews", methods=["GET"])
def fetch_all_reviews(id):
    data_to_return = []
    business = businesses.find_one( {"_id" : ObjectId(id) }, { "reviews" : 1, "_id" : 0})
    for review in business["reviews"]:
        review["_id"] = str(review["_id"])
        data_to_return.append(review)
    return make_response( jsonify( data_to_return), 200 )

@app.route("/api/v1.0/businesses/<string:business_id>/reviews/<string:review_id>", methods=["GET"])
def fetch_one_review(business_id, review_id):
    business = businesses.find_one( { "reviews._id" : ObjectId(review_id)}, {"_id" : 0, "reviews.$" : 1} )
    if business is None:
        return make_response ( jsonify( { "error" : "Invalid business ID or Review ID" } ), 404 )
    business["reviews"][0]["_id"] = str(business["reviews"][0]["_id"])
    return make_response( jsonify( business["reviews"][0] ), 200)

@app.route("/api/v1.0/businesses/<string:id>/reviews/<string:review_id>", methods=["PUT"])
def edit_review(id, review_id): 
    edited_review = {
        "reviews.$.username" : request.form["username"],
        "reviews.$.comment" : request.form["comment"],
        "reviews.$.stars" : request.form["stars"]
    }
    businesses.update_one( { "reviews._id" : ObjectId(review_id) }, {"$set" : edited_review })
    edit_review_url = "http://localhost:5000/api/v1.0/businesses/" + id + "/reviews/" + review_id 
    return make_response( jsonify({ "url" : edit_review_url } ), 200 )



@app.route("/api/v1.0/businesses/<string:id>/reviews/<string:review_id>", methods=["DELETE"])
@jwt_required
@admin_required
def delete_review(id, review_id):      
    businesses.update_one( { "_id" : ObjectId(id) }, { "$pull" : {"reviews" : { "_id" : ObjectId(review_id) } } } )    
    return make_response( jsonify( {} ), 204)



@app.route("/api/v1.0/login", methods = ["GET"])
def login():
    auth = request.authorization
    if auth: 
        user = users.find_one( { "username" : auth.username } )
        if user is not None:
            if bcrypt.checkpw(bytes(auth.password, 'UTF-8'), user["password"]):
                token = jwt.encode( {
                    'user' : auth.username,
                    'admin' : user["admin"], 
                    'exp' : datetime.datetime.utcnow() + datetime.timedelta(minutes=30) 
                }, app.config['SECRET_KEY'])
                return make_response (jsonify({ 'token' : token.decode('UTF-8') }), 200)
            else:
                return make_response(jsonify({ "message" : "Bad password" } ), 401 )
        else:
            return make_response(jsonify({ "message" : "Bad username" } ), 401 )

    return make_response(jsonify({ "message" : "Authentication required" } ), 401 )
    
@app.route("/api/v1.0/logout", methods=["GET"])
@jwt_required
def logout():
    token = None
    if 'x-access-token' in request.headers:
        token = request.headers['x-access-token']
    if not token:
        return make_response(jsonify({ "message" : "Token is missing" } ), 401 )
    else:
        blacklist.insert_one( { "token" : token } )
        return make_response(jsonify({ "message" : "Logout Successful" } ), 200 )




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

The error I get back is simply:

pymongo.errors.WriteError: The field 'reviews' must be an array but is of type object in document {_id: ObjectId('5ff3b9a145f6d9013ddb7586')}, full error: {'index': 0, 'code': 2, 'errmsg': "The field 'reviews' must be an array but is of type object in document {_id: ObjectId('5ff3b9a145f6d9013ddb7586')}"}


Solution

  • You're using a $push on a field that isn't an array. Likely because you are setting it to a dict somewhere. Possibly change the line

    "reviews" : {}
    

    to

    "reviews" : []