Skip to content
Snippets Groups Projects
Commit 31117a06 authored by Martin Burchell's avatar Martin Burchell
Browse files

Merge branch 'develop' into ui-bug-fixes

parents 7da60728 cb1139d8
No related branches found
No related tags found
No related merge requests found
import django_tables2 as tables
from django.conf import settings
from django.template import loader
from django.utils.translation import ugettext_lazy as _
......@@ -28,16 +29,25 @@ class ItemTable(tables.Table):
format=settings.SHORT_DATETIME_FORMAT,
)
body = tables.Column(verbose_name=_('Message'))
category = tables.Column(
verbose_name=_('Category'),
category = tables.TemplateColumn(
template_name='hid/categories_column.html',
accessor='terms.0.name',
default=_('Uncategorized')
)
def __init__(self, *args, **kwargs):
self.categories = kwargs.pop('categories')
super(ItemTable, self).__init__(*args, **kwargs)
def render_category(self, record, value):
Template = loader.get_template('hid/categories_column.html')
ctx = {
'categories': self.categories,
'category': value,
'record': record
}
return Template.render(ctx)
@staticmethod
def get_selected(params):
""" Given a request parameter list, return the items that were
......@@ -49,3 +59,24 @@ class ItemTable(tables.Table):
List of selected record ids as integers
"""
return [int(x) for x in params.getlist("select_item_id", [])]
@staticmethod
def get_row_select_values(params, input_prefix):
""" Given a request parameter list, return the values that were
set on each of the given rows using the given drop down or
input field.
Args:
- params: GET/POST parameter list
- input_prefix: Prefix used for each rows' input field,
such that the input field's name is
<input_prefix>-<row id>
Returns:
List of tupples (row id, field value)
"""
values = []
for name, value in params.items():
if value and name.startswith(input_prefix + '-'):
row_id = int(name[len(input_prefix)+1:])
values.append((row_id, value))
return values
{% if categories %}
<select name="category-{{ record.id }}" class="form-control">
{% if not category %}
<option value="" selected="selected">---------</option>
{% endif %}
{% for cat in categories %}
<option value="{{ cat.0 }}"{% if category == cat.0 %} selected="selected"{% endif %}>{{ cat.0 }}</option>
{% endfor %}
</select>
{% endif %}
......@@ -26,24 +26,15 @@
method="post"
class="view-items-form">
{% csrf_token %}
<div class="form-group form-header-group">
<select name='action' class='form-control' required>
<option value="" default selected">{% trans "Actions" %}</option>
{% for group in actions %}
<optgroup label="{{ group.label }}">
{% for action, action_label in group.items.items %}
<option value="{{ action }}">
{{ action_label }}
</option>
{% endfor %}
</optgroup>
{% endfor %}
</select>
{% bootstrap_button "Update" button_type="submit" value="Update" button_class="btn btn-success btn-sm table-submit" %}
</div>
{% with button_placement="top" %}
{% include "hid/view_and_edit_buttons.html" %}
{% endwith %}
{% with pagination_class="pagination-circle-nav" %}
{% render_table table %}
{% endwith %}
{% with button_placement="bottom" %}
{% include "hid/view_and_edit_buttons.html" %}
{% endwith %}
</form>
</div>
</div>
......
{% load i18n %}
{% load bootstrap3 %}
<div class="form-group table-button-group">
<select name='batchaction-{{button_placement}}' class='form-control'>
{% for group in actions %}
<optgroup label="{{ group.label }}">
{% for action, action_label in group.items.items %}
<option value="{{ action }}">
{{ action_label }}
</option>
{% endfor %}
</optgroup>
{% endfor %}
</select>
{% bootstrap_button "Apply action" button_type="submit" name="action" value="batchupdate-"|add:button_placement button_class="btn btn-default btn-sm table-submit" %}
<div class="pull-right">
{% bootstrap_button "Save changes" button_type="submit" name="action" value="save-"|add:button_placement button_class="btn btn-success btn-sm table-submit" %}
</div>
</div>
......@@ -14,27 +14,48 @@ ReqFactory = RequestFactory()
@pytest.fixture
def term():
def terms():
# TODO rewrite using transport.terms, etc.
taxonomy = TaxonomyFactory(name="Ebola Questions")
return TermFactory(taxonomy=taxonomy, name="Vacciene")
taxonomy = TaxonomyFactory(name="Test Ebola Questions")
return [
TermFactory(taxonomy=taxonomy, name="Vacciene"),
TermFactory(taxonomy=taxonomy, name="Origin")
]
@pytest.fixture
def item():
data = {'body': 'test message'}
return transport.items.create(data)
def items():
return [
transport.items.create({'body': 'test message one'}),
transport.items.create({'body': 'test message two'})
]
@pytest.mark.django_db
def test_add_items_categories_adds_term_to_item(term, item):
def test_add_items_categories_adds_term_to_items(terms, items):
url = reverse('data-view-process')
request = ReqFactory.post(url, {'a': 'b'})
request = fix_messages(request)
add_items_categories(request, [item['id']], term.name)
[item_data] = transport.items.list()
[term_data] = item_data['terms']
assert term_data['name'] == term.name
assert term_data['taxonomy'] == term.taxonomy.slug
expected = {
items[0]['id']: terms[0],
items[1]['id']: terms[1]
}
category_map = [
(item_id, term.taxonomy.slug, term.name)
for item_id, term in expected.items()
]
add_items_categories(request, category_map)
fetched_items = transport.items.list()
found = 0
for item in fetched_items:
if item['id'] in expected:
found += 1
assert len(item['terms']) == 1
[term_data] = item['terms']
assert term_data['name'] == expected[item['id']].name
assert term_data['taxonomy'] == expected[item['id']].taxonomy.slug
assert found == 2
......@@ -16,3 +16,48 @@ def test_get_selected_returns_submitted_values_as_ints():
params.getlist.return_value = ["201", "199", "3"]
assert ItemTable.get_selected(params) == [201, 199, 3]
def test_get_row_select_values_returns_id_value_pairs():
post_params = {
'category-123': "second",
'category-99': "third",
'category-56': "first",
'category-1': "second",
}
expected = [
(123, "second"),
(99, "third"),
(56, "first"),
(1, "second")
]
actual = ItemTable.get_row_select_values(post_params, 'category')
assert sorted(expected) == sorted(actual) # Order is not important
def test_get_row_select_values_reads_params_from_prefix():
post_params = {
'prefix-123': "second",
'prefix-99': "third",
'other-1': "second",
}
expected = [
(123, "second"),
(99, "third"),
]
actual = ItemTable.get_row_select_values(post_params, 'prefix')
assert sorted(expected) == sorted(actual) # Order is not important
def test_get_row_select_values_removes_empty():
post_params = {
'category-123': "second",
'category-99': "third",
'category-56': "",
}
expected = [
(123, "second"),
(99, "third"),
]
actual = ItemTable.get_row_select_values(post_params, 'category')
assert sorted(expected) == sorted(actual) # Order is not important
from __future__ import unicode_literals, absolute_import
import pytest
from django.core.urlresolvers import reverse
from django.test import Client
from django.utils.six.moves.urllib.parse import urlsplit
from users.models import User
@pytest.mark.django_db
def test_user_directed_to_login_page_when_csrf_error():
username = 'william'
password = 'passw0rd'
User.objects.create_user(username, 'william@example.com', password)
client = Client(enforce_csrf_checks=True)
data = {'username': username,
'password': password,
'csrfmiddlewaretoken': 'notavalidtoken'}
response = client.post(reverse('login'),
data=data, follow=True)
assert hasattr(response, 'redirect_chain')
assert len(response.redirect_chain) > 0, "Response didn't redirect"
assert response.redirect_chain[0][1] == 302
url, _ = response.redirect_chain[-1]
scheme, netloc, path, query, fragment = urlsplit(url)
assert path == reverse('login')
url, _ = response.redirect_chain[-2]
scheme, netloc, path, query, fragment = urlsplit(url)
assert path == reverse('dashboard')
assert response.status_code == 200
......@@ -2,7 +2,7 @@ import pytest
from django.contrib.messages.storage.fallback import FallbackStorage
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.http import HttpResponseRedirect, QueryDict
from django.test import RequestFactory
from ..views import (
......@@ -54,7 +54,8 @@ def request_item():
url = reverse('data-view-process')
request = ReqFactory.post(url, {
'action': DELETE_COMMAND,
'action': 'batchupdate-top',
'batchaction-top': DELETE_COMMAND,
'select_item_id': [item['id']]}
)
request = fix_messages(request)
......@@ -63,7 +64,7 @@ def request_item():
def check_item_was_deleted(request):
assert check_message(request, u"Successfully deleted 1 item.") is True
assert check_message(request, u"1 item deleted.") is True
items = list(transport.items.list())
assert len(list(items)) == 0
......@@ -155,3 +156,68 @@ def test_get_category_options_with_no_taxonomy_returns_all():
assert (type_1.name, type_1.long_name) in options
assert (other_term.name, other_term.long_name) in options
@pytest.mark.django_db
def test_get_category_options_orders_by_lowercase_name():
# TODO: Rewrite tests to use transport layer
ebola_questions = TaxonomyFactory(name="Ebola Questions")
test_term_values = [
('test a1', '1'), ('test b1', '2'),
('test A2', '3'), ('test B2', '4')
]
for test_value in test_term_values:
TermFactory(
name=test_value[0],
long_name=test_value[1],
taxonomy=ebola_questions
)
view = ViewItems()
options = view.get_category_options(ebola_questions.id)
# Make sure we are only comparing with out test values!
options = [o for o in options if o in test_term_values]
# Expected is the list ordered by lowercase short name.
expected = sorted(test_term_values, key=lambda e: e[0].lower())
assert options == expected
def test_views_item_get_request_parameters_renames_items_of_active_location():
query = QueryDict(
'action=something-bottom&item-top=top-value&item-bottom=bottom-value'
)
expected = {
'action': 'something',
'item': 'bottom-value',
'item-top': 'top-value'
}
actual = ViewItems.get_request_parameters(query)
assert actual.dict() == expected
def test_views_item_get_request_parameters_sets_default_location():
query = QueryDict(
'action=something&item-top=top-value&item-bottom=bottom-value'
)
expected = {
'action': 'something',
'item': 'top-value',
'item-bottom': 'bottom-value'
}
actual = ViewItems.get_request_parameters(query)
assert actual.dict() == expected
def test_views_item_get_request_parameters_sets_default_action_and_location():
query = QueryDict(
'item-top=top-value&item-bottom=bottom-value'
)
expected = {
'action': 'none',
'item': 'top-value',
'item-bottom': 'bottom-value'
}
actual = ViewItems.get_request_parameters(query)
assert actual.dict() == expected
import re
from collections import OrderedDict
from django.contrib import messages
from django.contrib.auth.views import login
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.http import HttpResponseRedirect, QueryDict
from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
from django.views.generic import FormView
......@@ -20,6 +25,7 @@ from .tables import ItemTable
QUESTION_TYPE_TAXONOMY = 'ebola-questions'
ADD_CATEGORY_PREFIX = 'add-category-'
DELETE_COMMAND = 'delete'
NONE_COMMAND = 'none'
class ListSources(TemplateView):
......@@ -87,14 +93,19 @@ class ViewItems(SingleTableView):
def get_category_options(self, categories_id=None):
# TODO: Use data layer
terms = self.get_matching_terms(categories_id)
return tuple((t.name, t.long_name) for t in terms)
def get_matching_terms(self, categories_id):
if categories_id is None:
return Term.objects.all()
return (Term.objects
.extra(select={'name_lower': 'lower(name)'})
.order_by('name_lower')
.all())
return Term.objects.filter(taxonomy__id=categories_id)
return (Term.objects
.extra(select={'name_lower': 'lower(name)'})
.order_by('name_lower')
.filter(taxonomy__id=categories_id))
def get_table(self, **kwargs):
# TODO: Filter on taxonomy
......@@ -107,11 +118,17 @@ class ViewItems(SingleTableView):
context['upload_form'] = UploadForm(initial={'source': 'geopoll'})
context['actions'] = [
self._build_action_dropdown_group(
items=[(DELETE_COMMAND, _('Delete Selected'))]
label=_('Actions'),
items=[
(NONE_COMMAND, '---------'),
(DELETE_COMMAND, _('Delete Selected'))
]
),
self._build_action_dropdown_group(
label=_('Set question type'),
items=self.get_category_options(),
items=[(short_name, short_name)
for short_name, long_name
in self.get_category_options()],
prefix=ADD_CATEGORY_PREFIX
)
]
......@@ -136,11 +153,53 @@ class ViewItems(SingleTableView):
"""
return {
'label': label,
'items': dict(
[(prefix + k, v) for k, v in items]
'items': OrderedDict(
[(prefix + entry_cmd, entry_label)
for entry_cmd, entry_label in items]
)
}
@staticmethod
def get_request_parameters(params):
""" Return the parameters of the given request.
The form has mirrored inputs as the top and the
bottom of the form. This detects which one was used
to submit the form, and returns the parameters
associated with that one.
It is expected that:
- All mirrored form elements are named as
<name>-<placement>
- The busmit button is called 'action',
and it's value is <action>-<placement>
Args:
- params: GET or POST request parameters
Returns:
The list of invoked parameters renamed such
that the active parameters match the submit
button that was invoked. If no 'action' exists
it is defaulted to 'none' and placement to 'top'.
"""
new_params = QueryDict('', mutable=True)
action = params.get('action', 'none-top')
if '-' in action:
placement = re.sub('^[^-]+-', '', action)
action = action[0:len(action) - len(placement) - 1]
else:
placement = 'top'
for name, value in params.iterlists():
if name == 'action':
value = [action]
elif name.endswith(placement):
name = name[0:len(name)-len(placement)-1]
new_params.setlist(name, value)
if 'action' not in new_params:
new_params['action'] = 'none'
return new_params
class ViewSingleItem(TemplateView):
template_name = "hid/item.html"
......@@ -161,8 +220,8 @@ def delete_items(request, deleted):
try:
transport.items.bulk_delete(deleted)
num_deleted = len(deleted)
msg = ungettext("Successfully deleted %d item.",
"Successfully deleted %d items.",
msg = ungettext("%d item deleted.",
"%d items deleted.",
num_deleted) % num_deleted
messages.success(request, msg)
except:
......@@ -170,30 +229,30 @@ def delete_items(request, deleted):
messages.error(request, msg)
def add_items_categories(request, items, category):
def add_items_categories(request, items):
""" Add the given category to the given items,
and set a success/failure on the request
Args:
- request: Current request object
- items: List of item ids to add the category too
- category: Category name to add
- items: List of (item id, taxonomy_slug, term_name) tupples to
update.
"""
success = 0
failed = 0
for item_id in items:
for item_id, taxonomy_slug, term_name in items:
try:
transport.items.add_term(
item_id,
QUESTION_TYPE_TAXONOMY,
category,
taxonomy_slug,
term_name
)
success += 1
except TransportException:
failed += 1
if success > 0:
msg = ungettext("Successfully updated %d item.",
"Successfully updated %d items.",
msg = ungettext("Updated %d item.",
"Updated %d items.",
len(items)) % len(items)
messages.success(request, msg)
if failed > 0:
......@@ -217,14 +276,41 @@ def process_items(request):
redirect_url = reverse("data-view")
# Just redirect back to items view on GET
if request.method == "POST":
selected = ItemTable.get_selected(request.POST)
action = request.POST.get('action')
if action == DELETE_COMMAND:
delete_items(request, selected)
elif action and action.startswith(ADD_CATEGORY_PREFIX):
category = action[len(ADD_CATEGORY_PREFIX):]
add_items_categories(request, selected, category)
else:
params = ViewItems.get_request_parameters(request.POST)
if params['action'] == 'batchupdate':
selected = ItemTable.get_selected(params)
batch_action = params['batchaction']
if batch_action == DELETE_COMMAND:
delete_items(request, selected)
elif batch_action and batch_action.startswith(ADD_CATEGORY_PREFIX):
category = batch_action[len(ADD_CATEGORY_PREFIX):]
add_items_categories(
request,
[(item, QUESTION_TYPE_TAXONOMY, category)
for item in selected]
)
elif batch_action == NONE_COMMAND:
pass
else:
messages.error(request, _('Unknown batch action'))
elif params['action'] == 'save':
changes = ItemTable.get_row_select_values(params, 'category')
add_items_categories(
request,
[(item, QUESTION_TYPE_TAXONOMY, category)
for item, category in changes]
)
elif params['action'] != 'none':
messages.error(request, _('Unknown action'))
return HttpResponseRedirect(redirect_url)
def csrf_failure(request, reason=''):
# If the user presses the back button in the browser to go back to the
# login page and logs in again, they will get a CSRF error page because
# the token will be wrong.
# We override this with a redirect to the dashboard, which if not already
# logged in, will redirect to the login page (with a fresh token).
return HttpResponseRedirect(reverse('dashboard'))
......@@ -81,12 +81,12 @@ td.created, td.timestamp {
float:inherit;
}
// Form header actions
.panel-body form .form-header-group {
// Button header (and footer) for tables
.panel-body form .table-button-group {
margin-bottom: @padding-base-vertical;
}
.panel-body form .form-header-group .form-control {
.panel-body form .table-button-group .form-control {
float: left;
width: auto;
height: auto;
......@@ -94,7 +94,7 @@ td.created, td.timestamp {
margin-right: @padding-small-horizontal;
}
.panel-body .form-header-group select option[default] {
.panel-body .table-button-group select option[default] {
font-weight: bold;
}
......
......@@ -390,5 +390,6 @@ else:
)
########## END TEMPLATE CONFIGURATION
CSRF_FAILURE_VIEW = 'hid.views.csrf_failure'
########## Your stuff: Below this line define 3rd party libary settings
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment