Skip to content

Commit

Permalink
Merge pull request #1679 from alexandermendes/fix-1593
Browse files Browse the repository at this point in the history
Fix 1593
  • Loading branch information
teleyinex committed Oct 3, 2017
2 parents e23eb09 + cbbe69f commit b59fad1
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 22 deletions.
120 changes: 112 additions & 8 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ If you want, you can order them in descending order::
GET http://{pybossa-site-url}/api/task?orderby=id&desc=true


Check all the attritbutes that you can use to order by in the `Domain Object section <http://docs.pybossa.com/en/latest/model.html>`_.
Check all the attributes that you can use to order by in the `Domain Object section <http://docs.pybossa.com/en/latest/model.html>`_.

.. note::
Please, notice that in order to keep users privacy, only their locale and
Expand Down Expand Up @@ -899,7 +899,7 @@ the following header: "X-CSRFToken".

It returns a JSON object with the following information:

* **flash**: A success message, or error indicating if the request was succesful.
* **flash**: A success message, or error indicating if the request was successful.
* **form**: the form fields with the sent information. It contains the csrf token for validating the post, as well as an errors field in case that something is wrong.

**Example output**
Expand Down Expand Up @@ -1253,7 +1253,7 @@ To send a valid POST request you need to pass the *csrf token* in the headers. U
the following header: "X-CSRFToken".

As this endpoint supports **three** different forms, you must specify which form are
you targetting adding an extra key: **btn**. The options for this key are:
you targeting adding an extra key: **btn**. The options for this key are:

* **Profile**: to update the **form**.
**Upload**: to update the **upload_form**.
Expand All @@ -1265,7 +1265,7 @@ you targetting adding an extra key: **btn**. The options for this key are:

It returns a JSON object with the following information:

* **flash**: A success message, or error indicating if the request was succesful.
* **flash**: A success message, or error indicating if the request was successful.
* **form**: the form fields with the sent information. It contains the csrf token for validating the post, as well as an errors field in case that something is wrong.

**Example output**
Expand Down Expand Up @@ -3339,6 +3339,96 @@ Gives you the global stats of the PYBOSSA server.
}
}
Project Category
~~~~~~~~~~~~~~~~
**Endpoint: /project/category/<short_name>/**
*Allowed methods*: **GET**
**GET**
Gives you the list of projects in a category.
* **pagination**: A pagination object for getting projects from this category.
* **active_cat**: Active category.
* **projects**: List of projects belonging to this category.
* **categories**: List of available categories in this server.
* **template**: The Jinja2 template that could be rendered.
* **title**: the title for the endpoint.
**Example output**
.. code-block:: python
{
"active_cat": {
"created": null,
"description": "Social projects",
"id": 2,
"name": "Social",
"short_name": "social"
},
"categories": [
{
"created": null,
"description": "Featured projects",
"id": null,
"name": "Featured",
"short_name": "featured"
},
{
"created": null,
"description": "Social projects",
"id": 2,
"name": "Social",
"short_name": "social"
},
{
"created": "2013-06-18T11:13:44.789149",
"description": "Art projects",
"id": 3,
"name": "Art",
"short_name": "art"
},
],
"pagination": {
"next": false,
"page": 1,
"per_page": 20,
"prev": false,
"total": 1
},
"projects": [
{
"created": "2014-02-22T15:09:23.691811",
"description": "Image pattern recognition",
"id": 1377,
"info": {
"container": "7",
"thumbnail": "58.png"
},
"last_activity": "2 weeks ago",
"last_activity_raw": "2017-01-31T09:18:28.450391",
"n_tasks": 169671,
"n_volunteers": 17499,
"name": "Name",
"overall_progress": 80,
"owner": "John Doe",
"short_name": "name",
"updated": "2017-01-31T09:18:28.491496"
},
],
"template": "/projects/index.html",
"title": "Projects"
}
.. note::
To override the default ranking you pass the **orderby** query parameter to
sort projects by any of the attributes listed above, such as *n_volunteers*
or *n_tasks*. The **desc** query parameter can also be added to sort in
descending order. For example:
GET /project/category/<short_name>/?orderby=n_tasks&desc=True
Project Category Featured
~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -3350,7 +3440,7 @@ Project Category Featured
Gives you the list of featured projects.
* **pagination**: A pagination object for getting new featured projets from this category.
* **pagination**: A pagination object for getting new featured projects from this category.
* **active_cat**: Active category.
* **projects**: List of projects belonging to this category.
* **categories**: List of available categories in this server.
Expand Down Expand Up @@ -3423,6 +3513,13 @@ Gives you the list of featured projects.
"title": "Projects"
}
.. note::
To override the default ranking you pass the **orderby** query parameter to
sort projects by any of the attributes listed above, such as *n_volunteers*
or *n_tasks*. The **desc** query parameter can also be added to sort in
descending order. For example:
GET /project/category/featured/?orderby=n_tasks&desc=True
Project Category Draft
~~~~~~~~~~~~~~~~~~~~~~
**Endpoint: /project/category/draft/**
Expand Down Expand Up @@ -3506,6 +3603,13 @@ Gives you the list of featured projects.
"title": "Projects"
}
.. note::
To override the default ranking you pass the **orderby** query parameter to
sort projects by any of the attributes listed above, such as *n_volunteers*
or *n_tasks*. The **desc** query parameter can also be added to sort in
descending order. For example:
GET /project/category/draft/?orderby=n_tasks&desc=True
Project Creation
~~~~~~~~~~~~~~~~~~~~~~
**Endpoint: /project/new**
Expand Down Expand Up @@ -3554,7 +3658,7 @@ Gives you the list of posted blogs by the given project short name.
* **project**: Info about the project.
The project and owner fields will have more information if the onwer of the project does the request, providing its private information like api_key, password keys, etc. Otherwise it will be removed and only show public info.
The project and owner fields will have more information if the owner of the project does the request, providing its private information like api_key, password keys, etc. Otherwise it will be removed and only show public info.
**Example public output**
Expand Down Expand Up @@ -3808,7 +3912,7 @@ the following header: "X-CSRFToken". You will have to POST the data fields found
as it contains the information about the fields: specifically **editor** with the HTML/CSS/JS that you want
to provide.
If the post is successfull, you will get the following output:
If the post is successful, you will get the following output:
**Example output**
Expand Down Expand Up @@ -4024,7 +4128,7 @@ To send a valid POST request you need to pass the *csrf token* in the headers. U
the following header: "X-CSRFToken".
As this endpoint supports **two** different forms, you must specify which form are
you targetting adding an extra key: **btn**. The options for this key are:
you targeting adding an extra key: **btn**. The options for this key are:
**Upload**: to update the **upload_form**.
Expand Down
14 changes: 10 additions & 4 deletions pybossa/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,9 +402,12 @@ def username_from_full_name(username):
return username.encode('ascii', 'ignore').decode('utf-8').lower().replace(' ', '')


def rank(projects):
"""Takes a list of (published) projects (as dicts) and orders them by
activity, number of volunteers, number of tasks and other criteria."""
def rank(projects, order_by=None, desc=False):
"""By default, takes a list of (published) projects (as dicts) and orders
them by activity, number of volunteers, number of tasks and other criteria.
Alternatively ranks by order_by and desc.
"""
def earned_points(project):
points = 0
if project['overall_progress'] != 100L:
Expand All @@ -419,7 +422,10 @@ def earned_points(project):
points += _last_activity_points(project)
return points

projects.sort(key=earned_points, reverse=True)
if order_by:
projects.sort(key=lambda x: x[str(order_by)], reverse=desc)
else:
projects.sort(key=earned_points, reverse=True)
return projects


Expand Down
23 changes: 13 additions & 10 deletions pybossa/view/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,29 +151,27 @@ def pro_features(owner=None):
@blueprint.route('/category/featured/page/<int:page>/')
def index(page):
"""List projects in the system"""
order_by = request.args.get('orderby', None)
desc = bool(request.args.get('desc', False))
if cached_projects.n_count('featured') > 0:
return project_index(page, cached_projects.get_all_featured,
'featured',
True, False)
'featured', True, False, order_by, desc)
else:
categories = cached_cat.get_all()
cat_short_name = categories[0].short_name
return redirect_content_type(url_for('.project_cat_index', category=cat_short_name))


def project_index(page, lookup, category, fallback, use_count):
def project_index(page, lookup, category, fallback, use_count, order_by=None,
desc=False):
"""Show projects of a category"""

per_page = current_app.config['APPS_PER_PAGE']
ranked_projects = rank(lookup(category), order_by, desc)

ranked_projects = rank(lookup(category))
offset = (page - 1) * per_page
projects = ranked_projects[offset:offset+per_page]

count = cached_projects.n_count(category)

data = []

if fallback and not projects: # pragma: no cover
return redirect(url_for('.index'))

Expand Down Expand Up @@ -214,15 +212,20 @@ def project_index(page, lookup, category, fallback, use_count):
@admin_required
def draft(page):
"""Show the Draft projects"""
order_by = request.args.get('orderby', None)
desc = bool(request.args.get('desc', False))
return project_index(page, cached_projects.get_all_draft, 'draft',
False, True)
False, True, order_by, desc)


@blueprint.route('/category/<string:category>/', defaults={'page': 1})
@blueprint.route('/category/<string:category>/page/<int:page>/')
def project_cat_index(category, page):
"""Show Projects that belong to a given category"""
return project_index(page, cached_projects.get_all, category, False, True)
order_by = request.args.get('orderby', None)
desc = bool(request.args.get('desc', False))
return project_index(page, cached_projects.get_all, category, False, True,
order_by, desc)


@blueprint.route('/new', methods=['GET', 'POST'])
Expand Down
50 changes: 50 additions & 0 deletions test/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,56 @@ def test_rank_by_recent_updates_or_contributions(self):
assert ranked[3]['name'] == 'fourth', ranked[3]['name']
assert ranked[4]['name'] == 'last', ranked[4]['name']

def test_rank_by_chosen_attribute(self):
projects = [
{'info': {},
'n_tasks': 1, 'name': u'last', 'short_name': u'a',
'overall_progress': 0, 'n_volunteers': 10},
{'info': {},
'n_tasks': 11, 'name': u'fourth', 'short_name': u'b',
'overall_progress': 0, 'n_volunteers': 25},
{'info': {},
'n_tasks': 21, 'name': u'third', 'short_name': u'c',
'overall_progress': 0, 'n_volunteers': 15},
{'info': {},
'n_tasks': 51, 'name': u'second', 'short_name': u'd',
'overall_progress': 0, 'n_volunteers': 1},
{'info': {},
'n_tasks': 101, 'name': u'first', 'short_name': u'e',
'overall_progress': 0, 'n_volunteers': 5}]
ranked = util.rank(projects, order_by='n_volunteers')

assert ranked[0]['name'] == 'second'
assert ranked[1]['name'] == 'first'
assert ranked[2]['name'] == 'last'
assert ranked[3]['name'] == 'third'
assert ranked[4]['name'] == 'fourth'

def test_rank_by_chosen_attribute_reversed(self):
projects = [
{'info': {},
'n_tasks': 1, 'name': u'last', 'short_name': u'a',
'overall_progress': 0, 'n_volunteers': 1},
{'info': {},
'n_tasks': 11, 'name': u'fourth', 'short_name': u'b',
'overall_progress': 0, 'n_volunteers': 5},
{'info': {},
'n_tasks': 21, 'name': u'third', 'short_name': u'c',
'overall_progress': 0, 'n_volunteers': 10},
{'info': {},
'n_tasks': 51, 'name': u'second', 'short_name': u'd',
'overall_progress': 0, 'n_volunteers': 20},
{'info': {},
'n_tasks': 101, 'name': u'first', 'short_name': u'e',
'overall_progress': 0, 'n_volunteers': 30}]
ranked = util.rank(projects, order_by='n_volunteers', desc=True)

assert ranked[0]['name'] == 'first'
assert ranked[1]['name'] == 'second'
assert ranked[2]['name'] == 'third'
assert ranked[3]['name'] == 'fourth'
assert ranked[4]['name'] == 'last'

@patch('pybossa.util.url_for')
def test_get_avatar_url(self, mock_url_for):
"""Test get_avatar_url works."""
Expand Down
38 changes: 38 additions & 0 deletions test/test_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -6917,3 +6917,41 @@ def test_login_expired_otp(self, OtpAuth, retrieve_user_otp_secret):
err_msg = 'OTP should be expired'
assert data['status'] == ERROR, (err_msg, data)
assert 'Expired one time password' in data.get('flash'), (err_msg, data)

@with_context
@patch('pybossa.view.projects.rank', autospec=True)
def test_project_index_sorting(self, mock_rank):
"""Test WEB Project index parameters passed for sorting."""
self.register()
self.create()
project = db.session.query(Project).get(1)

order_by = u'n_volunteers'
desc = True
query = 'orderby=%s&desc=%s' % (order_by, desc)

# Test named category
url = 'project/category/%s?%s' % (Fixtures.cat_1, query)
self.app.get(url, follow_redirects=True)
assert mock_rank.call_args_list[0][0][0][0]['name'] == project.name
assert mock_rank.call_args_list[0][0][1] == order_by
assert mock_rank.call_args_list[0][0][2] == desc

# Test featured
project.featured = True
project_repo.save(project)
url = 'project/category/featured?%s' % query
self.app.get(url, follow_redirects=True)
assert mock_rank.call_args_list[1][0][0][0]['name'] == project.name
assert mock_rank.call_args_list[1][0][1] == order_by
assert mock_rank.call_args_list[1][0][2] == desc

# Test draft
project.featured = False
project.published = False
project_repo.save(project)
url = 'project/category/draft/?%s' % query
res = self.app.get(url, follow_redirects=True)
assert mock_rank.call_args_list[2][0][0][0]['name'] == project.name
assert mock_rank.call_args_list[2][0][1] == order_by
assert mock_rank.call_args_list[2][0][2] == desc

0 comments on commit b59fad1

Please sign in to comment.