diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2018-07-09 20:59:24 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2018-08-13 18:26:03 +0200 |
commit | a2e2e57d792c8ad9f1ced7f3807b97635f38c36d (patch) | |
tree | 59990ede30849daf44a9323d7e9a18e17d707285 | |
parent | f642961947a1a4fb79e49ff764d93b3f0ab474db (diff) | |
download | Ishtar-a2e2e57d792c8ad9f1ced7f3807b97635f38c36d.tar.bz2 Ishtar-a2e2e57d792c8ad9f1ced7f3807b97635f38c36d.zip |
Save search queries
-rw-r--r-- | ishtar_common/forms_common.py | 56 | ||||
-rw-r--r-- | ishtar_common/migrations/0062_searchquery.py | 33 | ||||
-rw-r--r-- | ishtar_common/models.py | 19 | ||||
-rw-r--r-- | ishtar_common/static/js/ishtar.js | 14 | ||||
-rw-r--r-- | ishtar_common/templates/base.html | 3 | ||||
-rw-r--r-- | ishtar_common/templates/blocks/DataTables.html | 3 | ||||
-rw-r--r-- | ishtar_common/templates/blocks/bs_form_snippet.html | 5 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/forms/search_query.html | 122 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/forms/success.html | 20 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/wizard/search.html | 2 | ||||
-rw-r--r-- | ishtar_common/templates/widgets/search_input.html | 32 | ||||
-rw-r--r-- | ishtar_common/urls.py | 6 | ||||
-rw-r--r-- | ishtar_common/views.py | 45 |
13 files changed, 346 insertions, 14 deletions
diff --git a/ishtar_common/forms_common.py b/ishtar_common/forms_common.py index 47e976452..43652ba07 100644 --- a/ishtar_common/forms_common.py +++ b/ishtar_common/forms_common.py @@ -27,6 +27,7 @@ import tempfile from django import forms from django.conf import settings from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType from django.core import validators from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist @@ -1276,3 +1277,58 @@ AuthorFormset = formset_factory(AuthorFormSelection, can_delete=True, AuthorFormset.form_label = _("Authors") AuthorFormset.form_admin_name = _(u"Authors") AuthorFormset.form_slug = "authors" + + +class SearchQueryForm(forms.Form): + query = forms.CharField(max_length=None, label=_(u"Query"), disabled=True, + initial='*') + search_query = forms.ChoiceField(label=_(u"Search query"), required=False, + choices=[]) + label = forms.CharField(max_length=None, label=_(u"Name"), required=False) + is_alert = forms.BooleanField(label=_(u"Is an alert"), required=False) + create_or_update = forms.ChoiceField( + choices=(('create', _(u"Create")), + ('update', _(u"Update"))), initial='create') + + def __init__(self, profile, content_type, *args, **kwargs): + self.profile = profile + self.content_type = content_type + super(SearchQueryForm, self).__init__(*args, **kwargs) + self.fields['search_query'].choices = [ + (c.pk, c.label) for c in models.SearchQuery.objects.filter( + content_type=content_type, profile=profile).all()] + if not self.fields['search_query'].choices: + self.fields.pop('search_query') + + def clean(self): + data = self.cleaned_data + if data['create_or_update'] == 'create' and not data['label']: + raise forms.ValidationError(_(u"A label is required for a new " + u"search query.")) + elif data['create_or_update'] == 'update': + if not data['search_query']: + raise forms.ValidationError(_(u"Select the search query to " + u"update")) + q = models.SearchQuery.objects.filter( + profile=self.profile, content_type=self.content_type, + pk=data['search_query']) + if not q.count(): + raise forms.ValidationError(_(u"Query does not exist.")) + return data + + def save(self): + data = self.cleaned_data + if data['create_or_update'] == 'create': + sq = models.SearchQuery.objects.create( + label=data['label'], query=data['query'], profile=self.profile, + content_type=self.content_type, is_alert=data['is_alert']) + else: + try: + sq = models.SearchQuery.objects.get( + profile=self.profile, content_type=self.content_type, + pk=data['search_query']) + except models.SearchQuery.DoesNotExist: + raise forms.ValidationError(_(u"Query does not exist.")) + sq.query = data['query'] + sq.save() + return sq diff --git a/ishtar_common/migrations/0062_searchquery.py b/ishtar_common/migrations/0062_searchquery.py new file mode 100644 index 000000000..e886fe45a --- /dev/null +++ b/ishtar_common/migrations/0062_searchquery.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-07-09 13:12 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('ishtar_common', '0061_auto_20180704_1225'), + ] + + operations = [ + migrations.CreateModel( + name='SearchQuery', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.TextField(blank=True, verbose_name='Label')), + ('query', models.TextField(blank=True, verbose_name='Query')), + ('is_alert', models.BooleanField(default=False, verbose_name='Is an alert')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='Content type')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ishtar_common.UserProfile', verbose_name='Profile')), + ], + options={ + 'ordering': ['label'], + 'verbose_name': 'Search query', + 'verbose_name_plural': 'Search queries', + }, + ), + ] diff --git a/ishtar_common/models.py b/ishtar_common/models.py index d65c08f93..016883abe 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -1110,6 +1110,8 @@ class FullSearch(models.Model): :param save: True if you want to save the object immediately :return: True if modified """ + if not hasattr(self, 'search_vector'): + return if not self.pk: logger.warning("Cannot update search vector before save or " "after deletion.") @@ -1555,6 +1557,23 @@ def post_delete_record_relation(sender, instance, **kwargs): q.delete() +class SearchQuery(models.Model): + label = models.TextField(_(u"Label"), blank=True) + query = models.TextField(_(u"Query"), blank=True) + content_type = models.ForeignKey(ContentType, + verbose_name=_(u"Content type")) + profile = models.ForeignKey("UserProfile", verbose_name=_(u"Profile")) + is_alert = models.BooleanField(_(u"Is an alert"), default=False) + + class Meta: + verbose_name = _(u"Search query") + verbose_name_plural = _(u"Search queries") + ordering = ['label'] + + def __unicode__(self): + return unicode(self.label) + + class ShortMenuItem(object): @classmethod def get_short_menu_class(cls, pk): diff --git a/ishtar_common/static/js/ishtar.js b/ishtar_common/static/js/ishtar.js index 279d43b0e..0df286c26 100644 --- a/ishtar_common/static/js/ishtar.js +++ b/ishtar_common/static/js/ishtar.js @@ -30,6 +30,8 @@ function manage_async_link(event){ $.get(url, function(data) { $(target).html(data); }); + var modal_open = $(this).attr('data-modal-open'); + if (modal_open) $(modal_open).modal('show'); } /* default function to prevent undefined */ @@ -266,6 +268,7 @@ $(document).ready(function(){ $(".chosen-select").chosen(); $(".clear-search").click(function(){ $(this).parent().parent().children('input').prop("value", ""); + enable_save(); }); }); @@ -534,9 +537,18 @@ function get_label_from_input(input){ return input.parent().attr('data-alt-name'); } +var enable_save = function(){ + if ($(".search-widget input").val()){ + $("#save-search-button").removeClass('disabled'); + } else { + $("#save-search-button").addClass('disabled'); + } +} + function clear_search_field(){ $("#id_search_vector").val(""); add_message("-", 'info', "#advanced-search-info", true); + enable_save(); } function update_search_field(){ @@ -595,6 +607,7 @@ function update_search_field(){ } else { add_message("-", 'info', "#advanced-search-info", true); } + enable_save(); } var sheet_list = new Array(); @@ -646,3 +659,4 @@ function manage_pinned_search(name, data){ $('#pinned_search_' + name).hide(); } } + diff --git a/ishtar_common/templates/base.html b/ishtar_common/templates/base.html index bbf030409..40e30706c 100644 --- a/ishtar_common/templates/base.html +++ b/ishtar_common/templates/base.html @@ -148,6 +148,9 @@ </div> </div> </div> + <div class="modal" id="modal-dynamic-form" tabindex="-1" role="dialog" + data-backdrop="static" data-keyboard="true" aria-hidden="true"> + </div> <div id='message'> <div class='information'><i class="fa fa-info-circle" aria-hidden="true"></i> <span class='content'></span></div> </div> diff --git a/ishtar_common/templates/blocks/DataTables.html b/ishtar_common/templates/blocks/DataTables.html index fdd358363..4beabe82a 100644 --- a/ishtar_common/templates/blocks/DataTables.html +++ b/ishtar_common/templates/blocks/DataTables.html @@ -2,7 +2,8 @@ <p class="text-center"> -<button id='search_{{name}}' class='btn btn-primary' type="button"> +<button id='search_{{name}}' class='btn btn-primary search_button' + type="button"> {% trans "Search" %}</button> {% if url_new %} diff --git a/ishtar_common/templates/blocks/bs_form_snippet.html b/ishtar_common/templates/blocks/bs_form_snippet.html index 7bdac3bf3..95b8ac086 100644 --- a/ishtar_common/templates/blocks/bs_form_snippet.html +++ b/ishtar_common/templates/blocks/bs_form_snippet.html @@ -40,7 +40,7 @@ {% else %} {% if search and forloop.counter0 == 1 %} <div class="modal" id="modal-advanced-search" tabindex="-1" role="dialog" - data-backdrop="static" data-keyboard="false" aria-hidden="true"> + data-backdrop="static" data-keyboard="true" aria-hidden="true"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> @@ -114,5 +114,6 @@ {% if search %} <script type="text/javascript"> -$(register_advanced_search);</script> +$(register_advanced_search); +</script> {% endif %} diff --git a/ishtar_common/templates/ishtar/forms/search_query.html b/ishtar_common/templates/ishtar/forms/search_query.html new file mode 100644 index 000000000..e842fd591 --- /dev/null +++ b/ishtar_common/templates/ishtar/forms/search_query.html @@ -0,0 +1,122 @@ +{% load i18n inline_formset table_form %} +<div class="modal-dialog modal-sm" id="save-search-div"> + <div class="modal-content"> + <div class="modal-header"> + <h5>{% trans "Save search" %}</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <form enctype="multipart/form-data" + action="{% url 'save-search-query' app_label model %}" + method="post" + id="save-search-form"> + <div class="modal-body form-row"> + {% csrf_token %} + + <div class='form'> + + {% if form.non_field_errors %} + <div class="alert alert-danger" role="alert"> + {{form.non_field_errors}} + </div> + {% endif %} + + {% for hidden in form.hidden_fields %} + {{hidden}} + {% if hidden.errors %}<div class="invalid-feedback"> + {{ hidden.errors }} + </div>{% endif %} + {% endfor %} + + <div class="form-row"> + <div class="form-group col-lg-12"> + {{form.query}} + </div> + </div> + {% if form.search_query %} + <div class="form-row"> + <input type="radio" name="create_or_update" value="create" + id="create-choice" checked > + <label for="create-choice">{% trans "New" %}</label> + </div> + {% else %} + <input type="hidden" name="create_or_update" value="create"> + {% endif %} + <div id="new-search-query"> + <div class="form-row"> + {% with form.label as field %} + {% include "blocks/bs_field_snippet.html" %} + {% endwith %} + </div> + <div class="form-row"> + {% with form.is_alert as field %} + {% include "blocks/bs_field_snippet.html" %} + {% endwith %} + </div> + </div> + {% if form.search_query %} + <div class="form-row"> + <input type="radio" name="create_or_update" value="update" + id="update-choice"> + <label for="update-choice">{% trans "Update" %}</label> + </div> + <div id="update-search-query"> + <div class="form-row"> + {% with form.search_query as field %} + {% include "blocks/bs_field_snippet.html" %} + {% endwith %} + </div> + </div> + {% endif %} + </div> + </div> + <div class="modal-footer"> + <div class="col-sm"> + <button type="button" id="search-save-submit" + name='validate' + value="validate" class="btn btn-success"> + {% trans "Validate" %} + </button> + </div> + </div> + </form> + </div> +</div> + +<script type='text/javascript'> + +var update_form_display = function(){ + if ($("#create-choice:checked").length){ + $("#update-search-query").hide(); + $("#new-search-query").show(); + } else { + $("#new-search-query").hide(); + $("#update-search-query").show(); + } +} + +$(document).ready(function(){ + $("#id_query").val($(".search-vector").val()); + $("#search-save-submit").click( + function(){ + $.ajax({ + type: "POST", + url: "{% url 'save-search-query' app_label model %}", + data: $("#save-search-form").serialize(), + success: function(data){ + $("#save-search-div").parent().html(data); + }, + dataType: 'html' + }); + return false; + } + ); + $("#create-choice").click(update_form_display); + $("#update-choice").click(update_form_display); + update_form_display(); +}); + +</script> + + diff --git a/ishtar_common/templates/ishtar/forms/success.html b/ishtar_common/templates/ishtar/forms/success.html new file mode 100644 index 000000000..125ba62e7 --- /dev/null +++ b/ishtar_common/templates/ishtar/forms/success.html @@ -0,0 +1,20 @@ +{% load i18n inline_formset table_form %} +<script type='text/javascript'> + +$(document).ready(function(){ + $("#form-result-div").parent().modal("hide"); +}); + +</script> +<div class="modal-dialog modal-sm" id="form-result-div"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body form-row"> + {% trans "Form successfully submited" %} + </div> + </div> +</div> diff --git a/ishtar_common/templates/ishtar/wizard/search.html b/ishtar_common/templates/ishtar/wizard/search.html index e1f2433ea..eebd58d89 100644 --- a/ishtar_common/templates/ishtar/wizard/search.html +++ b/ishtar_common/templates/ishtar/wizard/search.html @@ -18,7 +18,7 @@ <form action="." class='wizard-form' id="wizard-form" method="post" name='wizard'>{% csrf_token %} {% if wizard.form.forms %} <div class='form'> -<div class='top_button'><input type="submit" id="submit_form" value="{% trans "Validate" %}"/></div> +<div class='top_button'><input type="submit" id="submit_form" value="{% trans 'Validate' %}"/></div> <table class='formset'> {%if wizard.form.non_form_errors%}<tr class='error'><th colspan='2'>{{wizard.form.non_form_errors}}</th></tr>{%endif%} {{ wizard.form.management_form }} diff --git a/ishtar_common/templates/widgets/search_input.html b/ishtar_common/templates/widgets/search_input.html index a1e5aa2e4..ddb62969f 100644 --- a/ishtar_common/templates/widgets/search_input.html +++ b/ishtar_common/templates/widgets/search_input.html @@ -4,28 +4,40 @@ <i class="fa fa-search" aria-hidden="true"></i> </span> </span> - <input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %} /> + <input type="{{ widget.type }}" name="{{ widget.name }}" + {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %} /> <span class="input-group-append"> <button type="button" class="btn btn-secondary" data-toggle="modal" - data-target="#modal-advanced-search"> + data-target="#modal-advanced-search" + title="{% trans 'Criteria' %}"> <i class="fa fa-cogs" aria-hidden="true"></i> </button> - </span> - <span class="input-group-append"> - <span class="input-group-text clear-search"> - <i class="fa fa-times" aria-hidden="true"></i> - </span> + <a type="button" class="async-link btn btn-secondary disabled" + id="save-search-button" + data-target="#modal-dynamic-form" + data-modal-open="#modal-dynamic-form" + title="{% trans 'Save search' %}" + href="{% url 'save-search-query' 'archaeological-operations' 'operation'%}"> + <i class="fa fa-floppy-o" aria-hidden="true"></i> + </a> + <span class="input-group-text clear-search"> + <i class="fa fa-times" aria-hidden="true"></i> + </span> </span> </div> <script type="text/javascript"> + $(document).ready(function(){ $(".search-widget input").keypress(function(e) { - if(e.which == 13) { - $(".search-widget").parents( - '.search').find("p > .btn-primary").click(); + if (e.which == 13) { + $(".search_button").click(); $(this).focus(); } }); + $(".search-widget input").keyup(function(e) { + enable_save(); + }); + enable_save(); }); </script> diff --git a/ishtar_common/urls.py b/ishtar_common/urls.py index 4ba551857..6b778ae45 100644 --- a/ishtar_common/urls.py +++ b/ishtar_common/urls.py @@ -124,6 +124,12 @@ urlpatterns = [ views.ImportStepByStepView.as_view(), name='import_step_by_step'), url(r'^profile(?:/(?P<pk>[0-9]+))?/$', views.ProfileEdit.as_view(), name='profile'), + url(r'^save-search/(?P<app_label>[a-z-]+)/(?P<model>[a-z-]+)/$', + views.SearchQueryEdit.as_view(), + name='save-search-query'), + url(r'^success/$', + TemplateView.as_view(template_name="ishtar/forms/success.html"), + name='success'), ] menu = Menu(None) diff --git a/ishtar_common/views.py b/ishtar_common/views.py index 5a35c2509..586e1f0a1 100644 --- a/ishtar_common/views.py +++ b/ishtar_common/views.py @@ -27,6 +27,7 @@ import unicodecsv from django.conf import settings from django.contrib.auth import logout from django.contrib.auth.decorators import login_required +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse, NoReverseMatch from django.db.models import Q @@ -1659,3 +1660,47 @@ document_deletion_wizard = wizards.DocumentDeletionWizard.as_view( document_deletion_steps, label=_(u"Document deletion"), url_name='document_deletion',) + + +class SearchQueryEdit(LoginRequiredMixin, FormView): + template_name = 'ishtar/forms/search_query.html' + form_class = forms.SearchQueryForm + + def dispatch(self, request, *args, **kwargs): + if not request.user.pk: + raise Http404() + try: + self.profile = models.UserProfile.objects.get( + current=True, person__ishtaruser__user_ptr=request.user) + except models.UserProfile.DoesNotExist: + # no current profile + raise Http404() + self.app_label = kwargs.get('app_label') + self.model = kwargs.get('model') + try: + self.content_type = ContentType.objects.get( + app_label=self.app_label.replace('-', '_'), + model=self.model.replace('-', '_') + ) + except ContentType.DoesNotExist: + raise Http404() + return super(SearchQueryEdit, self).dispatch(request, *args, **kwargs) + + def get_form_kwargs(self): + kwargs = super(SearchQueryEdit, self).get_form_kwargs() + kwargs['profile'] = self.profile + kwargs['content_type'] = self.content_type + return kwargs + + def get_context_data(self, **kwargs): + data = super(SearchQueryEdit, self).get_context_data(**kwargs) + data['app_label'] = self.app_label + data['model'] = self.model + return data + + def form_valid(self, form): + form.save() + return HttpResponseRedirect(self.get_success_url()) + + def get_success_url(self): + return reverse('success') |