Skip to content

Commit

Permalink
Merge pull request #1333 from Scifabric/json-admin
Browse files Browse the repository at this point in the history
Json admin
  • Loading branch information
teleyinex authored Dec 29, 2016
2 parents 8974788 + dd9c6db commit 9a0133d
Show file tree
Hide file tree
Showing 16 changed files with 454 additions and 48 deletions.
74 changes: 74 additions & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,77 @@ Create a Project object:
.. code-block:: bash
curl -X POST -H "Content-Type:application/json" -s -d '{"name":"myproject", "info":{"xyz":1}}' 'http://localhost:5000/api/project?api_key=API-KEY'
PYBOSSA endpoints
-----------------

The following endpoints of PYBOSSA server can be requested setting the header *Content-Type* to *application/json* so you can retrieve the data using JavaScript.

.. note::
If a key has the value **null** is because, that view is not populating that specific field. However, that value should be retrieved in a different one. Please, see all the documentation.

Account index
~~~~~~~~~~~~~

**Endpoint: /account/page/<int:page>**

It returns a JSON object with the following information:

* **accounts**: this key holds the list of accounts for the given page.
* **pagination**: this key holds the pagination information.
* **top_users**: this key holds the top users (including the user if authenticated) with their rank and scores.
* **update_feed**: the latest actions in the server (users created, contributions, new tasks, etc.).
* **template**: the Jinja2 template that should be rendered in case of text/html.
* **title**: the title for the endpoint.

**Example output**

.. code-block:: python
{
"accounts": [
{
"created": "2015-06-10T15:02:38.411497",
"fullname": "Scifabric",
"info": {
"avatar": "avatar.png",
"container": "user_234234dd3"
},
"locale": null,
"name": "Scifabric",
"rank": null,
"registered_ago": "1 year ago",
"score": null,
"task_runs": 3
},
],
"pagination": {
"next": true,
"page": 3,
"per_page": 24,
"prev": true,
"total": 11121
},
"template": "account/index.html",
"title": "Community",
"top_users": [
{
"created": "2014-08-17T18:28:56.738119",
"fullname": "Buzz Bot",
"info": {
"avatar": "avatar.png",
"container": "user_55"
},
"locale": null,
"name": "buzzbot",
"rank": 1,
"registered_ago": null,
"score": 54247,
"task_runs": null
},
],
"total": 11121,
"update_feed": []
}
13 changes: 11 additions & 2 deletions pybossa/cache/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ def get_leaderboard(n, user_id=None):

results = session.execute(sql, dict(limit=n))

u = User()

top_users = []
user_in_top = False
for row in results:
Expand All @@ -59,7 +61,8 @@ def get_leaderboard(n, user_id=None):
info=row.info,
created=row.created,
score=row.score)
top_users.append(user)
tmp = u.to_public_json(data=user)
top_users.append(tmp)
if (user_id is not None):
if not user_in_top:
sql = text('''
Expand Down Expand Up @@ -87,6 +90,7 @@ def get_leaderboard(n, user_id=None):
info=u.info,
created=u.created,
score=-1)
user = u.to_public_json(data=user)
for row in user_rank: # pragma: no cover
user = dict(
rank=row.rank,
Expand All @@ -97,6 +101,7 @@ def get_leaderboard(n, user_id=None):
info=row.info,
created=row.created,
score=row.score)
user = u.to_public_json(data=user)
top_users.append(user)

return top_users
Expand Down Expand Up @@ -270,12 +275,16 @@ def get_users_page(page, per_page=24):
ORDER BY "user".created DESC LIMIT :limit OFFSET :offset''')
results = session.execute(sql, dict(limit=per_page, offset=offset))
accounts = []

u = User()

for row in results:
user = dict(id=row.id, name=row.name, fullname=row.fullname,
email_addr=row.email_addr, created=row.created,
task_runs=row.task_runs, info=row.info,
registered_ago=pretty_date(row.created))
accounts.append(user)
tmp = u.to_public_json(data=user)
accounts.append(tmp)
return accounts


Expand Down
20 changes: 15 additions & 5 deletions pybossa/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import os
import logging
import humanize
from werkzeug.exceptions import Forbidden, Unauthorized, InternalServerError
from werkzeug.exceptions import NotFound
from flask import Flask, url_for, request, render_template, \
flash, _app_ctx_stack
from flask.ext.login import current_user
Expand All @@ -28,7 +30,7 @@
from pybossa.extensions import *
from pybossa.ratelimit import get_view_rate_limit
from raven.contrib.flask import Sentry
from pybossa.util import pretty_date
from pybossa.util import pretty_date, handle_content_type
from pybossa.news import FEED_KEY as NEWS_FEED_KEY
from pybossa.news import get_news

Expand Down Expand Up @@ -435,19 +437,27 @@ def setup_error_handlers(app):
"""Setup error handlers."""
@app.errorhandler(404)
def _page_not_found(e):
return render_template('404.html'), 404
response = dict(template='404.html', code=404,
description=NotFound.description)
return handle_content_type(response)

@app.errorhandler(500)
def _server_error(e): # pragma: no cover
return render_template('500.html'), 500
response = dict(template='500.html', code=500,
description=InternalServerError.description)
return handle_content_type(response)

@app.errorhandler(403)
def _forbidden(e):
return render_template('403.html'), 403
response = dict(template='403.html', code=403,
description=Forbidden.description)
return handle_content_type(response)

@app.errorhandler(401)
def _unauthorized(e):
return render_template('401.html'), 401
response = dict(template='401.html', code=401,
description=Unauthorized.description)
return handle_content_type(response)


def setup_hooks(app):
Expand Down
7 changes: 5 additions & 2 deletions pybossa/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@
__all__ = ['sentinel', 'db', 'signer', 'mail', 'login_manager', 'facebook',
'twitter', 'google', 'misaka', 'babel', 'uploader', 'debug_toolbar',
'csrf', 'timeouts', 'ratelimits', 'user_repo', 'project_repo',
'task_repo', 'blog_repo', 'auditlog_repo', 'newsletter', 'importer',
'flickr', 'plugin_manager', 'assets']
'task_repo', 'blog_repo', 'auditlog_repo', 'webhook_repo',
'result_repo', 'newsletter', 'importer', 'flickr',
'plugin_manager', 'assets']

# CACHE
from pybossa.sentinel import Sentinel
Expand All @@ -55,6 +56,8 @@
blog_repo = None
task_repo = None
auditlog_repo = None
webhook_repo = None
result_repo = None

# Signer
from pybossa.signer import Signer
Expand Down
32 changes: 32 additions & 0 deletions pybossa/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,38 @@ def dictize(self):
out[col.name] = getattr(self, col.name)
return out

def info_public_keys(self, data=None):
"""Return a dictionary of info field with public keys."""
out = dict()
if data is None:
data = self.dictize()
for key in self.public_info_keys():
if data.get('info'):
out[key] = data.get('info').get(key)
return out

def to_public_json(self, data=None):
"""Return a dict that can be exported to JSON
with only public attributes."""

out = dict()
if data is None:
data = self.dictize()
for col in self.public_attributes():
if col == 'info':
out[col] = self.info_public_keys(data=data)
else:
out[col] = data.get(col)
return out

def public_attributes(self): # pragma: no cover
"""To be override by other class."""
pass

def public_info_keys(self): # pragma: no cover
"""To be override by other class."""
pass

@classmethod
def undictize(cls, dict_):
raise NotImplementedError()
Expand Down
6 changes: 3 additions & 3 deletions pybossa/model/event_listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

@event.listens_for(Blogpost, 'after_insert')
def add_blog_event(mapper, conn, target):
"""Update PyBossa feed with new blog post."""
"""Update PYBOSSA feed with new blog post."""
sql_query = ('select name, short_name, info from project \
where id=%s') % target.project_id
results = conn.execute(sql_query)
Expand Down Expand Up @@ -90,8 +90,8 @@ def add_task_event(mapper, conn, target):

@event.listens_for(User, 'after_insert')
def add_user_event(mapper, conn, target):
"""Update PyBossa feed with new user."""
obj = target.dictize()
"""Update PYBOSSA feed with new user."""
obj = target.to_public_json()
obj['action_updated']='User'
update_feed(obj)

Expand Down
12 changes: 11 additions & 1 deletion pybossa/model/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@


class User(db.Model, DomainObject, UserMixin):
'''A registered user of the PyBossa system'''
'''A registered user of the PYBOSSA system'''

__tablename__ = 'user'

Expand Down Expand Up @@ -82,3 +82,13 @@ def check_password(self, password):
if self.passwd_hash:
return signer.check_password_hash(self.passwd_hash, password)
return False

@classmethod
def public_attributes(self):
"""Return a list of public attributes."""
return ['created', 'name', 'fullname', 'locale', 'info',
'task_runs', 'registered_ago', 'rank', 'score']

def public_info_keys(self):
"""Return a list of public info keys."""
return ['avatar', 'container']
32 changes: 32 additions & 0 deletions pybossa/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,36 @@
import codecs
import cStringIO
from flask import abort, request, make_response, current_app
from flask import render_template, jsonify
from functools import wraps
from flask.ext.login import current_user
from math import ceil
import json


def handle_content_type(data):
"""Return HTML or JSON based on request type."""
if request.headers['Content-Type'] == 'application/json':
if 'form' in data.keys():
del data['form']
if 'pagination' in data.keys():
pagination = data['pagination'].to_json()
data['pagination'] = pagination
if 'code' in data.keys():
return jsonify(data), data['code']
else:
return jsonify(data)
else:
template = data['template']
del data['template']
if 'code' in data.keys():
error_code = data['code']
del data['code']
return render_template(template, **data), error_code
else:
return render_template(template, **data)


def jsonpify(f):
"""Wrap JSONified output for JSONP."""
@wraps(f)
Expand Down Expand Up @@ -191,6 +215,14 @@ def iter_pages(self, left_edge=0, left_current=2, right_current=3,
yield num
last = num

def to_json(self):
"""Return the object in JSON format."""
return dict(page=self.page,
per_page=self.per_page,
total=self.total_count,
next=self.has_next,
prev=self.has_prev)


def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
"""Unicode CSV reader."""
Expand Down
31 changes: 15 additions & 16 deletions pybossa/view/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
import pybossa.model as model
from flask.ext.babel import gettext
from pybossa.core import signer, uploader, sentinel, newsletter
from pybossa.util import Pagination
from pybossa.util import get_user_signup_method
from pybossa.util import Pagination, handle_content_type
from pybossa.util import get_user_signup_method, handle_content_type
from pybossa.cache import users as cached_users
from pybossa.auth import ensure_authorized_to
from pybossa.jobs import send_mail
Expand All @@ -58,12 +58,8 @@
@blueprint.route('/', defaults={'page': 1})
@blueprint.route('/page/<int:page>')
def index(page):
"""
Index page for all PyBossa registered users.
Returns a Jinja2 rendered template with the users.
"""Index page for all PYBOSSA registered users."""

"""
update_feed = get_update_feed()
per_page = 24
count = cached_users.get_total_users()
Expand All @@ -77,11 +73,12 @@ def index(page):
user_id = None
top_users = cached_users.get_leaderboard(current_app.config['LEADERBOARD'],
user_id)
return render_template('account/index.html', accounts=accounts,
total=count,
top_users=top_users,
title="Community", pagination=pagination,
update_feed=update_feed)
tmp = dict(template='account/index.html', accounts=accounts,
total=count,
top_users=top_users,
title="Community", pagination=pagination,
update_feed=update_feed)
return handle_content_type(tmp)


@blueprint.route('/signin', methods=['GET', 'POST'])
Expand Down Expand Up @@ -124,10 +121,12 @@ def signin():
auth['facebook'] = True
if ('google' in current_app.blueprints): # pragma: no cover
auth['google'] = True
return render_template('account/signin.html',
title="Sign in",
form=form, auth=auth,
next=request.args.get('next'))
response = dict(template='account/signin.html',
title="Sign in",
form=form,
auth=auth,
next=request.args.get('next'))
return handle_content_type(response)
else:
# User already signed in, so redirect to home page
return redirect(url_for("home.home"))
Expand Down
Loading

0 comments on commit 9a0133d

Please sign in to comment.