diff --git a/django/website/data_layer/migrations/0004_auto_20150720_1723.py b/django/website/data_layer/migrations/0004_auto_20150720_1723.py new file mode 100644 index 0000000000000000000000000000000000000000..aaa9d331e09f6d9ddf98b5d9f0d4fbb9f5113da9 --- /dev/null +++ b/django/website/data_layer/migrations/0004_auto_20150720_1723.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('data_layer', '0003_message_terms'), + ] + + operations = [ + migrations.AlterField( + model_name='message', + name='terms', + field=models.ManyToManyField(related_name='items', to='taxonomies.Term'), + ), + ] diff --git a/django/website/data_layer/models.py b/django/website/data_layer/models.py index 244aa6e90c65b97aea1b225cfcaa0f256b748d89..fa04cfe55ba847dac221853f1f24ea3953ad2e7d 100644 --- a/django/website/data_layer/models.py +++ b/django/website/data_layer/models.py @@ -12,7 +12,7 @@ class DataLayerModel(models.Model): class Message(DataLayerModel): body = models.TextField() timestamp = models.DateTimeField(null=True) - terms = models.ManyToManyField(Term) + terms = models.ManyToManyField(Term, related_name="items") def apply_term(self, term): # TODO: test this diff --git a/django/website/hid/assets.py b/django/website/hid/assets.py index 36acaff8819dc7821232f071f2a4c49393d5c5de..309cd040267230b84221aed44ebe74d1628ee333 100644 --- a/django/website/hid/assets.py +++ b/django/website/hid/assets.py @@ -37,7 +37,8 @@ _assets = [ 'hid/widgets/chart.js', 'hid/js/spinner.js', 'hid/js/messages.js', - 'hid/js/automatic_file_upload.js' + 'hid/js/automatic_file_upload.js', + 'hid/js/select_all_checkbox.js' ] diff --git a/django/website/hid/migrations/0002_rename_question_type_terms.py b/django/website/hid/migrations/0002_rename_question_type_terms.py new file mode 100644 index 0000000000000000000000000000000000000000..db0f99d21e0ee8bf72420b9424f20e5e9b06a167 --- /dev/null +++ b/django/website/hid/migrations/0002_rename_question_type_terms.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + +QUESTION_TYPES = ( + ('Ebola updates', 'What are the current updates on Ebola?'), + ('Ebola authenticity', 'Is Ebola a real disease?'), + ('Ebola prevention', 'What measures could be put in place to end Ebola?'), + ('Ebola origins', 'What is the origin of Ebola?'), + ('Non-Ebola concerns', 'What are the non-Ebola related concerns?'), + ('Ebola symptoms', 'What are the symptoms of Ebola?'), + ('Ebola vaccine', 'What are the stakes of the Ebola vaccine?'), + ('Liberia Ebola-free', 'Can Liberia be Ebola free?'), + ('Unknown', 'Unknown.'), +) + + +def rename_question_type_terms(apps, schema_editor): + Taxonomy = apps.get_model('taxonomies', 'Taxonomy') + Term = apps.get_model('taxonomies', 'Term') + (taxonomy, _) = Taxonomy.objects.get_or_create( + slug="ebola-questions", + name="Ebola Questions", + ) + Term.objects.filter(taxonomy=taxonomy).delete() + new_terms = [ + Term(name=qt[0], long_name=qt[1], taxonomy=taxonomy) + for qt in QUESTION_TYPES + ] + + Term.objects.bulk_create(new_terms) + + +class Migration(migrations.Migration): + + dependencies = [ + ('hid', '0001_question_types'), + ] + + operations = [ + migrations.RunPython(rename_question_type_terms) + ] diff --git a/django/website/hid/static/hid/js/automatic_file_upload.js b/django/website/hid/static/hid/js/automatic_file_upload.js index a72b5151f7db862ad2f46cd9d9d14dcde29f1314..35a49bc06ed6efab9da78c22c5bfeaaff890e4d4 100644 --- a/django/website/hid/static/hid/js/automatic_file_upload.js +++ b/django/website/hid/static/hid/js/automatic_file_upload.js @@ -7,6 +7,11 @@ * - The form element should have a 'auto-upload-file' class; * - The upload button replacement should have an 'upload-button' class * (optional, we can also use the normal upload button). + * - The file input tag should be a descendant of a .form-group element + * (optional, this will be hidden if we have a button replacement). + * + * This is progressive enhancement, so the upload-button should be hidden + * to be begin with, and will only be displayed if the javascript executes. * * Note that this will only work if the form has one file upload element. * @@ -17,6 +22,13 @@ var $form = $(this); var $button = $('.upload-button', $form); var $file_input = $('[type="file"]', $form); + var $input_group = $file_input.closest('.form-group'); + + // Hide the file input group and show the alternative button. + if ($button.length > 0) { + $input_group.hide(); + $button.show(); + } // Auto-submit $file_input.on('change', function(e) { diff --git a/django/website/hid/static/hid/js/select_all_checkbox.js b/django/website/hid/static/hid/js/select_all_checkbox.js new file mode 100644 index 0000000000000000000000000000000000000000..cefd900538159807c72b433b040590248b01a804 --- /dev/null +++ b/django/website/hid/static/hid/js/select_all_checkbox.js @@ -0,0 +1,53 @@ +/** + * Add a checkbox to select all entries to tables with a column containing + * a checkbox. + * + * Usage: + * - The th/td elements containing the checkbox must have + * a 'select_action' class. In Django tables this is done + * by naming the column select_action, eg.: + * + * select_action = NamedCheckBoxColumn(accessor='id', verbose_name='Select') + */ +(function($) { + $(document).ready(function() { + var selectors = { + td_input : 'form td.select_item input[type="checkbox"]', + th_select : 'form th.select_item' + }; + + function addCheckbox(selector, check_id_base) { + $(selector).each(function(i, column_header) { + var input_id = check_id_base + '-' + i; + var check = $('<input>', { + type: 'checkbox', + id: input_id + }); + $(column_header).html("") + .append(check); + }); + } + + function toggleSelection(e) { + var checked = $(e.target).prop("checked"); + $(selectors.td_input).prop("checked", checked); + } + + function setSelectAll(e) { + var checked = $(selectors.td_input + ':checked').length, + all = $(selectors.td_input).length, + checked_all = all === checked; + + $(selectors.th_select + ' input').prop("checked", checked_all); + } + + function init() { + addCheckbox(selectors.th_select, 'select-all-items'); + + $(selectors.th_select + ' input').on('change', toggleSelection); + $(selectors.td_input).on('change', setSelectAll); + } + + init(); + }); +})(jQuery); diff --git a/django/website/hid/tables.py b/django/website/hid/tables.py index fb5c2a6fc805804283eaa0591fa4e81670e7a68b..d270635c6453bff249442c4933ba0775ed9d8b26 100644 --- a/django/website/hid/tables.py +++ b/django/website/hid/tables.py @@ -1,6 +1,6 @@ import django_tables2 as tables from django.conf import settings -from django.template import loader +from django.utils.translation import ugettext_lazy as _ class NamedCheckBoxColumn(tables.CheckBoxColumn): @@ -15,32 +15,37 @@ class ItemTable(tables.Table): template = 'hid/table.html' order_by = ('-created',) + select_item = tables.TemplateColumn( + template_name='hid/select_item_id_checkbox_column.html', + verbose_name=_('Select') + ) created = tables.columns.DateTimeColumn( - verbose_name='Imported', + verbose_name=_('Imported'), format=settings.SHORT_DATETIME_FORMAT, ) timestamp = tables.columns.DateTimeColumn( - verbose_name='Created', + verbose_name=_('Created'), format=settings.SHORT_DATETIME_FORMAT, ) - body = tables.Column(verbose_name='Message') - category = tables.TemplateColumn( - template_name='hid/categories_column.html', + body = tables.Column(verbose_name=_('Message')) + category = tables.Column( + verbose_name=_('Category'), accessor='terms.0.name', + default=_('Uncategorized') ) - delete = NamedCheckBoxColumn(accessor='id', verbose_name='Delete') def __init__(self, *args, **kwargs): self.categories = kwargs.pop('categories') super(ItemTable, self).__init__(*args, **kwargs) - def render_category(self, record, value): - # TODO: Test this - 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 + selected using the select_item column. + + Args: + - params: GET/POST parameter list + Returns: + List of selected record ids as integers + """ + return [int(x) for x in params.getlist("select_item_id", [])] diff --git a/django/website/hid/templates/hid/categories_column.html b/django/website/hid/templates/hid/categories_column.html deleted file mode 100644 index 5d90046b97c16d24a4c1b2d0d528b3c8e1157960..0000000000000000000000000000000000000000 --- a/django/website/hid/templates/hid/categories_column.html +++ /dev/null @@ -1,10 +0,0 @@ -{% 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.1 }}</option> - {% endfor %} -</select> -{% endif %} diff --git a/django/website/hid/templates/hid/select_item_id_checkbox_column.html b/django/website/hid/templates/hid/select_item_id_checkbox_column.html new file mode 100644 index 0000000000000000000000000000000000000000..d54a6fa5eaade5454c7e249101b8d4e23a47e484 --- /dev/null +++ b/django/website/hid/templates/hid/select_item_id_checkbox_column.html @@ -0,0 +1,9 @@ +<div class='select-item-id-checkbox'> + <input + type='checkbox' + id='select-item-id-checkbox-{{ record.id }}' + value='{{ record.id }}' + name='select_item_id' + /> + <label for='select-item-id-checkbox-{{ record.id }}'></label> +</div> diff --git a/django/website/hid/templates/hid/sources.html b/django/website/hid/templates/hid/sources.html index abb55d883fdb45251461aeaf666828ab02cd7c9f..ee0d9e809f3286afb5340bcda308febee861e9fd 100644 --- a/django/website/hid/templates/hid/sources.html +++ b/django/website/hid/templates/hid/sources.html @@ -13,9 +13,13 @@ <form action="{% url "sources-upload" %}" method="post" enctype="multipart/form-data" class="item-source-actions auto-upload-file pull-right"> {% csrf_token %} - <a class="btn btn-primary btn-block btn-sm" value="View/Edit data" type="button" href="{% url "data-view" %}"><span class="fa fa-pencil fa-fw"></span> View & Edit data</a> - {% bootstrap_form source.form show_label=False %} - <a class="btn btn-sm item-source-upload upload-button btn-block btn-primary" type="button" value="Upload" href="{% url "data-view" %}"><span class="fa fa-upload fa-fw"></span> Upload</a> + <div class='form-group'> + <a class="btn btn-primary btn-block btn-sm" value="View/Edit data" type="button" href="{% url "data-view" %}"><span class="fa fa-pencil fa-fw"></span> View & Edit data</a> + </div> + <div class='form-group'> + {% bootstrap_form source.form show_label=False %} + <a class="btn btn-sm item-source-upload upload-button btn-block btn-primary" type="button" value="Upload" href="{% url "data-view" %}"><span class="fa fa-upload fa-fw"></span> Upload</a> + </div> </form> </li> {% endfor %} diff --git a/django/website/hid/templates/hid/tests/select_all_checkbox.html b/django/website/hid/templates/hid/tests/select_all_checkbox.html new file mode 100644 index 0000000000000000000000000000000000000000..125f151c42a2b51b49350a55756e738ba1f9f76e --- /dev/null +++ b/django/website/hid/templates/hid/tests/select_all_checkbox.html @@ -0,0 +1,56 @@ +{% extends "djangojs/qunit-runner.html" %} +{% block js_content %} + <script> + QUnit.test('Select all checbox is added to header', function(assert) { + var $header_checkbox = jQuery('th.select_item input[type="checkbox"]'); + assert.equal($header_checkbox.length, 1, 'Expected a checkbox in the header'); + }); + QUnit.test('Clicking the select all checkbox selects all', function(assert) { + var $header_checkbox = jQuery('th.select_item input[type="checkbox"]'); + var $selected_checkboxes = jQuery('td.select_item input:checked'); + + // Ensure none are checked + $selected_checkboxes.prop('checked', false); + + // Simulte click + $header_checkbox.click(); + + // Ensure all are checked + $selected_checkboxes = jQuery('td.select_item input:checked'); + assert.equal($selected_checkboxes.length, 2, 'Expected all checkboxes to be checked'); + }); + </script> +{% endblock %} +{% block body_content %} + <form> + <table> + <thead> + <tr> + <th class="select_item"> + </th> + <th> + A field + </th> + </tr> + </thead> + <tbody> + <tr> + <td class="select_item"> + <input type="checkbox" /> + </td> + <td> + A value + </td> + </tr> + <tr> + <td class="select_item"> + <input type="checkbox" /> + </td> + <td> + Another value + </td> + </tr> + </tbody> + </table> + </form> +{% endblock %} diff --git a/django/website/hid/templates/hid/view.html b/django/website/hid/templates/hid/view.html index 20ebc81b5eb65316c2b831e6256c57169fc53b9b..7e4164bdae05019b6bfb18bc9c9ff5ca114df284 100644 --- a/django/website/hid/templates/hid/view.html +++ b/django/website/hid/templates/hid/view.html @@ -7,86 +7,47 @@ <h1 class="page-header"><span class="fa fa-pencil fa-fw"></span>{% trans "View & Edit" %}</h1> <div class='row'> <div class="col-lg-12"> - <form action="{% url "sources-upload" %}" method="post" enctype="multipart/form-data" class="auto-upload-file header-upload-form"> - {% csrf_token %} - {% bootstrap_form upload_form show_label=False %} - <a class="btn btn-sm item-source-upload btn-block btn-primary upload-button" type="button" value="Upload" href="{% url "data-view" %}"><span class="fa fa-upload fa-fw"></span>{% blocktrans %}Upload {{ type_label }}{% endblocktrans %}</a> - </form> - <form action="{% url "data-view-process" %}" method="post" class="view-items-form">{% csrf_token %} - <div class="panel panel-default"> - <div class="panel-heading"> - <span class="fa fa-table fa-fw"></span> Questions - <div class="pull-right"> - <div class="btn-group"> - {% bootstrap_button "Save Changes" button_type="submit" value="Update" button_class="btn btn-success btn-sm table-submit" %} - </div> - </div> + <div class="panel panel-default"> + <div class="panel-heading clearfix"> + <span class="fa fa-table fa-fw"></span>{{ type_label }} + <div class="pull-right"> + <form action="{% url "sources-upload" %}" + method="post" + enctype="multipart/form-data" + class="auto-upload-file"> + {% csrf_token %} + {% bootstrap_form upload_form show_label=False %} + <a class="btn item-source-upload btn-block btn-primary upload-button" type="button" value="Upload" href="{% url "data-view" %}"><span class="fa fa-upload fa-fw"></span>{% blocktrans %}Upload {{ type_label }}{% endblocktrans %}</a> + </form> </div> - <div class="panel-body"> + </div> + <div class="panel-body"> + <form action="{% url "data-view-process" %}" + 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 pagination_class="pagination-circle-nav" %} - {% render_table table %} + {% render_table table %} {% endwith %} - - </div> - <div class="panel-footer clearfix"> - {% bootstrap_button "Save Changes" button_type="submit" value="Update" button_class="btn btn-success pull-right btn-sm table-submit" %} - - </div> - </div> - - </form> + </form> + </div> + </div> </div> </div> - {% endblock maincontent %} -{% block lastjs %} -<script type="text/javascript"> -(function ($) { - var selectors = { - td_input : '.view-items-form td.delete > input', - th_delete : '.view-items-form th.delete' - }; - - function addCheckbox(selector, check_id) { - var check = $('<input>', { - type: 'checkbox', - id: check_id - }), - label = $('<label>', { - for: check_id - }); - - label.html( - $(selector).html() - ); - - $(selector).html("") - .append(check) - .append(label); - } - - function toggleSelection(e) { - var checked = $(e.target).prop("checked"); - $(selectors.td_input).prop("checked", checked); - } - - function setSelectAll(e) { - var checked = $(selectors.td_input + ':checked').length, - all = $(selectors.td_input).length, - checked_all = all === checked; - - $(selectors.th_delete + ' input').prop("checked", checked_all); - } - - function init() { - addCheckbox(selectors.th_delete, 'select-all-items'); - - $(selectors.th_delete + ' input').on('change', toggleSelection); - $(selectors.td_input).on('change', setSelectAll); - } - - init(); -})(jQuery); -</script> -{% endblock lastjs %} diff --git a/django/website/hid/tests/categorize_items_tests.py b/django/website/hid/tests/categorize_items_tests.py index f4f80d93bc83811c787f0c443eb3d09e734aefa5..75c1ad6c332133a5e318ae4f5e1bf46da7f1adb8 100644 --- a/django/website/hid/tests/categorize_items_tests.py +++ b/django/website/hid/tests/categorize_items_tests.py @@ -1,10 +1,16 @@ from __future__ import unicode_literals, absolute_import import pytest +from django.core.urlresolvers import reverse +from django.test import RequestFactory from taxonomies.tests.factories import TermFactory, TaxonomyFactory import transport -from ..views import add_categories +from .views_tests import fix_messages +from ..views import add_items_categories + + +ReqFactory = RequestFactory() @pytest.fixture @@ -21,10 +27,12 @@ def item(): @pytest.mark.django_db -def test_add_categories_adds_term_to_item(term, item): - category_list = [(item['id'], term.name), ] +def test_add_items_categories_adds_term_to_item(term, item): + url = reverse('data-view-process') + request = ReqFactory.post(url, {'a': 'b'}) + request = fix_messages(request) - add_categories(category_list) + add_items_categories(request, [item['id']], term.name) [item_data] = transport.items.list() [term_data] = item_data['terms'] diff --git a/django/website/hid/tests/itemtable_tests.py b/django/website/hid/tests/itemtable_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ffc61fb4c3f61b84cda4f5a4ace17878d715ade --- /dev/null +++ b/django/website/hid/tests/itemtable_tests.py @@ -0,0 +1,18 @@ +import mock + + +from hid.tables import ItemTable + + +def test_get_selected_returns_empty_list_on_empty_selection(): + params = mock.MagicMock() + params.getlist.return_value = [] + + assert ItemTable.get_selected(params) == [] + + +def test_get_selected_returns_submitted_values_as_ints(): + params = mock.MagicMock() + params.getlist.return_value = ["201", "199", "3"] + + assert ItemTable.get_selected(params) == [201, 199, 3] diff --git a/django/website/hid/tests/util_javascript_tests.py b/django/website/hid/tests/util_javascript_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..78ab552f514236017f315cb8683192747c093f30 --- /dev/null +++ b/django/website/hid/tests/util_javascript_tests.py @@ -0,0 +1,16 @@ +""" This file defines a number of Javascript tests + for a range of small javascript enhancements + and utilities. + + Each feature should have it's own test class and + test template. +""" +from djangojs.runners import QUnitSuite, JsTemplateTestCase + + +class SelectAllCheckboxTest(QUnitSuite, JsTemplateTestCase): + template_name = 'hid/tests/select_all_checkbox.html' + js_files = ( + 'js/jquery.min.js', + 'hid/js/select_all_checkbox.js' + ) diff --git a/django/website/hid/tests/views_tests.py b/django/website/hid/tests/views_tests.py index 4c8b34380d258b97a0033c1de88e71f475963d06..75c285f49e7de6d6dd46539f251f2e6952791d4f 100644 --- a/django/website/hid/tests/views_tests.py +++ b/django/website/hid/tests/views_tests.py @@ -1,4 +1,3 @@ -import mock import pytest from django.contrib.messages.storage.fallback import FallbackStorage @@ -7,11 +6,10 @@ from django.http import HttpResponseRedirect from django.test import RequestFactory from ..views import ( - get_deleted, process_items, - get_categories, delete_items, ViewItems, + DELETE_COMMAND ) from taxonomies.tests.factories import ( @@ -45,64 +43,6 @@ def check_message(request, content): return False -def test_get_deleted_returns_empty_list_on_empty_selection(): - params = mock.MagicMock() - params.getlist.return_value = [] - - assert get_deleted(params) == [] - - -def test_get_deleted_returns_submitted_values_as_ints(): - params = mock.MagicMock() - params.getlist.return_value = ["201", "199", "3"] - - assert get_deleted(params) == [201, 199, 3] - - -def test_get_categories_returns_id_category_pairs(): - post_params = { - 'category-123': "second", - 'category-99': "third", - 'category-56': "first", - 'category-1': "second", - } - expected = [ - (123, "second"), - (99, "third"), - (56, "first"), - (1, "second") - ] - assert sorted(get_categories(post_params)) == sorted(expected) # Order is not important - - -def test_get_categories_filters_out_non_categories(): - post_params = { - 'category-123': "second", - 'category-99': "third", - 'notcat-1': "second", - } - expected = [ - (123, "second"), - (99, "third"), - ] - assert sorted(get_categories(post_params)) == sorted(expected) # Order is not important - - -def test_get_categories_filters_out_removed(): - post_params = { - 'category-123': "second", - 'category-99': "third", - 'category-56': "first", - 'category-1': "second", - } - removed = [1, 56] - expected = [ - (123, "second"), - (99, "third"), - ] - assert sorted(get_categories(post_params, removed)) == sorted(expected) # Order is not important - - @pytest.fixture def request_item(): '''Create item and request''' @@ -113,7 +53,10 @@ def request_item(): [item] = list(transport.items.list()) url = reverse('data-view-process') - request = ReqFactory.post(url, {'delete': [item['id']]}) + request = ReqFactory.post(url, { + 'action': DELETE_COMMAND, + 'select_item_id': [item['id']]} + ) request = fix_messages(request) return [request, item] @@ -151,7 +94,8 @@ def test_process_items_always_redirects_to_data_view(): assert isinstance(response, HttpResponseRedirect) is True request.method = 'POST' - request = ReqFactory.post(url) + request = ReqFactory.post(url, {}) + request = fix_messages(request) response = process_items(request) assert response.url == redirect_url assert isinstance(response, HttpResponseRedirect) is True @@ -190,6 +134,7 @@ def test_get_category_options_uses_terms(): assert (type_3.name, type_3.long_name) in options assert (other_term.name, other_term.long_name) not in options + @pytest.mark.django_db def test_get_category_options_with_no_taxonomy_returns_all(): # TODO: Rewrite tests to use transport layer diff --git a/django/website/hid/views.py b/django/website/hid/views.py index 97fed520e5334556c8574f608804ef625c9bb0d4..bce3045d9604d0b1ce754b9a39723bf3873a60ee 100644 --- a/django/website/hid/views.py +++ b/django/website/hid/views.py @@ -11,11 +11,15 @@ from django_tables2 import SingleTableView from chn_spreadsheet.importer import Importer, SheetImportException from data_layer.models import Term import transport +from transport.exceptions import TransportException +from .assets import require_assets from .forms import UploadForm, get_spreadsheet_choices from .tables import ItemTable QUESTION_TYPE_TAXONOMY = 'ebola-questions' +ADD_CATEGORY_PREFIX = 'add-category-' +DELETE_COMMAND = 'delete' class ListSources(TemplateView): @@ -101,23 +105,51 @@ class ViewItems(SingleTableView): context = super(ViewItems, self).get_context_data(**kwargs) context['type_label'] = _('Questions') context['upload_form'] = UploadForm(initial={'source': 'geopoll'}) + context['actions'] = [ + self._build_action_dropdown_group( + items=[(DELETE_COMMAND, _('Delete Selected'))] + ), + self._build_action_dropdown_group( + label=_('Set question type'), + items=self.get_category_options(), + prefix=ADD_CATEGORY_PREFIX + ) + ] + + require_assets('hid/js/automatic_file_upload.js') + require_assets('hid/js/select_all_checkbox.js') return context + def _build_action_dropdown_group(self, label='', items=[], prefix=''): + """ Helper method to build a group of actions used in the + action dropdown. -def get_deleted(params): - return [int(x) for x in params.getlist("delete", [])] + Args: + - label: Label of the group of action; + - items: List of items in the group. Each item is a tupple + consisting of the command suffix and the display + name; + - prefix: A string used to prefix the command string. - -def get_categories(params, deleted_ids=[]): - removed = set(deleted_ids) - - categories = [(int(key[9:]), val) - for key, val in params.items() - if key.startswith("category-")] - return [cat for cat in categories if cat[1] and cat[0] not in removed] + Returns: + A dictionary representing the action group. + """ + return { + 'label': label, + 'items': dict( + [(prefix + k, v) for k, v in items] + ) + } def delete_items(request, deleted): + """ Delete the given items, and set a success/failure + on the request + + Args: + - request: Current request object + - items: List of items to delete + """ try: transport.items.bulk_delete(deleted) num_deleted = len(deleted) @@ -130,38 +162,61 @@ def delete_items(request, deleted): messages.error(request, msg) -def add_categories(categories): - """ Add specified category Terms to The items - as specified in categories list. +def add_items_categories(request, items, category): + """ Add the given category to the given items, + and set a success/failure on the request - args: - categories: a list of item ids and term names: - [ (<item-id>, <term-name>), ... ] + Args: + - request: Current request object + - items: List of item ids to add the category too + - category: Category name to add """ - for item_id, term_name in categories: - transport.items.add_term( - item_id, - QUESTION_TYPE_TAXONOMY, - term_name, - ) - # Did we want to test for any failures or exceptions ? - # TODO: Add messages/success/error reporting here? + success = 0 + failed = 0 + for item_id in items: + try: + transport.items.add_term( + item_id, + QUESTION_TYPE_TAXONOMY, + category, + ) + success += 1 + except TransportException: + failed += 1 + if success > 0: + msg = ungettext("Successfully updated %d item.", + "Successfully updated %d items.", + len(items)) % len(items) + messages.success(request, msg) + if failed > 0: + msg = ungettext("Failed to update %d item.", + "Failed to update %d items.", + len(items)) % len(items) + messages.success(request, msg) def process_items(request): - ''' - If POST request, then: - - delete items that were checked - - update categories on those that weren't deleted. - ''' + """ Request to process a selection of items from the + view & edit page. + + Args: + - request: Request object. This should contain + a POST request defining: + - action: The action to apply + - select_action: List of items to apply + the action too. + """ redirect_url = reverse("data-view") # Just redirect back to items view on GET if request.method == "POST": - deleted = get_deleted(request.POST) - categories = get_categories(request.POST, deleted) - if len(deleted): - delete_items(request, deleted) - if len(categories): - add_categories(categories) + 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: + messages.error(request, _('Unknown action')) return HttpResponseRedirect(redirect_url) diff --git a/django/website/media/less/internews.less b/django/website/media/less/internews.less index bff61e8d249d6d77bda86e920625d52658c3b121..b8733ad924397eb2a49b210d22f34070a4a97775 100644 --- a/django/website/media/less/internews.less +++ b/django/website/media/less/internews.less @@ -402,6 +402,12 @@ body { line-height: 1.33; } +// Auto-upload button. Progressive enhencement, this +// will be displayed if the associated js executes. +.auto-upload-file .upload-button { + display: none; +} + // Grid Demo Elements .show-grid [class^="col-"] { @@ -519,4 +525,4 @@ body { .fa-fw { color:@gray-base; margin-right:5px; -} \ No newline at end of file +} diff --git a/django/website/media/less/sources.less b/django/website/media/less/sources.less index 8cd494cc98b5eef1b5a215b9a6c1ef5585474f3a..545060171168898f634957b6c08ce67607bcb86c 100644 --- a/django/website/media/less/sources.less +++ b/django/website/media/less/sources.less @@ -1,9 +1,5 @@ //Styles for import, loading and sources -.bootstrap3-multi-input { - display:none; -} - .item-source-info > h2 { margin:15px; } @@ -12,4 +8,4 @@ .item-source-info > h2 > img { max-width: 120px; } -} \ No newline at end of file +} diff --git a/django/website/media/less/view-edit.less b/django/website/media/less/view-edit.less index a255a1943875f441f858cfbb741360a91e99c4b9..923bfa2bcf335e2da0bccdfdef4c1a34bdc79fe0 100644 --- a/django/website/media/less/view-edit.less +++ b/django/website/media/less/view-edit.less @@ -29,7 +29,7 @@ td.created, td.timestamp { font-size:@font-size-small; } -.delete > label { +.select_action > label { margin-bottom:0; } @@ -82,10 +82,27 @@ td.created, td.timestamp { float:inherit; } +// Form header actions +.panel-body form .form-header-group { + margin-bottom: @padding-base-vertical; +} + +.panel-body form .form-header-group .form-control { + float: left; + width: auto; + height: auto; + padding: @padding-xs-horizontal @padding-base-vertical; + margin-right: @padding-small-horizontal; +} + +.panel-body .form-header-group select option[default] { + font-weight: bold; +} + // Checkboxes -td.delete, -th.delete { +td.select_action, +th.select_action { background-color:rgba(0,0,0,0.05); text-align:center; } @@ -117,10 +134,3 @@ input[type="checkbox"]:checked { outline:0; border:none; } - -// Place the upload button in the header. -form.header-upload-form { - position: absolute; - top: 10px; - right: 150px; -} diff --git a/django/website/rest_api/views.py b/django/website/rest_api/views.py index ba74fe4ccfa5ca5a68c8a5082b084473bd23f5cd..5893fbccbee318296b6682dcaf67f833a6eac00b 100644 --- a/django/website/rest_api/views.py +++ b/django/website/rest_api/views.py @@ -70,7 +70,9 @@ class TaxonomyViewSet(viewsets.ModelViewSet): data = {'detail': message} return Response(data, status=status.HTTP_400_BAD_REQUEST) - terms = Term.objects.filter(taxonomy=taxonomy).annotate(count=Count('message')) + terms = Term.objects.filter(taxonomy=taxonomy).annotate( + count=Count('items') + ) data = TermItemCountSerializer(terms, many=True).data return Response(data, status=status.HTTP_200_OK) diff --git a/django/website/settings.py b/django/website/settings.py index 854f9b9f6d030b505fccf041421b8cd80f2e0a8f..3493ef3b7112ddbdcfb56c864ce5e7ffad5cf557 100644 --- a/django/website/settings.py +++ b/django/website/settings.py @@ -153,7 +153,7 @@ DATA_LAYER_APPS = ( 'transport', ) -INSTALLED_APPS = LOCAL_APPS + DATA_LAYER_APPS + THIRD_PARTY_APPS + DJANGO_APPS +INSTALLED_APPS = DATA_LAYER_APPS + LOCAL_APPS + THIRD_PARTY_APPS + DJANGO_APPS ########## END APP CONFIGURATION diff --git a/django/website/templates/base_side.html b/django/website/templates/base_side.html index 7140c4619ef76741ed4d1f7386e9605788d15210..33524e649e08cefa293c2583201a8642671cf751 100644 --- a/django/website/templates/base_side.html +++ b/django/website/templates/base_side.html @@ -9,7 +9,7 @@ <nav class="sidebar-nav navbar-collapse collapse"> <ul class="nav" id="side-menu"> <li><a class="active" href="{% url "dashboard" %}"><span class="fa fa-dashboard fa-fw"></span> Dashboard</a></li> - <li><a href="{% url "sources" %}"><span class="fa fa-download fa-fw"></span> Sources</a></li> + {# <li><a href="{% url "sources" %}"><span class="fa fa-download fa-fw"></span> Sources</a></li> #} <li><a href="{% url "data-view" %}"><span class="fa fa-pencil fa-fw"></span> View & Edit</a></li> </ul> </nav> diff --git a/django/website/transport/items.py b/django/website/transport/items.py index c8464efe6327b8608a1d314fed0f3f70ea01ef73..1bf54b00bc2adc0e53be8e573eade7dc6625f6a7 100644 --- a/django/website/transport/items.py +++ b/django/website/transport/items.py @@ -98,6 +98,9 @@ def add_term(item_id, taxonomy_slug, name): returns: response from the server + raises: + TransportException on failure + For the moment both the taxonomy and term must already exist. """ view = ItemViewSet.as_view(actions={'post': 'add_term'})