reworked filters in jobs list view
authorMaciej Tronowski <mtro@man.poznan.pl>
Tue, 24 Feb 2015 16:51:30 +0000 (17:51 +0100)
committerMaciej Tronowski <mtro@man.poznan.pl>
Tue, 24 Feb 2015 16:51:30 +0000 (17:51 +0100)
plgng/settings.py
qcg/forms.py
qcg/templates/qcg/jobs.html
qcg/templatetags/qcg_utils.py
qcg/templatetags/query_string.py [new file with mode: 0644]
qcg/views.py

index 0567769..24f298b 100644 (file)
@@ -90,7 +90,7 @@ USE_L10N = True
 USE_TZ = True
 
 
-CUSTOM_DATETIME_FORMAT = 'j b Y, G:i'
+CUSTOM_DATETIME_FORMAT = 'j b Y, H:i'
 
 
 # Static files (CSS, JavaScript, Images)
@@ -127,3 +127,12 @@ OPENID_STRICT_USERNAMES = True
 
 
 SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
+
+
+# 3-rd party settings
+
+BOOTSTRAP3 = {
+    'horizontal_label_class': 'col-md-4',
+    'horizontal_field_class': 'col-md-6',
+    'set_placeholder': False,
+}
index 3d90145..d9d7a92 100644 (file)
@@ -26,6 +26,8 @@ class FiltersForm(forms.Form):
 
     STATUS_MAP = {
         ACTIVE: (
+            Task.STATUS_CHOICES_REVERSED[TaskStatus.UNSUBMITTED],
+            Task.STATUS_CHOICES_REVERSED[TaskStatus.UNCOMMITTED],
             Task.STATUS_CHOICES_REVERSED[TaskStatus.QUEUED],
             Task.STATUS_CHOICES_REVERSED[TaskStatus.PREPROCESSING],
             Task.STATUS_CHOICES_REVERSED[TaskStatus.PENDING],
@@ -35,14 +37,13 @@ class FiltersForm(forms.Form):
         ),
         FINISHED: (
             Task.STATUS_CHOICES_REVERSED[TaskStatus.FINISHED],
-            Task.STATUS_CHOICES_REVERSED[TaskStatus.FAILED],
-            Task.STATUS_CHOICES_REVERSED[TaskStatus.CANCELED],
         ),
         FAILED: (
             Task.STATUS_CHOICES_REVERSED[TaskStatus.FAILED],
             Task.STATUS_CHOICES_REVERSED[TaskStatus.CANCELED],
         ),
     }
+    STATUS_CHOICES_DICT = dict(STATUS_CHOICES)
 
     status = forms.MultipleChoiceField(choices=STATUS_CHOICES, label=u"Status", required=False,
                                        widget=forms.CheckboxSelectMultiple)
@@ -51,6 +52,5 @@ class FiltersForm(forms.Form):
 
     # advanced
     keywords = forms.CharField(max_length=100, label=u"Wyszukaj frazę", required=False)
-    status_exact = forms.ChoiceField(choices=[(None, u"----------")] + Task.STATUS_CHOICES, label=u"Status", required=False)
     submission = forms.CharField(label=u"Data zlecenia", validators=[date_range_validator], required=False)
     finish = forms.CharField(label=u"Data zakończenia", validators=[date_range_validator], required=False)
index 58b4998..f2b9efe 100644 (file)
@@ -1,5 +1,5 @@
 {% extends 'qcg/base.html' %}
-{% load staticfiles bootstrap3 %}
+{% load staticfiles bootstrap3 query_string qcg_utils %}
 
 {% block extra_css %}
     <link href="{% static 'qcg/treegrid/css/jquery.treegrid.css' %}" rel="stylesheet">
                     firstDay: 1
                 }
             });
-
-            $('#toggle-advanced').click(function() {
-                if($('#advanced-filters').attr('aria-expanded') == "false")
-                    $(this).text('« mniej');
-                else
-                    $(this).text('więcej »');
-            });
         });
     </script>
 {% endblock %}
 
 {% block container %}
+    <form id="simple-form" action="." role="search" class="pull-right">
+        {% get_params_as_hidden 'host' 'submission' 'finish' %}
+        <div style="display: inline-block; vertical-align: middle;">
+            <div class="input-group">
+                <input type="text" name="keywords" value="{{ filters.keywords.value|default:'' }}"
+                       maxlength="500" class="form-control" placeholder="Wyszukaj frazę">
+                <span class="input-group-btn">
+                    <button class="btn btn-default" type="submit" title="Szukaj">
+                        <span class="glyphicon glyphicon-search"></span>
+                    </button>
+                </span>
+            </div>
+        </div>
+        <div class="btn-group" data-toggle="buttons">
+            <label class="btn btn-default{% if checked_status.0 %} active{% endif %}" title="Aktywne">
+                <input name="status" value="0" type="checkbox" {% if checked_status.0 %}checked{% endif %} onchange="this.form.submit()">
+                <span class="glyphicon glyphicon-hourglass" aria-hidden="true"></span>
+            </label>
+            <label class="btn btn-default{% if checked_status.1 %} active{% endif %}" title="Zakończone">
+                <input name="status" value="1" type="checkbox" {% if checked_status.1 %}checked{% endif %} onchange="this.form.submit()">
+                <span class="glyphicon glyphicon-saved" aria-hidden="true"></span>
+            </label>
+            <label class="btn btn-default{% if checked_status.2 %} active{% endif %}" title="Niepowodzenia">
+                <input name="status" value="2" type="checkbox" {% if checked_status.2 %}checked{% endif %} onchange="this.form.submit()">
+                <span class="glyphicon glyphicon-alert" aria-hidden="true"></span>
+            </label>
+        </div>
+        <a href="#advanced" data-toggle="modal" class="btn btn-default" title="Filtry zaawansowane" style="margin-left: 4px">
+            <span class="glyphicon glyphicon-option-horizontal"></span>
+        </a>
+    </form>
+
     <h1 class="page-header">
         {% block title %}Lista zadań{% endblock %}
     </h1>
 
-    <form action=".">
+    {% if selected_filters %}
         <div class="row">
-            <div class="col-lg-10 col-lg-offset-1">
-                <div class="pull-right">
-                    <a id="toggle-advanced" href="#advanced-filters" data-toggle="collapse">
-                        {% if advanced %}
-                            &laquo;&nbsp;mniej
-                        {% else %}
-                            więcej&nbsp;&raquo;
-                        {% endif %}
-                    </a>
-                    &nbsp;
-                    <a href="." class="btn btn-default{% if not filters.data %} disabled{% endif %}">Wyczyść</a>
-                    <button type="submit" class="btn btn-default">Filtruj</button>
-                </div>
-                <div class="btn-toolbar">
-                    <div class="btn-group" data-toggle="buttons">
-                        {% for choice in filters.status %}
-                            <label class="btn btn-default{% if choice.is_checked %} active{% endif %}">
-                                <input type="checkbox" name="{{ choice.name }}" value="{{ choice.choice_value }}"
-                                       {% if choice.is_checked %}checked{% endif %}> {{ choice.choice_label }}
-                            </label>
-                        {% endfor %}
-                    </div>
-                    <div class="btn-group" data-toggle="buttons">
-                        {% for choice in filters.host %}
-                            <label class="btn btn-default{% if choice.is_checked %} active{% endif %}">
-                                <input type="checkbox" name="{{ choice.name }}" value="{{ choice.choice_value }}"
-                                       {% if choice.is_checked %}checked{% endif %}> {{ choice.choice_label }}
-                            </label>
-                        {% endfor %}
-                    </div>
-                </div>
-            </div>
-        </div>
-{#        <div id="advanced-filters" class="form-horizontal collapse in" aria-expanded="true" style="margin-top: 10px">#}
-{#            <div class="row">#}
-{#                {% bootstrap_field filters.keywords layout='horizontal' horizontal_label_class='col-md-5' horizontal_field_class='col-md-4' bound_css_class=' ' %}#}
-{#            </div>#}
-{#            <div class="row">#}
-{#                {% bootstrap_field filters.status_exact layout='horizontal' horizontal_label_class='col-md-5' horizontal_field_class='col-md-4' bound_css_class=' ' %}#}
-{#            </div>#}
-{#            <div class="row">#}
-{#                {% bootstrap_field filters.submission layout='horizontal' horizontal_label_class='col-md-5' horizontal_field_class='col-md-4' bound_css_class=' ' %}#}
-{#            </div>#}
-{#            <div class="row">#}
-{#                {% bootstrap_field filters.finish layout='horizontal' horizontal_label_class='col-md-5' horizontal_field_class='col-md-4' bound_css_class=' ' %}#}
-{#            </div>#}
-{#        </div>#}
-        <div id="advanced-filters" class="form-horizontal collapse{% if advanced %} in" aria-expanded="true"{% else %}" aria-expanded="false"{% endif %} style="margin-top: 10px">
-            <div class="row">
-                <div class="col-ld-1"></div>
-                {% bootstrap_field filters.keywords layout='horizontal' form_group_class='form-group row col-lg-5 col-md-6' horizontal_label_class='col-md-4' horizontal_field_class='col-md-8' bound_css_class=' ' %}
-                {% bootstrap_field filters.status_exact layout='horizontal' form_group_class='form-group row col-lg-5 col-md-6' horizontal_label_class='col-md-4' horizontal_field_class='col-md-8' bound_css_class=' ' %}
-            </div>
-            <div class="row">
-                <div class="col-ld-1"></div>
-                {% bootstrap_field filters.submission layout='horizontal' form_group_class='form-group row col-lg-5 col-md-6' horizontal_label_class='col-md-4' horizontal_field_class='col-md-8' bound_css_class=' ' %}
-                {% bootstrap_field filters.finish layout='horizontal' form_group_class='form-group row col-lg-5 col-md-6' horizontal_label_class='col-md-4' horizontal_field_class='col-md-8' bound_css_class=' ' %}
+            <div class="col-md-offset-1 col-md-10">
+                <a href="." class="pull-right"><span class="text-muted small">Wyczyść wszystkie filtry</span></a>
+                <strong>Wybrane filtry:</strong>
+                {% for label, param, val in selected_filters %}
+                    <span class="label label-primary">{{ label }}&nbsp;<a href="{% query_string param-=val %}" style="color: white"><span class="glyphicon glyphicon-remove"></span></a></span>
+                {% endfor %}
             </div>
         </div>
-    </form>
-
-    <hr />
 
+        <hr>
+    {% endif %}
 
     <nav class="text-center" style="margin-bottom: 15px">
-        <form action="{{ request.path }}" method="get" class="form-inline" role="form" style="display: inline-block">
+        <form action="." method="get" class="form-inline" role="form" style="display: inline-block">
             <div class="input-group input-group-sm">
                 <span class="input-group-btn">
                     {% if page.has_previous %}
-                        <a href="{{ request.path }}?page={{ page.previous_page_number }}" class="btn btn-default">
+                        <a href="{% query_string "page"=page.previous_page_number %}" class="btn btn-default">
                     {% else %}
                         <a href="#" class="btn btn-default disabled">
                     {% endif %}
                 <input type="text" class="form-control text-center" id="page" name="page" value="{{ page.number }}" style="width: 50px">
                 <span class="input-group-btn">
                     {% if page.has_next %}
-                        <a href="{{ request.path }}?page={{ page.next_page_number }}" class="btn btn-default">
+                        <a href="{% query_string "page"=page.next_page_number %}" class="btn btn-default">
                     {% else %}
                         <a href="#" class="btn btn-default disabled">
                     {% endif %}
             </div>
         </form>
         &nbsp;z&nbsp;
-        <a href="{{ request.path }}?page={{ num_pages }}">{{ num_pages }}</a>
+        <a href="{% query_string "page"=num_pages %}">{{ num_pages }}</a>
     </nav>
 
     <table class="table table-hover tree">
                 <th>Koniec</th>
                 <th>Status</th>
                 <th>Host</th>
+                <th></th>
 {#                <th>Uwagi</th>#}
             </tr>
         </thead>
                             <td>{{ task.finish_time|date:"CUSTOM_DATETIME_FORMAT" }}</td>
                             <td>{{ task.get_status_display }}</td>
                             <td>{{ task.short_host_names|join:', ' }}</td>
+                            <td><a href="{{ task.get_absolute_url }}">szczegóły&nbsp;&rsaquo;</a></td>
                         </tr>
                     {% endwith %}
                 {% else %}
                         <td>{{ job.grouper.finish_time|date:"CUSTOM_DATETIME_FORMAT" }}</td>
                         <td>{{ job.grouper.get_status_display }}</td>
                         <td>-</td>
+                        <td><a href="{{ job.grouper.get_absolute_url }}">szczegóły&nbsp;&rsaquo;</a></td>
                     </tr>
 
                     {% for task in job.list %}
                             <td>{{ task.finish_time|date:"CUSTOM_DATETIME_FORMAT" }}</td>
                             <td>{{ task.get_status_display }}</td>
                             <td>{{ task.short_host_names|join:', ' }}</td>
+                            <td><a href="{{ task.get_absolute_url }}">szczegóły&nbsp;&rsaquo;</a></td>
                         </tr>
                     {% endfor %}
                 {% endifequal %}
     </table>
 
     {% if not page %}
-        <div class="alert alert-info">Brak elementów</div>
+        <div class="alert alert-info">
+            <span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
+            Brak zadań spełniających wybrane kryteria
+        </div>
     {% endif %}
 
     <nav class="text-center">
         <ul class="pagination">
             {% if page.has_previous %}
                 <li>
-                    <a href="{{ request.path }}?page={{ page.previous_page_number }}" aria-label="Previous">
+                    <a href="{% query_string "page"=page.previous_page_number %}" aria-label="Previous">
                     <span aria-hidden="true"><span class="glyphicon glyphicon-chevron-left"></span>&nbsp;poprzednia</span>
                 </a>
             {% endif %}
             </li>
 
             <li{% ifequal page.number 1 %} class="active"{% endifequal %}>
-                <a href="{{ request.path }}?page=1">1</a>
+                <a href="{% query_string "page"=1 %}">1</a>
             </li>
 
             {% if pages_range.0 > 2 %}
 
             {% for num in pages_range %}
                 <li{% ifequal page.number num %} class="active"{% endifequal %}>
-                    <a href="{{ request.path }}?page={{ num }}">{{ num }}</a>
+                    <a href="{% query_string "page"=num %}">{{ num }}</a>
                 </li>
             {% endfor %}
 
             <li class="disabled"><span aria-hidden="true"><strong>z</strong></span></li>
-            <li><a href="{{ request.path }}?page={{ num_pages }}">{{ num_pages }}</a></li>
+            <li><a href="{% query_string "page"=num_pages %}">{{ num_pages }}</a></li>
 
             {% if page.has_next %}
                 <li>
-                    <a href="{{ request.path }}?page={{ page.next_page_number }}" aria-label="Next">
+                    <a href="{% query_string "page"=page.next_page_number %}" aria-label="Next">
                     <span aria-hidden="true">następna&nbsp;<span class="glyphicon glyphicon-chevron-right"></span></span>
                 </a>
             {% endif %}
         </ul>
     </nav>
 
+    <!-- Modal -->
+    <div class="modal fade" id="advanced" tabindex="-1" role="dialog" aria-labelledby="modal-label" aria-hidden="true">
+        <div class="modal-dialog modal-lg">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+                    <h4 class="modal-title" id="modal-label">Filtry zaawansowane</h4>
+                </div>
+                <div class="modal-body">
+                    <form id="advanced-form" action="." class="form-horizontal">
+                        {% bootstrap_field filters.keywords layout='horizontal' bound_css_class=' ' %}
+                        <div class="form-group">
+                            <label class="col-md-4 control-label">Status</label>
+                            <div class="col-md-8">
+                                <div class="btn-group" data-toggle="buttons">
+                                    {% for choice in filters.status %}
+                                        <label class="btn btn-default{% if choice.is_checked %} active{% endif %}">
+                                            <input type="checkbox" name="{{ choice.name }}" value="{{ choice.choice_value }}"
+                                                   {% if choice.is_checked %}checked{% endif %}> {{ choice.choice_label }}
+                                        </label>
+                                    {% endfor %}
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="form-group">
+                            <label class="col-md-4 control-label">Host</label>
+                            <div class="col-md-8">
+                                <div class="btn-group" data-toggle="buttons">
+                                    {% for choice in filters.host %}
+                                        <label class="btn btn-default{% if choice.is_checked %} active{% endif %}">
+                                            <input type="checkbox" name="{{ choice.name }}" value="{{ choice.choice_value }}"
+                                                   {% if choice.is_checked %}checked{% endif %}> {{ choice.choice_label }}
+                                        </label>
+                                    {% endfor %}
+                                </div>
+                            </div>
+                        </div>
+                        {% bootstrap_field filters.submission layout='horizontal' bound_css_class=' ' %}
+                        {% bootstrap_field filters.finish layout='horizontal' bound_css_class=' ' %}
+                    </form>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-default" data-dismiss="modal">Anuluj</button>
+                    <a href="." class="btn btn-warning">Wyczyść filtry</a>
+                    <button type="submit" class="btn btn-primary" form="advanced-form">Filtruj</button>
+                </div>
+            </div>
+        </div>
+    </div>
+
 {% endblock container %}
index 33dd748..2dfe96e 100644 (file)
@@ -1,7 +1,7 @@
 from datetime import datetime, timedelta
 
 from django import template
-from django.utils.html import format_html
+from django.utils.html import format_html, format_html_join
 from django.utils.timesince import timesince
 from django.utils.timezone import now
 
@@ -27,3 +27,9 @@ def display_attribute(label, value):
                        u'   <div class="col-sm-3 text-right text-muted">{}</div>'
                        u'   <div class="col-sm-9">{}</div>'
                        u'</div>', label, value)
+
+
+@register.simple_tag(takes_context=True)
+def get_params_as_hidden(context, *params):
+    return format_html_join('\n', u'<input type="hidden" name="{}" value="{}">',
+                            ((param, v) for param in params for v in context['request'].GET.getlist(param, [])))
diff --git a/qcg/templatetags/query_string.py b/qcg/templatetags/query_string.py
new file mode 100644 (file)
index 0000000..235f3e2
--- /dev/null
@@ -0,0 +1,82 @@
+import re
+
+from django import template
+from django.utils.html import escape
+
+
+register = template.Library()
+
+
+class QueryStringNode(template.Node):
+    REPLACE = '='
+    APPEND = '+='
+    REMOVE = '-='
+    PURGE = '!'
+
+    OPERATORS = [REPLACE, APPEND, REMOVE, PURGE]
+
+    op_regex = re.compile('|'.join(map(re.escape, OPERATORS)))
+
+    def __init__(self, tag_name, parsed_args):
+        self.tag_name = tag_name
+        self.parsed_args = parsed_args
+
+    def render(self, context):
+        # django.core.context_processors.request should be enabled in
+        # settings.TEMPLATE_CONTEXT_PROCESSORS.
+        # Or else, directly pass the HttpRequest object as 'request' in context.
+
+        try:
+            query_dict = context['request'].GET.copy()
+        except KeyError:
+            return ''
+
+        for key, op, value in self.parsed_args:
+            key = key.resolve(context)
+
+            if op == self.APPEND:
+                # item+="foo": Append to current query arguments.
+                # e.g. item=1 -> item=1&item=foo
+                query_dict.appendlist(key, value.resolve(context))
+            elif op == self.REMOVE:
+                # item-="bar": Remove from current query arguments.
+                # e.g. item=1&item=bar -> item=1
+                try:
+                    query_dict.getlist(key).remove(value.resolve(context))
+                except KeyError:
+                    pass
+            elif op == self.PURGE:
+                # item!: Completely remove from current query arguments.
+                # e.g. item=1&item=2 -> ''
+                try:
+                    del query_dict[key]
+                except KeyError:
+                    pass
+            else:
+                # item=1: Replace current query arguments, e.g. item=2 -> item=1
+                query_dict[key] = value.resolve(context)
+
+        qs = query_dict.urlencode()
+        return '?' + escape(qs) if qs else ''
+
+
+@register.tag
+def query_string(parser, token):
+    # {% query_string page=1 size! item+="foo" item-="bar" %}
+    args = token.split_contents()
+
+    tag_name = args[0]
+    raw_pairs = args[1:]
+
+    parsed_args = []
+    for pair in raw_pairs:
+        try:
+            key, value = QueryStringNode.op_regex.split(pair, 1)
+        except:
+            raise template.TemplateSyntaxError("{} tag's argument should be in format foo({})bar".format(
+                tag_name, '|'.join(QueryStringNode.OPERATORS)))
+
+        parsed_args.append(
+            (parser.compile_filter(key), QueryStringNode.op_regex.search(pair).group(), parser.compile_filter(value)))
+
+    return QueryStringNode(tag_name, parsed_args)
index f3fd389..e996da0 100644 (file)
@@ -1,3 +1,4 @@
+# coding=utf-8
 from datetime import datetime, timedelta
 from django.conf import settings
 from django.contrib.auth import REDIRECT_FIELD_NAME
@@ -60,24 +61,14 @@ def jobs_list(request):
         .select_related('job').prefetch_related('allocations__nodes')
 
     filters = FiltersForm(request.GET)
-    advanced = False
+    selected_filters = []
     if filters.is_valid():
         keywords = filters.cleaned_data['keywords']
         status = filters.cleaned_data['status']
         host = filters.cleaned_data['host']
-        status_exact = filters.cleaned_data['status_exact']
         submission = filters.cleaned_data['submission']
         finish = filters.cleaned_data['finish']
 
-        if status:
-            statuses = []
-            for s in status:
-                statuses.extend(FiltersForm.STATUS_MAP[int(s)])
-
-            tasks = tasks.filter(status__in=statuses)
-        if host:
-            tasks = tasks.filter(allocations__host_name__in=host)
-
         if keywords:
             and_query = Q()
 
@@ -88,22 +79,43 @@ def jobs_list(request):
                 and_query &= or_query
 
             tasks = tasks.filter(and_query)
-        if status_exact:
-            tasks = tasks.filter(status=status_exact)
+            selected_filters.append((keywords, 'keywords', keywords))
+
+        if status:
+            statuses = []
+            for s in status:
+                si = int(s)
+                statuses.extend(FiltersForm.STATUS_MAP[si])
+                selected_filters.append((FiltersForm.STATUS_CHOICES_DICT[si], 'status', s))
+
+            tasks = tasks.filter(status__in=statuses)
+
+        if host:
+            tasks = tasks.filter(allocations__host_name__in=host)
+
+            host_dict = dict(filters.fields['host'].choices)
+            for h in host:
+                selected_filters.append((host_dict[h], 'host', h))
+
         if submission:
             start, end = submission.split('-')
 
             tasks = tasks.filter(submission_time__gte=parse_date(start),
                                  submission_time__lte=parse_date(end) + timedelta(days=1))
+            selected_filters.append((u'Data zlecenia: ' + submission, 'submission', submission))
+
         if finish:
             start, end = finish.split('-')
 
             tasks = tasks.filter(finish_time__gte=parse_date(start),
                                  finish_time__lte=parse_date(end) + timedelta(days=1))
+            selected_filters.append((u'Data zakończenia: ' + finish, 'finish', finish))
+
+        tasks = tasks.distinct()
 
-        advanced = bool(keywords or status_exact or submission or finish)
+    checked_status = {i: widget.is_checked() for i, widget in enumerate(filters['status'])}
 
-    context = {'filters': filters, 'advanced': advanced}
+    context = {'filters': filters, 'checked_status': checked_status, 'selected_filters': selected_filters}
     context.update(paginator_context(request, tasks))
 
     return render(request, 'qcg/jobs.html', context)