Browse Source

Merge branch 'rest-api'

sh4nks 10 years ago
parent
commit
19304f7ce1

+ 33 - 0
flaskbb/api/__init__.py

@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.api
+    ~~~~~~~~~~~
+
+    The API provides the possibility to get the data in JSON format
+    for the views.
+
+    :copyright: (c) 2015 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+from flask import jsonify, make_response
+
+from flask_httpauth import HTTPBasicAuth
+
+from flaskbb.user.models import User
+
+auth = HTTPBasicAuth()
+
+
+@auth.verify_password
+def verify_password(username, password):
+    user, authenticated = User.authenticate(username, password)
+    if user and authenticated:
+        return True
+    return False
+
+
+@auth.error_handler
+def unauthorized():
+    # return 403 instead of 401 to prevent browsers from displaying the default
+    # auth dialog
+    return make_response(jsonify({'message': 'Unauthorized access'}), 403)

+ 219 - 0
flaskbb/api/forums.py

@@ -0,0 +1,219 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.api.forums
+    ~~~~~~~~~~~~~~~~~~
+
+    The Forum API.
+    TODO: - POST/PUT/DELETE stuff
+          - Permission checks.
+
+    :copyright: (c) 2015 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+from flask_restful import Resource, fields, marshal, abort, reqparse
+
+from flaskbb.forum.models import Category, Forum, Topic, Post
+
+
+category_fields = {
+    'id': fields.Integer,
+    'title': fields.String,
+    'description': fields.String,
+    'slug': fields.String,
+    'forums': fields.String(attribute="forums.title")
+}
+
+forum_fields = {
+    'id': fields.Integer,
+    'category_id': fields.Integer,
+    'title': fields.String,
+    'description': fields.String,
+    'position': fields.Integer,
+    'locked': fields.Boolean,
+    'show_moderators': fields.Boolean,
+    'external': fields.String,
+    'post_count': fields.Integer,
+    'topic_count': fields.Integer,
+    'last_post_id': fields.Integer,
+    'last_post_title': fields.String,
+    'last_post_created': fields.DateTime,
+    'last_post_username': fields.String
+}
+
+topic_fields = {
+    'id': fields.Integer,
+    'forum_id': fields.Integer,
+    'title': fields.String,
+    'user_id': fields.Integer,
+    'username': fields.String,
+    'date_created': fields.DateTime,
+    'last_updated': fields.DateTime,
+    'locked': fields.Boolean,
+    'important': fields.Boolean,
+    'views': fields.Integer,
+    'post_count': fields.Integer,
+    'content': fields.String(attribute='first_post.content'),
+    'first_post_id': fields.Integer,
+    'last_post_id': fields.Integer,
+}
+
+post_fields = {
+    'id': fields.Integer,
+    'topic_id': fields.Integer,
+    'user_id': fields.Integer,
+    'username': fields.String,
+    'content': fields.String,
+    'date_created': fields.DateTime,
+    'date_modified': fields.DateTime,
+    'modified_by': fields.String
+}
+
+
+class CategoryListAPI(Resource):
+
+    def __init__(self):
+        super(CategoryListAPI, self).__init__()
+
+    def get(self):
+        categories_list = Category.query.order_by(Category.position).all()
+
+        categories = {'categories': [marshal(category, category_fields)
+                                     for category in categories_list]}
+        return categories
+
+
+class CategoryAPI(Resource):
+
+    def __init__(self):
+        super(CategoryAPI, self).__init__()
+
+    def get(self, id):
+        category = Category.query.filter_by(id=id).first()
+
+        if not category:
+            abort(404)
+
+        return {'category': marshal(category, category_fields)}
+
+
+class ForumListAPI(Resource):
+
+    def __init__(self):
+        self.reqparse = reqparse.RequestParser()
+        self.reqparse.add_argument('category_id', type=int, location='args')
+        super(ForumListAPI, self).__init__()
+
+    def get(self):
+        # get the forums for a category or get all
+        args = self.reqparse.parse_args()
+        if args['category_id'] is not None:
+            forums_list = Forum.query.\
+                filter(Forum.category_id == args['category_id']).\
+                order_by(Forum.position).all()
+        else:
+            forums_list = Forum.query.order_by(Forum.position).all()
+
+        forums = {'forums': [marshal(forum, forum_fields)
+                             for forum in forums_list]}
+        return forums
+
+
+class ForumAPI(Resource):
+
+    def __init__(self):
+        super(ForumAPI, self).__init__()
+
+    def get(self, id):
+        forum = Forum.query.filter_by(id=id).first()
+
+        if not forum:
+            abort(404)
+
+        return {'forum': marshal(forum, forum_fields)}
+
+
+class TopicListAPI(Resource):
+
+    def __init__(self):
+        self.reqparse = reqparse.RequestParser()
+        self.reqparse.add_argument('page', type=int, location='args')
+        self.reqparse.add_argument('per_page', type=int, location='args')
+        self.reqparse.add_argument('forum_id', type=int, location='args')
+        super(TopicListAPI, self).__init__()
+
+    def get(self):
+        args = self.reqparse.parse_args()
+        page = args['page'] or 1
+        per_page = args['per_page'] or 20
+        forum_id = args['forum_id']
+
+        if forum_id is not None:
+            topics_list = Topic.query.filter_by(forum_id=forum_id).\
+                order_by(Topic.important.desc(), Topic.last_updated.desc()).\
+                paginate(page, per_page, True)
+        else:
+            topics_list = Topic.query.\
+                order_by(Topic.important.desc(), Topic.last_updated.desc()).\
+                paginate(page, per_page, True)
+
+        topics = {'topics': [marshal(topic, topic_fields)
+                             for topic in topics_list.items]}
+        return topics
+
+
+class TopicAPI(Resource):
+
+    def __init__(self):
+        super(TopicAPI, self).__init__()
+
+    def get(self, id):
+        topic = Topic.query.filter_by(id=id).first()
+
+        if not topic:
+            abort(404)
+
+        return {'topic': marshal(topic, topic_fields)}
+
+
+class PostListAPI(Resource):
+
+    def __init__(self):
+        self.reqparse = reqparse.RequestParser()
+        self.reqparse.add_argument('page', type=int, location='args')
+        self.reqparse.add_argument('per_page', type=int, location='args')
+        self.reqparse.add_argument('topic_id', type=int, location='args')
+        super(PostListAPI, self).__init__()
+
+    def get(self):
+        args = self.reqparse.parse_args()
+        page = args['page'] or 1
+        per_page = args['per_page'] or 20
+        topic_id = args['topic_id']
+
+        if topic_id is not None:
+            posts_list = Post.query.\
+                filter_by(topic_id=id).\
+                order_by(Post.id.asc()).\
+                paginate(page, per_page)
+        else:
+            posts_list = Post.query.\
+                order_by(Post.id.asc()).\
+                paginate(page, per_page)
+
+        posts = {'posts': [marshal(post, post_fields)
+                           for post in posts_list.items]}
+        return posts
+
+
+class PostAPI(Resource):
+
+    def __init__(self):
+        super(PostAPI, self).__init__()
+
+    def get(self, id):
+        post = Post.query.filter_by(id=id).first()
+
+        if not post:
+            abort(404)
+
+        return {'post': marshal(post, post_fields)}

+ 131 - 0
flaskbb/api/users.py

@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.api.users
+    ~~~~~~~~~~~~~~~~~
+
+    The User API.
+    TODO: Permission checks.
+
+    :copyright: (c) 2015 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+from datetime import datetime
+
+from flask_restful import Resource, reqparse, fields, marshal, abort
+
+from flaskbb.api import auth
+from flaskbb.user.models import User
+
+# CREATE NEW USER
+# curl -u test:test1 -i -H "Content-Type: application/json" -X POST -d '{"username":"test6", "password": "test", "email": "test6@example.org"}' http://localhost:8080/api/users
+
+# UPDATE USER
+# curl -u test1:test -i -H "Content-Type: application/json" -X PUT -d '{"email": "test7@example.org"}' http://localhost:8080/api/users/5
+
+# GET USER
+# curl -i http://localhost:8080/api/users
+
+user_fields = {
+    'id': fields.Integer,
+    'username': fields.String,
+    'email': fields.String,
+    'date_joined': fields.DateTime,
+    'lastseen': fields.DateTime,
+    'birthday': fields.DateTime,
+    'gender': fields.String,
+    'website': fields.String,
+    'location': fields.String,
+    'signature': fields.String,
+    'notes': fields.String,
+    'theme': fields.String,
+    'language': fields.String,
+    'post_count': fields.Integer,
+    'primary_group': fields.String(attribute="primary_group.name")
+}
+
+
+class UserListAPI(Resource):
+
+    def __init__(self):
+        self.reqparse = reqparse.RequestParser()
+        self.reqparse.add_argument('username', type=str, required=True,
+                                   location="json")
+        self.reqparse.add_argument('email', type=str, required=True,
+                                   location='json')
+        self.reqparse.add_argument('password', type=str, required=True,
+                                   location='json')
+        self.user_fields = user_fields
+        super(UserListAPI, self).__init__()
+
+    def get(self):
+        users = {'users': [marshal(user, user_fields)
+                           for user in User.query.all()]}
+        return users
+
+    @auth.login_required
+    def post(self):
+        args = self.reqparse.parse_args()
+        user = User(username=args['username'],
+                    password=args['password'],
+                    email=args['email'],
+                    date_joined=datetime.utcnow(),
+                    primary_group_id=4)
+        user.save()
+
+        return {'user': marshal(user, user_fields)}, 201
+
+
+class UserAPI(Resource):
+
+    def __init__(self):
+        self.reqparse = reqparse.RequestParser()
+        self.reqparse.add_argument('email', type=str, location='json')
+        self.reqparse.add_argument('birthday', type=str, location='json')
+        self.reqparse.add_argument('gender', type=str, location='json')
+        self.reqparse.add_argument('website', type=str, location='json')
+        self.reqparse.add_argument('location', type=str, location='json')
+        self.reqparse.add_argument('signature', type=str, location='json')
+        self.reqparse.add_argument('notes', type=str, location='json')
+        self.reqparse.add_argument('theme', type=str, location='json')
+        self.reqparse.add_argument('language', type=str, location='json')
+
+        super(UserAPI, self).__init__()
+
+    def get(self, id):
+        print auth.username()
+        user = User.query.filter_by(id=id).first()
+
+        if not user:
+            abort(404)
+
+        return {'user': marshal(user, user_fields)}
+
+    @auth.login_required
+    def put(self, id):
+        user = User.query.filter_by(id=id).first()
+
+        if not user:
+            abort(404)
+
+        if user.username != auth.username():
+            abort(403, message="You are not allowed to modify this user.")
+
+        args = self.reqparse.parse_args()
+        for k, v in args.items():
+            if v is not None:
+                setattr(user, k, v)
+        user.save()
+        return {'user': marshal(user, user_fields)}
+
+    @auth.login_required
+    def delete(self, id):
+        user = User.query.filter_by(id=id).first()
+
+        if not user:
+            abort(404)
+
+        if user.username != auth.username() and not user.permissions['admin']:
+            abort(403, message="You are not allowed to delete this user.")
+
+        user.delete()
+        return {'result': True}

+ 67 - 1
flaskbb/app.py

@@ -32,7 +32,7 @@ from flaskbb.forum.views import forum
 from flaskbb.forum.models import Post, Topic, Category, Forum
 # extensions
 from flaskbb.extensions import db, login_manager, mail, cache, redis_store, \
-    debugtoolbar, migrate, themes, plugin_manager, babel
+    debugtoolbar, migrate, themes, plugin_manager, babel, restful
 # various helpers
 from flaskbb.utils.helpers import format_date, time_since, crop_title, \
     is_online, render_markup, mark_online, forum_is_unread, topic_is_unread, \
@@ -60,6 +60,7 @@ def create_app(config=None):
     # try to update the config via the environment variable
     app.config.from_envvar("FLASKBB_SETTINGS", silent=True)
 
+    configure_api(app)
     configure_blueprints(app)
     configure_extensions(app)
     configure_template_filters(app)
@@ -80,10 +81,75 @@ def configure_blueprints(app):
     )
 
 
+def configure_api(app):
+    from flaskbb.api.users import UserAPI, UserListAPI
+    from flaskbb.api.forums import (CategoryListAPI, CategoryAPI,
+                                    ForumListAPI, ForumAPI,
+                                    TopicListAPI, TopicAPI,
+                                    PostListAPI, PostAPI)
+    # User API
+    restful.add_resource(
+        UserListAPI,
+        "{}/users".format(app.config["API_URL_PREFIX"]),
+        endpoint='tasks'
+    )
+    restful.add_resource(
+        UserAPI,
+        '{}/users/<int:id>'.format(app.config["API_URL_PREFIX"]),
+        endpoint='task'
+    )
+
+    # Forum API
+    restful.add_resource(
+        CategoryListAPI,
+        "{}/categories".format(app.config["API_URL_PREFIX"]),
+        endpoint='categories'
+    )
+    restful.add_resource(
+        CategoryAPI,
+        '{}/categories/<int:id>'.format(app.config["API_URL_PREFIX"]),
+        endpoint='category'
+    )
+    restful.add_resource(
+        ForumListAPI,
+        "{}/forums".format(app.config["API_URL_PREFIX"]),
+        endpoint='forums'
+    )
+    restful.add_resource(
+        ForumAPI,
+        '{}/forums/<int:id>'.format(app.config["API_URL_PREFIX"]),
+        endpoint='forum'
+    )
+    restful.add_resource(
+        TopicListAPI,
+        "{}/topics".format(app.config["API_URL_PREFIX"]),
+        endpoint='topics'
+    )
+    restful.add_resource(
+        TopicAPI,
+        '{}/topics/<int:id>'.format(app.config["API_URL_PREFIX"]),
+        endpoint='topic'
+    )
+    restful.add_resource(
+        PostListAPI,
+        "{}/posts".format(app.config["API_URL_PREFIX"]),
+        endpoint='posts'
+    )
+    restful.add_resource(
+        PostAPI,
+        '{}/posts/<int:id>'.format(app.config["API_URL_PREFIX"]),
+        endpoint='post'
+    )
+
+    # Management API
+
+
 def configure_extensions(app):
     """
     Configures the extensions
     """
+    # Flask-Restful
+    restful.init_app(app)
 
     # Flask-Plugins
     plugin_manager.init_app(app)

+ 1 - 0
flaskbb/configs/default.py

@@ -88,3 +88,4 @@ class DefaultConfig(object):
     USER_URL_PREFIX = "/user"
     AUTH_URL_PREFIX = "/auth"
     ADMIN_URL_PREFIX = "/admin"
+    API_URL_PREFIX = "/api"

+ 2 - 1
flaskbb/configs/production.py.example

@@ -85,8 +85,9 @@ class ProductionConfig(DefaultConfig):
     REDIS_URL = "redis://:password@localhost:6379"
     REDIS_DATABASE = 0
 
-    # URL Prefixes.
+    # URL Prefixes. Only change it when you know what you are doing.
     FORUM_URL_PREFIX = ""
     USER_URL_PREFIX = "/user"
     AUTH_URL_PREFIX = "/auth"
     ADMIN_URL_PREFIX = "/admin"
+    API_URL_PREFIX = "/api"

+ 7 - 0
flaskbb/extensions.py

@@ -18,6 +18,9 @@ from flask_migrate import Migrate
 from flask_themes2 import Themes
 from flask_plugins import PluginManager
 from flask_babelex import Babel
+from flask_restful import Api
+from flask_httpauth import HTTPBasicAuth
+
 
 # Database
 db = SQLAlchemy()
@@ -48,3 +51,7 @@ plugin_manager = PluginManager()
 
 # Babel
 babel = Babel()
+
+# Flask-Restful with Auth
+restful = Api()
+auth = HTTPBasicAuth()

+ 1 - 1
flaskbb/templates/forum/forum.html

@@ -114,7 +114,7 @@
         {% else %}
         <tr>
             <td colspan="5">
-                {% trans %}No topics.{% endtrans %}
+                {% trans %}No Topics.{% endtrans %}
             </td>
         </tr>
         {% endfor %}

+ 1 - 0
requirements.txt

@@ -11,6 +11,7 @@ Flask-Mail==0.9.1
 Flask-Migrate==1.3.0
 Flask-Plugins==1.5
 Flask-Redis==0.0.6
+Flask-RESTful==0.3.1
 Flask-Script==2.0.5
 Flask-SQLAlchemy==2.0
 Flask-Themes2==0.1.3