import datetime from flask import Blueprint, jsonify, current_app, request, abort, url_for from flask_pymongo import PyMongo from marshmallow import ValidationError import pymongo import pytz from .schema import NodeSchema # Setup PyMongo mongo = PyMongo(current_app, tz_aware=True) mongo.db.nodes.create_index([('fields.value', pymongo.TEXT), ('name', pymongo.TEXT)], name='fields.value_text_name_text') mongo.db.nodes.create_index([('parent_id', pymongo.ASCENDING)], name='parent_id') app = Blueprint('api', __name__) # Error handling @app.errorhandler(400) def on_invalid_request(e): return jsonify({'message': e.description or 'Invalid request'}), 400 @app.errorhandler(404) def on_not_found(e): return jsonify({'message': e.description or 'Not found'}), 404 @app.errorhandler(500) def on_internal_error(e): return jsonify({'message': e.description or 'Invalid request'}), 500 @app.errorhandler(ValidationError) def on_validation_error(e): return jsonify({'message': 'Validation error', 'fields': e.messages}), 400 # Routes @app.route('/nodes') def root_nodes(): schema = NodeSchema(many=True) data = schema.dump(mongo.db.nodes.find({'parent_id': None})) return jsonify(data) @app.route('/nodes', methods=['POST']) def add_node(): data = request.json if data is None or not isinstance(data, dict): abort(400, 'Payload must be a JSON object') schema = NodeSchema() node = schema.load(data) node['created_at'] = pytz.utc.localize(datetime.datetime.utcnow()) result = mongo.db.nodes.insert_one(node) if not result.acknowledged: abort(500, 'Write operation not acknowledged') node_id = result.inserted_id return jsonify({'id': str(node_id), 'url': url_for('.node', node_id=node_id)}), 201 @app.route('/nodes/') def node(node_id): result = mongo.db.nodes.aggregate([ {'$match': {'_id': node_id}}, { '$graphLookup': { 'from': 'nodes', 'startWith': '$parent_id', 'connectFromField': 'parent_id', 'connectToField': '_id', 'as': 'parents', }, }, { '$graphLookup': { 'from': 'nodes', 'startWith': '$_id', 'connectFromField': '_id', 'connectToField': 'parent_id', 'as': 'children', 'maxDepth': 1, }, }, ]) try: node = result.next() except StopIteration: abort(404, 'No node found') parents = dict((parent['_id'], parent) for parent in node.pop('parents')) current = node # Recursively assign each node their respective parent while current: # NOTE: Python assigns from left to right current['parent'] = current = parents.get(current.get('parent_id')) children = dict((child['_id'], child) for child in node.pop('children')) children_list = node['children'] = [] for child in children.values(): # Direct children should be assigned to the top-most list if child['parent_id'] == node['_id']: children_list.append(child) continue # Otherwise assign to their respective parent parent = children[child['parent_id']] parent.setdefault('children', []).append(child) schema = NodeSchema() return jsonify(schema.dump(node)) @app.route('/nodes/', methods=['PUT']) def update_node(node_id): data = request.json if data is None or not isinstance(data, dict): abort(400, 'Payload must be a JSON object') schema = NodeSchema() node = schema.load(data) node['updated_at'] = pytz.utc.localize(datetime.datetime.utcnow()) result = mongo.db.nodes.update_one({'_id': node_id}, {'$set': node}) if not result.acknowledged: abort(500, 'Write operation not acknowledged') return '', 204 @app.route('/nodes/', methods=['DELETE']) def delete_node(node_id): result = mongo.db.nodes.delete_one({'_id': node_id}) if result.deleted_count == 0: abort(404, 'No node found') return jsonify({}), 204 @app.route('/search', methods=['POST']) def find_nodes(): if 'q' not in request.form: abort(400, 'Missing q argument') schema = NodeSchema(many=True) data = schema.dump(mongo.db.nodes.find({'$text': {'$search': request.form['q']}})) return jsonify(data) @app.route('/') def index(): return 'api'