filtering in job list view
authorMaciej Tronowski <mtro@man.poznan.pl>
Mon, 23 Feb 2015 17:04:18 +0000 (18:04 +0100)
committerMaciej Tronowski <mtro@man.poznan.pl>
Mon, 23 Feb 2015 17:04:18 +0000 (18:04 +0100)
plgng/settings.py
qcg/forms.py [new file with mode: 0644]
qcg/templates/qcg/jobs.html
qcg/views.py
requirements.txt

index 2e2fed1..0567769 100644 (file)
@@ -44,6 +44,7 @@ INSTALLED_APPS = (
     'django.contrib.webdesign',
     'qcg',
     'django_openid_auth',
+    'bootstrap3',
 )
 
 MIDDLEWARE_CLASSES = (
diff --git a/qcg/forms.py b/qcg/forms.py
new file mode 100644 (file)
index 0000000..3d90145
--- /dev/null
@@ -0,0 +1,56 @@
+# coding=utf-8
+from django import forms
+from django.core.validators import RegexValidator
+from django.template.defaultfilters import capfirst
+from django.utils.functional import lazy
+from pyqcg.utils import TaskStatus
+
+from qcg.models import Task, Allocation
+
+
+def host_choices():
+    return tuple((host, capfirst(host.split('.')[0])) for host in
+                 Allocation.objects.values_list('host_name', flat=True).order_by('host_name').distinct())
+
+
+date_range_validator = RegexValidator(r'[0-9]{2}\.[0-9]{2}\.[0-9]{4} - [0-9]{2}\.[0-9]{2}\.[0-9]{4}')
+
+
+class FiltersForm(forms.Form):
+    ACTIVE, FINISHED, FAILED = range(3)
+    STATUS_CHOICES = (
+        (ACTIVE, u"Aktywne"),
+        (FINISHED, u"Zakończone"),
+        (FAILED, u"Niepowodzenia"),
+    )
+
+    STATUS_MAP = {
+        ACTIVE: (
+            Task.STATUS_CHOICES_REVERSED[TaskStatus.QUEUED],
+            Task.STATUS_CHOICES_REVERSED[TaskStatus.PREPROCESSING],
+            Task.STATUS_CHOICES_REVERSED[TaskStatus.PENDING],
+            Task.STATUS_CHOICES_REVERSED[TaskStatus.RUNNING],
+            Task.STATUS_CHOICES_REVERSED[TaskStatus.STOPPED],
+            Task.STATUS_CHOICES_REVERSED[TaskStatus.POSTPROCESSING],
+        ),
+        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 = forms.MultipleChoiceField(choices=STATUS_CHOICES, label=u"Status", required=False,
+                                       widget=forms.CheckboxSelectMultiple)
+    host = forms.MultipleChoiceField(choices=lazy(host_choices, tuple)(), label=u"Host", required=False,
+                                     widget=forms.CheckboxSelectMultiple)
+
+    # 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 c7ae855..58b4998 100644 (file)
@@ -1,9 +1,9 @@
 {% extends 'qcg/base.html' %}
-
-{% load staticfiles %}
+{% load staticfiles bootstrap3 %}
 
 {% block extra_css %}
     <link href="{% static 'qcg/treegrid/css/jquery.treegrid.css' %}" rel="stylesheet">
+    <link href="{% static 'qcg/daterangepicker/daterangepicker-bs3.css' %}" rel="stylesheet" />
 
     <style>
         .treegrid-expander {
@@ -18,6 +18,8 @@
 
 {% block extra_js %}
     <script src="{% static 'qcg/treegrid/js/jquery.treegrid.js' %}"></script>
+    <script src="{% static 'qcg/daterangepicker/moment.min.js' %}"></script>
+    <script src="{% static 'qcg/daterangepicker/daterangepicker.js' %}"></script>
 
     <script>
         $(function() {
                 }
             });
 
+            $('input[name="submission"],input[name="finish"]').daterangepicker({
+                opens: 'center',
+                format: 'DD.MM.YYYY',
+                ranges: {
+                    'Dzisiaj': [moment(), moment()],
+                    'Wczoraj': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
+                    'Ostatnie 7 dni': [moment().subtract(6, 'days'), moment()],
+                    'Ostatnie 30 dni': [moment().subtract(29, 'days'), moment()]
+                },
+                locale: {
+                    applyLabel: 'OK',
+                    cancelLabel: 'Anuluj',
+                    fromLabel: 'Od',
+                    toLabel: 'Do',
+                    weekLabel: 'T',
+                    customRangeLabel: 'Zakres',
+                    daysOfWeek: moment.weekdaysMin(),
+                    monthNames: ['Sty', 'Lut', 'Mar', 'Kwi', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Paź', 'Lis', 'Gru'],
+                    firstDay: 1
+                }
+            });
 
+            $('#toggle-advanced').click(function() {
+                if($('#advanced-filters').attr('aria-expanded') == "false")
+                    $(this).text('« mniej');
+                else
+                    $(this).text('więcej »');
+            });
         });
     </script>
 {% endblock %}
         {% block title %}Lista zadań{% endblock %}
     </h1>
 
-{#    <form class="row form-inline">#}
-{#        <div class="col-md-9 col-md-offset-1">#}
-{#            <div class="form-group">#}
-{#                <label class="sr-only" for="search">Szukaj w opisie lub uwagach</label>#}
-{#                <input type="text" class="form-control" id="search" placeholder="Szukaj w opisie lub uwagach">#}
+    <form action=".">
+        <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="form-group">#}
-{#                <label class="sr-only" for="status">Status</label>#}
-{#                <select class="form-control" id="status">#}
-{#                  <option>Wybierz stan</option>#}
-{#                  <option>PENDING</option>#}
-{#                  <option>RUNNING</option>#}
-{#                  <option>FAILED</option>#}
-{#                  <option>FINISHED</option>#}
-{#                </select>#}
+{#            <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="form-group">#}
-{#                <label class="sr-only" for="cluster">Klaster</label>#}
-{#                <select class="form-control" id="cluster">#}
-{#                  <option>Wybierz klaster</option>#}
-{#                  <option>Inula</option>#}
-{#                  <option>Galera</option>#}
-{#                  <option>Hydra</option>#}
-{#                  <option>Zeus</option>#}
-{#                </select>#}
+{#            <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 class="col-md-1">#}
-{#            <button type="submit" class="btn btn-default">Filtruj</button>#}
-{#        </div>#}
-{#    </form>#}
-{##}
-{#    <hr />#}
+        <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>
+        </div>
+    </form>
+
+    <hr />
 
 
     <nav class="text-center" style="margin-bottom: 15px">
         </tbody>
     </table>
 
+    {% if not page %}
+        <div class="alert alert-info">Brak elementów</div>
+    {% endif %}
+
     <nav class="text-center">
         <ul class="pagination">
             {% if page.has_previous %}
index 9f4a55e..0e637e5 100644 (file)
@@ -1,14 +1,18 @@
+from datetime import datetime, timedelta
 from django.conf import settings
 from django.contrib.auth import REDIRECT_FIELD_NAME
 from django.contrib.auth.decorators import login_required
 from django.core.urlresolvers import reverse
+from django.db.models import Q
 from django.http import HttpResponse
 from django.shortcuts import render, get_object_or_404
 from django.utils.http import urlencode
+from django.utils.timezone import UTC
 from django_openid_auth.views import make_consumer
 from openid.extensions import ax
 from pyqcg import QCG
 
+from qcg.forms import FiltersForm
 from qcg.utils import update_user_data, paginator_context
 
 
@@ -39,15 +43,70 @@ def openid_begin(request):
     return HttpResponse(openid_request.htmlMarkup(request.build_absolute_uri('/'), return_to))
 
 
+search_fields = ('status_description', 'type', 'note', 'task_id', 'job__note', 'job__project', 'job__job_id',
+                 'allocations__status_description', 'allocations__processes_group_id', 'allocations__comment')
+
+
+def parse_date(string):
+    return datetime.strptime(string.strip(), "%d.%m.%Y").replace(tzinfo=UTC())
+
+
 @login_required
 def jobs_list(request):
     # QCG.start()
     # update_user_data(request.user, request.session['proxy'])
 
     tasks = request.user.tasks.order_by('-job__submission_time', '-submission_time') \
-                              .select_related('job').prefetch_related('allocations__nodes')
-
-    context = paginator_context(request, tasks)
+        .select_related('job').prefetch_related('allocations__nodes')
+
+    filters = FiltersForm(request.GET)
+    advanced = False
+    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)])
+
+            print statuses
+
+            tasks = tasks.filter(status__in=statuses)
+        if host:
+            tasks = tasks.filter(allocations__host_name__in=host)
+
+        if keywords:
+            and_query = Q()
+
+            for q in keywords.split():
+                or_query = Q()
+                for field in search_fields:
+                    or_query |= Q(**{field + '__icontains': q})
+                and_query &= or_query
+
+            tasks = tasks.filter(and_query)
+        if status_exact:
+            tasks = tasks.filter(status=status_exact)
+        if submission:
+            start, end = submission.split('-')
+
+            tasks = tasks.filter(submission_time__gte=parse_date(start),
+                                 submission_time__lte=parse_date(end) + timedelta(days=1))
+        if finish:
+            start, end = finish.split('-')
+
+            tasks = tasks.filter(finish_time__gte=parse_date(start),
+                                 finish_time__lte=parse_date(end) + timedelta(days=1))
+
+        advanced = bool(keywords or status_exact or submission or finish)
+
+    context = {'filters': filters, 'advanced': advanced}
+    context.update(paginator_context(request, tasks))
 
     return render(request, 'qcg/jobs.html', context)
 
index f27deb6..f5b2d64 100644 (file)
@@ -1,4 +1,6 @@
 Django
 django-grappelli
+django-bootstrap3
 pkgs/django-openid-auth-0.5.1.tar.gz
 python-openid
+pytz