from collections import OrderedDict import re from django.contrib import messages from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect, QueryDict from django.utils.translation import ugettext as _ from django.utils.translation import ungettext from hid.assets import require_assets from hid.constants import ITEM_TYPE_CATEGORY from hid.forms.upload import UploadForm from hid.tables import ItemTable import transport from transport.exceptions import TransportException ADD_CATEGORY_PREFIX = 'add-category-' DELETE_COMMAND = 'delete' NONE_COMMAND = 'none' REMOVE_QTYPE_COMMAND = 'remove-question-type' class ViewAndEditTableTab(object): """ A table view that can be used to view messages, categorize them (individually and in batches) and delete them. Settings: label (str): Label for the table data type filters (dict): Filters to pass to the term list API categories (list of str): List of taxonomy slugs which indiciate the taxonomies the items in this view can be categorized by. columns (list of str): List of columns to display, from the columns available to ItemTable. If missing, all columns are displayed. per_page (int): Number of items to display per page. Defaults to 25. """ template_name = 'hid/tabs/view_and_edit_table.html' def _build_action_dropdown_group(self, label='', items=[], prefix=''): """ Helper method to build a group of actions used in the action dropdown. 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. Returns: A dictionary representing the action group. """ return { 'label': label, 'items': OrderedDict( [(prefix + entry_cmd, entry_label) for entry_cmd, entry_label in items] ) } def _get_items(self, **kwargs): """ Given the tab settings, return the list of items to include in the page Args: **kwargs (dict): Tab settings. If present kwargs['filters'] is expected to be a dictionary of filters that is passed on to the transport API. Reruns: QuerySet: The items to list on the page """ filters = kwargs.get('filters', {}) return transport.items.list(**filters) def _get_columns_to_exclude(self, **kwargs): """ Given the tab settings, return the columns to exclude from the page Args: **kwargs (dict): Tab settings. If present kwargs['columns'] is execpted to be a list of strings listing the columns to include Returns: list of str: List of columns to exclude """ included_columns = kwargs.get('columns', None) if included_columns is None: excluded_columns = () else: all_columns = [k for k, v in ItemTable.base_columns.items()] excluded_columns = set(all_columns) - set(included_columns) return excluded_columns def _get_category_options(self, **kwargs): """ Given the tab settings, return the options to fill the categorisation drop down on the page. Args: **kwargs (dict): Tab settings. If present, kwargs['categories'] is a list of taxonomy slugs representing the taxonomies that can be used to categorized the items in the table. At the moment only one such taxonomy is supported. Returns: set of (value, label) pairs: The options of the first categorie in categories. If no categories were present, this is empty. """ taxonomy_slugs = kwargs.get('categories', []) if len(taxonomy_slugs) > 1: raise Exception('ViewAndEditTableTab supports up to one category') if len(taxonomy_slugs) == 0: return () terms = transport.terms.list(taxonomy=taxonomy_slugs[0]) terms.sort(key=lambda e: e['name'].lower()) return tuple((t['name'], t['name']) for t in terms) def _build_actions_dropdown(self, question_types): items = [ (NONE_COMMAND, '---------'), (DELETE_COMMAND, _('Delete Selected')), ] if len(question_types) > 0: items.append((REMOVE_QTYPE_COMMAND, _('Remove Question Type')),) actions = [ self._build_action_dropdown_group( label=_('Actions'), items=items ) ] if len(question_types) > 0: actions.append( self._build_action_dropdown_group( label=_('Set question type'), items=question_types, prefix=ADD_CATEGORY_PREFIX ) ) return actions def _build_upload_form(self, tab_instance, source): if source is None: return None next_url = reverse( 'tabbed-page', kwargs={ 'name': tab_instance.page.name, 'tab_name': tab_instance.name, }) return UploadForm(initial={ 'source': source, 'next': next_url, }) def get_context_data(self, tab_instance, request, **kwargs): question_types = self._get_category_options(**kwargs) # Build the table table = ItemTable( self._get_items(**kwargs), categories=question_types, exclude=self._get_columns_to_exclude(**kwargs), orderable=True, order_by=request.GET.get('sort', None), ) table.paginate( per_page=kwargs.get('per_page', 25), page=request.GET.get('page', 1) ) upload_form = self._build_upload_form( tab_instance, kwargs.get('source', None) ) actions = self._build_actions_dropdown(question_types) # Ensure we have the assets we want require_assets('hid/js/automatic_file_upload.js') require_assets('hid/js/select_all_checkbox.js') # And return the context return { 'add_button_for': self._get_item_type_filter(kwargs), 'type_label': kwargs.get('label', '?'), 'table': table, 'upload_form': upload_form, 'actions': actions, 'has_categories': len(question_types) > 0, 'next': reverse('tabbed-page', kwargs={ 'name': tab_instance.page.name, 'tab_name': tab_instance.name }) } def _get_item_type_filter(self, kwargs): """ If this tab displays a single item-type, return the associated term This parses the filters to see if there is any item-types filter. Items can only have one item type. Args: kwargs: Tab settings Returns: dict or None: The term object """ if 'filters' not in kwargs or 'terms' not in kwargs['filters']: return None for filter_expr in kwargs['filters']['terms']: try: (tax, name) = filter_expr.split(':', 1) except ValueError: # Not our place to validate this. pass if tax == 'item-types': matches = transport.terms.list( taxonomy=tax, name=name ) if len(matches) > 0: return matches[0] return None def _get_view_and_edit_form_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 submit button is called 'action', and it's value is <action>-<placement> Args: params (QueryDict): GET or POST request parameters Returns: QueryDict: 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 def _handle_batch_action(request, batch_action, selected): if not batch_action: # TODO: is this ever called? messages.error(request, _('Missing batch action')) return if batch_action == NONE_COMMAND: return if batch_action == DELETE_COMMAND: _delete_items(request, selected) return if batch_action.startswith(ADD_CATEGORY_PREFIX): _categorize_items(request, selected, batch_action[len(ADD_CATEGORY_PREFIX):]) return if batch_action == REMOVE_QTYPE_COMMAND: _categorize_items(request, selected, '') return messages.error(request, _("Unknown batch action '%s'" % batch_action)) def _categorize_items(request, items, category): # TODO: Work out the item type. _add_items_categories( request, [(item, ITEM_TYPE_CATEGORY['question'], category) for item in items]) def view_and_edit_table_form_process_items(request): """ Request to process a selection of items from the view & edit table page. Args: request (Request): This should contain a POST request defining: - action: The action to apply - select_action: List of items to apply the action too. """ # Process the form if request.method == "POST": params = _get_view_and_edit_form_request_parameters(request.POST) if params['action'] == 'batchupdate': selected = ItemTable.get_selected(params) _handle_batch_action(request, params['batchaction'], selected) elif params['action'] == 'save': changes = ItemTable.get_row_select_values(params, 'category') # TODO: Work out the item type. _add_items_categories( request, [(item, ITEM_TYPE_CATEGORY['question'], category) for item, category in changes] ) elif params['action'] != 'none': messages.error(request, _('Unknown action')) # Find the tab to redirect to redirect_url = request.POST.get('next') if not redirect_url: redirect_url = reverse('tabbed-page', kwargs={ 'name': 'main', 'tab_name': 'all' }) return HttpResponseRedirect(redirect_url) def _delete_items(request, deleted): """ Delete the given items, and set a success/failure on the request Args: request (Request): Current request object items (list of int): List of items to delete """ try: transport.items.bulk_delete(deleted) num_deleted = len(deleted) msg = ungettext("%d item deleted.", "%d items deleted.", num_deleted) % num_deleted messages.success(request, msg) except: msg = _("There was an error while deleting.") messages.error(request, msg) def _add_items_categories(request, items): """ Add the given category to the given items, and set a success/failure on the request Args: request (Request): Current request object items (list of (item id, taxonomy_slug, term_name)): tupples to update. """ success = 0 failed = 0 for item_id, taxonomy_slug, term_name in items: try: if term_name: transport.items.add_terms( item_id, taxonomy_slug, term_name ) else: transport.items.delete_all_terms( item_id, taxonomy_slug ) success += 1 except TransportException: failed += 1 if success > 0: msg = ungettext("Updated %d item.", "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)