diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2018-06-05 20:42:14 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2018-06-12 09:57:24 +0200 |
commit | 5341c3beb0fc3b52e864156bbfee54db78612b20 (patch) | |
tree | 2e9d8c696298f89e33e713d4eaf2a4c1c48af3b5 /ishtar_common | |
parent | 5a52b18c2cd4fcde86cefb84f90b4bd0df7be5a3 (diff) | |
download | Ishtar-5341c3beb0fc3b52e864156bbfee54db78612b20.tar.bz2 Ishtar-5341c3beb0fc3b52e864156bbfee54db78612b20.zip |
Document form - refactoring (refs #4107)
Diffstat (limited to 'ishtar_common')
-rw-r--r-- | ishtar_common/context_processors.py | 2 | ||||
-rw-r--r-- | ishtar_common/forms_common.py | 35 | ||||
-rw-r--r-- | ishtar_common/models.py | 61 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/sheet_document.html | 4 | ||||
-rw-r--r-- | ishtar_common/templatetags/window_field.py | 24 | ||||
-rw-r--r-- | ishtar_common/urls.py | 8 | ||||
-rw-r--r-- | ishtar_common/utils.py | 46 | ||||
-rw-r--r-- | ishtar_common/views.py | 1003 | ||||
-rw-r--r-- | ishtar_common/views_item.py | 936 | ||||
-rw-r--r-- | ishtar_common/widgets.py | 54 |
10 files changed, 1156 insertions, 1017 deletions
diff --git a/ishtar_common/context_processors.py b/ishtar_common/context_processors.py index 108fb7ec4..225b004a4 100644 --- a/ishtar_common/context_processors.py +++ b/ishtar_common/context_processors.py @@ -28,7 +28,7 @@ from menus import Menu def get_base_context(request): - dct = {'URL_PATH': settings.URL_PATH} + dct = {'URL_PATH': settings.URL_PATH, 'BASE_URL': ''} if 'HTTP_HOST' in request.META: dct['BASE_URL'] = "{}://{}".format(request.scheme, request.META['HTTP_HOST']) diff --git a/ishtar_common/forms_common.py b/ishtar_common/forms_common.py index 0473d19b6..9abc6551a 100644 --- a/ishtar_common/forms_common.py +++ b/ishtar_common/forms_common.py @@ -34,7 +34,7 @@ from django.core.files import File from django.forms.formsets import formset_factory from django.forms.models import BaseModelFormSet, BaseFormSet from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, pgettext import models import widgets @@ -1057,24 +1057,30 @@ def get_image_help(): ####################### -class DocumentForm(CustomForm, ManageOldType): - form_label = _(u"Documentation informations") +class DocumentForm(forms.ModelForm, CustomForm, ManageOldType): + form_label = _(u"Documentation") form_admin_name = _("Document - General") - + form_slug = "document-general" file_upload = True associated_models = {'source_type': models.SourceType} + title = forms.CharField(label=_(u"Title"), required=False, validators=[validators.MaxLengthValidator(200)]) - source_type = forms.ChoiceField(label=_(u"Source type"), choices=[], - required=False) + source_type = widgets.ModelChoiceField( + model=models.SourceType, label=_(u"Source type"), choices=[], + required=False) + authors = widgets.ModelJQueryAutocompleteField( + model=models.Author, multiple=True, label=_(u"Authors"), new=True, + long_widget=True, required=False) + associated_url = forms.URLField( + max_length=1000, required=False, + label=_(u"Numerical ressource (web address)")) image = forms.ImageField( label=_(u"Image"), help_text=mark_safe(get_image_help()), max_length=255, required=False, widget=widgets.ImageFileInput()) associated_file = forms.FileField( - label=_(u"File"), max_length=255, required=False) - associated_url = forms.URLField( - max_length=1000, required=False, - label=_(u"Numerical ressource (web address)")) + label=pgettext(u"File", u"Not directory"), max_length=255, + required=False) reference = forms.CharField( label=_(u"Reference"), validators=[validators.MaxLengthValidator(100)], required=False) @@ -1102,6 +1108,15 @@ class DocumentForm(CustomForm, ManageOldType): FieldType('source_type', models.SourceType), ] + class Meta: + model = models.Document + fields = [ + 'title', 'source_type', 'authors', 'associated_url', 'image', + 'associated_file', 'reference', 'internal_reference', + 'receipt_date', 'creation_date', 'receipt_date_in_documentation', + 'comment', 'description', 'additional_information', 'duplicate' + ] + def clean(self): cleaned_data = self.cleaned_data if not cleaned_data.get('title', None) and \ diff --git a/ishtar_common/models.py b/ishtar_common/models.py index de34a2b9c..d13def4c7 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -88,41 +88,6 @@ def post_save_user(sender, **kwargs): post_save.connect(post_save_user, sender=User) -def check_model_access_control(request, model, available_perms=None): - """ - Check access control to a model for a specific request - - :param request: the current request - :param model: the concerned model - :param available_perms: specific permissions to check if not specified - "view" and "view_own" will be checked - :return: (allowed, own) tuple - """ - own = True # more restrictive by default - allowed = False - if not request.user.is_authenticated(): - return allowed, own - - if not available_perms: - available_perms = ['view_' + model.__name__.lower(), - 'view_own_' + model.__name__.lower()] - if request.user.ishtaruser.has_right('administrator', - session=request.session): - allowed = True - own = False - return allowed, own - for perm, lbl in model._meta.permissions: - if perm not in available_perms: - continue - if request.user.ishtaruser.person.has_right( - perm, session=request.session): - allowed = True - if "_own_" not in perm: - own = False - break # max right reach - return allowed, own - - class ValueGetter(object): _prefix = "" GET_VALUES_EXTRA = [] @@ -3128,7 +3093,7 @@ class Document(OwnPerms, ImageModel, FullSearch): scale = models.CharField(_(u"Scale"), max_length=30, null=True, blank=True) authors = models.ManyToManyField(Author, verbose_name=_(u"Authors"), - related_name="%(class)s_related") + related_name="documents") authors_raw = models.CharField(verbose_name=_(u"Authors (raw)"), blank=True, null=True, max_length=250) associated_url = models.URLField( @@ -3182,6 +3147,19 @@ class Document(OwnPerms, ImageModel, FullSearch): self.index) """ + @property + def images(self): + # mimic a queryset pointing to himself + return Document.objects.filter(pk=self.pk, + image__isnull=False).exclude(image='') + + @property + def has_related(self): + for rel in self.RELATED_MODELS: + if getattr(self, rel).count(): + return True + return False + @classmethod def get_query_owns(cls, ishtaruser): Operation = cls.operations.rel.related_model @@ -3204,11 +3182,12 @@ class Document(OwnPerms, ImageModel, FullSearch): return slugify(u"-".join(values)) def _get_base_image_paths(self): - for related_model in self.RELATED_MODELS: - q = getattr(self, related_model).all() - if q.count(): - item = q.all()[0] - yield item._get_base_image_path() + if self.pk: # m2m not available if not created... + for related_model in self.RELATED_MODELS: + q = getattr(self, related_model).all() + if q.count(): + item = q.all()[0] + yield item._get_base_image_path() def _get_base_image_path(self): for path in self._get_base_image_paths(): diff --git a/ishtar_common/templates/ishtar/sheet_document.html b/ishtar_common/templates/ishtar/sheet_document.html index 3d48963a2..4f067fe56 100644 --- a/ishtar_common/templates/ishtar/sheet_document.html +++ b/ishtar_common/templates/ishtar/sheet_document.html @@ -26,6 +26,8 @@ {% field_flex "Title" item.title %} {% field_flex "Index" item.index %} {% field_flex "Source type" item.source_type %} + {% trans "File" context "Not directory" as file_label %} + {% field_flex_file file_label item.associated_file %} {% field_flex "Format type" item.format_type %} {% field_flex "Scale" item.scale %} {% trans "Web link" as weblink_label %} @@ -44,8 +46,8 @@ </div> {% block related %} +{% if item.has_related %} <h2>{% trans "Related items" %}</h2> -{% if item.operations.count %} {% field_flex_full "Sites" item.sites|add_links %} {% field_flex_full "Operations" item.operations|add_links %} {% field_flex_full "Context records" item.context_records|add_links %} diff --git a/ishtar_common/templatetags/window_field.py b/ishtar_common/templatetags/window_field.py index cbc6f28c3..db57add4a 100644 --- a/ishtar_common/templatetags/window_field.py +++ b/ishtar_common/templatetags/window_field.py @@ -1,4 +1,7 @@ +import os + from django import template +from django.conf import settings from django.utils.translation import ugettext_lazy as _ from ishtar_common.templatetags.link_to_window import link_to_window @@ -37,12 +40,13 @@ def field_flex_full(caption, data, pre_data='', post_data=''): @register.inclusion_tag('ishtar/blocks/window_field_url.html') -def field_url(caption, link, link_name='', li=False): +def field_url(caption, link, link_name='', li=False, get_base_url=False): if link: link = link.strip() if not link.startswith('http://') and not link.startswith('https://'): link = 'http://' + link - return {'caption': caption, 'link': link, "link_name": link_name, 'li': li} + return {'caption': caption, 'link': link, "link_name": link_name, 'li': li, + 'get_base_url': get_base_url} @register.inclusion_tag('ishtar/blocks/window_field_url.html') @@ -55,6 +59,22 @@ def field_flex_url(caption, link, link_name=''): return field_url(caption, link, link_name) +@register.inclusion_tag('ishtar/blocks/window_field_url.html', + takes_context=True) +def field_file(context, caption, link): + link_name = "" + if link and getattr(link, 'path', None): + link = context['BASE_URL'] + link.url + link_name = link.split(os.sep)[-1] + return field_url(caption, link, link_name, get_base_url=True) + + +@register.inclusion_tag('ishtar/blocks/window_field_flex_url.html', + takes_context=True) +def field_flex_file(context, caption, link): + return field_file(context, caption, link) + + @register.inclusion_tag('ishtar/blocks/window_field_multiple.html') def field_multiple(caption, data, li=False): return {'caption': caption, 'data': data, 'li': li} diff --git a/ishtar_common/urls.py b/ishtar_common/urls.py index 787c72aba..5ef316723 100644 --- a/ishtar_common/urls.py +++ b/ishtar_common/urls.py @@ -228,13 +228,13 @@ urlpatterns += [ check_rights(['view_document', 'view_own_document'])( views.document_search_wizard), name='document_search'), - url(r'document_creation/(?P<step>.+)?$', + url(r'document_creation/$', check_rights(['add_document', 'add_own_document'])( - views.NewDocumentFormView.as_view()), + views.DocumentFormView.as_view()), name='document_creation'), - url(r'document_modification/(?P<step>.+)?$', + url(r'document_modification/(?P<pk>.+)/$', check_rights(['change_document', 'change_own_document'])( - views.NewDocumentFormView.as_view()), + views.DocumentFormView.as_view()), name='document_modification'), ] diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py index 0b5b1bd57..443a22111 100644 --- a/ishtar_common/utils.py +++ b/ishtar_common/utils.py @@ -17,6 +17,7 @@ # See the file COPYING for details. +from csv import QUOTE_ALL import datetime from functools import wraps from itertools import chain @@ -37,6 +38,7 @@ from django.contrib.sessions.backends.db import SessionStore from django.core.cache import cache from django.core.files import File from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect from django.utils.datastructures import MultiValueDict as BaseMultiValueDict from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _, ugettext @@ -57,6 +59,9 @@ class BColors: UNDERLINE = '\033[4m' +CSV_OPTIONS = {'delimiter': ',', 'quotechar': '"', 'quoting': QUOTE_ALL} + + def check_rights(rights=[], redirect_url='/'): """ Decorator that checks the rights to access the view. @@ -103,6 +108,41 @@ def check_rights_condition(rights): return func +def check_model_access_control(request, model, available_perms=None): + """ + Check access control to a model for a specific request + + :param request: the current request + :param model: the concerned model + :param available_perms: specific permissions to check if not specified + "view" and "view_own" will be checked + :return: (allowed, own) tuple + """ + own = True # more restrictive by default + allowed = False + if not request.user.is_authenticated(): + return allowed, own + + if not available_perms: + available_perms = ['view_' + model.__name__.lower(), + 'view_own_' + model.__name__.lower()] + if request.user.ishtaruser.has_right('administrator', + session=request.session): + allowed = True + own = False + return allowed, own + for perm, lbl in model._meta.permissions: + if perm not in available_perms: + continue + if request.user.ishtaruser.person.has_right( + perm, session=request.session): + allowed = True + if "_own_" not in perm: + own = False + break # max right reach + return allowed, own + + class MultiValueDict(BaseMultiValueDict): def get(self, *args, **kwargs): v = super(MultiValueDict, self).getlist(*args, **kwargs) @@ -756,10 +796,14 @@ def get_urls_for_model(model, views): Generate get and show url for a model """ urls = [ - url(r'show-{}(?:/(?P<pk>.+))?/(?P<type>.+)?$'.format(model.SLUG), + url(r'show-{}/(?P<pk>.+)/(?P<type>.+)?$'.format(model.SLUG), check_rights(['view_' + model.SLUG, 'view_own_' + model.SLUG])( getattr(views, 'show_' + model.SLUG)), name="show-" + model.SLUG), + url(r'^display-{}/(?P<pk>.+)/$'.format(model.SLUG), + check_rights(['view_' + model.SLUG, 'view_own_' + model.SLUG])( + getattr(views, 'display_' + model.SLUG)), + name='display-' + model.SLUG), url(r'get-{}/(?P<type>.+)?$'.format(model.SLUG), check_rights(['view_' + model.SLUG, 'view_own_' + model.SLUG])( getattr(views, 'get_' + model.SLUG)), diff --git a/ishtar_common/views.py b/ishtar_common/views.py index 187712676..81397dd70 100644 --- a/ishtar_common/views.py +++ b/ishtar_common/views.py @@ -17,74 +17,50 @@ # See the file COPYING for details. -from copy import copy, deepcopy import csv import datetime import json import logging -from markdown import markdown -import optparse -import re -from tempfile import NamedTemporaryFile -from tidylib import tidy_document as tidy import unicodedata -from unidecode import unidecode -import unicodecsv -from weasyprint import HTML, CSS -from weasyprint.fonts import FontConfiguration - -from extra_views import ModelFormSetView +import unicodecsv from django.conf import settings from django.contrib.auth import logout from django.contrib.auth.decorators import login_required -from django.contrib.postgres.search import SearchQuery -from django.contrib.staticfiles.templatetags.staticfiles import static from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse, NoReverseMatch -from django.db.models import Q, ImageField -from django.db.models.fields import FieldDoesNotExist +from django.db.models import Q from django.forms.models import modelformset_factory from django.http import HttpResponse, Http404, HttpResponseRedirect, \ HttpResponseBadRequest from django.shortcuts import redirect, render -from django.template import loader from django.utils.decorators import method_decorator from django.utils.translation import ugettext, ugettext_lazy as _ -from django.views.generic import ListView, UpdateView, TemplateView, FormView +from django.views.generic import ListView, UpdateView, TemplateView from django.views.generic.edit import CreateView, DeleteView, FormView +from extra_views import ModelFormSetView +from markdown import markdown -from xhtml2odt import xhtml2odt - -from menus import Menu - -from archaeological_files.models import File -from archaeological_operations.models import Operation +import models from archaeological_context_records.models import ContextRecord -from archaeological_finds.models import Find, Treatment, TreatmentFile, \ - FindBasket - -from archaeological_operations.forms import DashboardForm as DashboardFormOpe from archaeological_files.forms import DashboardForm as DashboardFormFile +from archaeological_files.models import File from archaeological_finds.forms import DashboardTreatmentForm, \ DashboardTreatmentFileForm - -from ishtar_common.forms import FinalForm, FinalDeleteForm -from ishtar_common.widgets import JQueryAutoComplete -from ishtar_common.utils import clean_session_cache, \ - get_all_field_names, get_field_labels_from_path, \ - get_random_item_image_link, shortify +from archaeological_finds.models import Find, Treatment, TreatmentFile +from archaeological_operations.forms import DashboardForm as DashboardFormOpe +from archaeological_operations.models import Operation from ishtar_common import forms_common as forms from ishtar_common import wizards -from ishtar_common.models import HistoryError, PRIVATE_FIELDS, \ - get_current_profile - +from ishtar_common.forms import FinalForm, FinalDeleteForm +from ishtar_common.models import get_current_profile from ishtar_common.templatetags.link_to_window import link_to_window +from ishtar_common.utils import clean_session_cache, CSV_OPTIONS, \ + get_field_labels_from_path, get_random_item_image_link, shortify +from ishtar_common.widgets import JQueryAutoComplete -import models - -CSV_OPTIONS = {'delimiter': ',', 'quotechar': '"', 'quoting': csv.QUOTE_ALL} -ENCODING = settings.ENCODING or 'utf-8' +from views_item import CURRENT_ITEM_KEYS, display_item, get_item, new_item, \ + show_item logger = logging.getLogger(__name__) @@ -367,15 +343,6 @@ def shortcut_menu(request): return render(request, 'ishtar/blocks/shortcut_menu.html', dct) -CURRENT_ITEM_KEYS = (('file', File), - ('operation', Operation), - ('contextrecord', ContextRecord), - ('find', Find), - ('treatmentfile', TreatmentFile), - ('treatment', Treatment)) -CURRENT_ITEM_KEYS_DICT = dict(CURRENT_ITEM_KEYS) - - def get_current_items(request): currents = {} for key, model in CURRENT_ITEM_KEYS: @@ -466,17 +433,27 @@ def update_current_item(request, item_type=None, pk=None): return HttpResponse('ok') -def check_permission(request, action_slug, obj_id=None): - MAIN_MENU = Menu(None) - MAIN_MENU.init() - if action_slug not in MAIN_MENU.items: - # TODO - return True - if obj_id: - return MAIN_MENU.items[action_slug].is_available( - request.user, obj_id, session=request.session) - return MAIN_MENU.items[action_slug].can_be_available( - request.user, session=request.session) +def get_by_importer(request, slug, data_type='json', full=False, + force_own=False, **dct): + q = models.ImporterType.objects.filter(slug=slug) + if not q.count(): + res = '' + if data_type == "json": + res = '{}' + return HttpResponse(res, content_type='text/plain') + imp = q.all()[0].get_importer_class() + cols, col_names = [], [] + for formater in imp.LINE_EXPORT_FORMAT: + if not formater: + cols.append('') + col_names.append("") + continue + cols.append(formater.export_field_name) + col_names.append(formater.label) + obj_name = imp.OBJECT_CLS.__name__.lower() + return get_item( + imp.OBJECT_CLS, 'get_' + obj_name, obj_name, own_table_cols=cols + )(request, data_type, full, force_own, col_names=col_names, **dct) def autocomplete_person_permissive(request, person_types=None, @@ -603,873 +580,6 @@ def department_by_state(request, state_id=''): return HttpResponse(data, content_type='text/plain') -def format_val(val): - if val is None: - return u"" - if type(val) == bool: - if val: - return unicode(_(u"True")) - else: - return unicode(_(u"False")) - if type(val) == str: - val = val.decode('utf-8') - return unicode(val) - - -HIERARCHIC_LEVELS = 5 -HIERARCHIC_FIELDS = ['periods', 'period', 'unit', 'material_types', - 'material_type', 'conservatory_state', 'object_types'] - - -def _get_values(request, val): - if hasattr(val, 'all'): # manage related objects - vals = list(val.all()) - else: - vals = [val] - new_vals = [] - for v in vals: - if callable(v): - v = v() - if hasattr(v, 'url'): - v = request.is_secure() and \ - 'https' or 'http' + '://' + \ - request.get_host() + v.url - new_vals.append(v) - return new_vals - - -def _search_manage_search_vector(dct): - if 'search_vector' in dct: - dct['search_vector'] = SearchQuery( - unidecode(dct['search_vector']), - config=settings.ISHTAR_SEARCH_LANGUAGE - ) - return dct - - -DEFAULT_ROW_NUMBER = 10 -# length is used by ajax DataTables requests -EXCLUDED_FIELDS = ['length'] - - -def get_item(model, func_name, default_name, extra_request_keys=[], - base_request=None, bool_fields=[], reversed_bool_fields=[], - dated_fields=[], associated_models=[], relative_session_names=[], - specific_perms=[], own_table_cols=None, relation_types_prefix={}, - do_not_deduplicate=False): - """ - Generic treatment of tables - - :param model: model used for query - :param func_name: name of the function (used for session storage) - :param default_name: key used for default search in session - :param extra_request_keys: default query limitation - :param base_request: - :param bool_fields: - :param reversed_bool_fields: - :param dated_fields: - :param associated_models: - :param relative_session_names: - :param specific_perms: - :param own_table_cols: - :param relation_types_prefix: - :param do_not_deduplicate: duplication of id can occurs on large queryset a - mecanism of deduplication is used. But duplicate ids can be normal (for - instance for record_relations view). - :return: - """ - def func(request, data_type='json', full=False, force_own=False, - col_names=None, **dct): - available_perms = [] - if specific_perms: - available_perms = specific_perms[:] - EMPTY = '' - if 'type' in dct: - data_type = dct.pop('type') - if not data_type: - EMPTY = '[]' - data_type = 'json' - - allowed, own = models.check_model_access_control(request, model, - available_perms) - if not allowed: - return HttpResponse(EMPTY, content_type='text/plain') - - if force_own: - own = True - if full == 'shortcut' and 'SHORTCUT_SEARCH' in request.session and \ - request.session['SHORTCUT_SEARCH'] == 'own': - own = True - - # get defaults from model - if not extra_request_keys and hasattr(model, 'EXTRA_REQUEST_KEYS'): - my_extra_request_keys = copy(model.EXTRA_REQUEST_KEYS) - else: - my_extra_request_keys = copy(extra_request_keys) - if base_request is None and hasattr(model, 'BASE_REQUEST'): - my_base_request = copy(model.BASE_REQUEST) - elif base_request is not None: - my_base_request = copy(base_request) - else: - my_base_request = {} - if not bool_fields and hasattr(model, 'BOOL_FIELDS'): - my_bool_fields = model.BOOL_FIELDS[:] - else: - my_bool_fields = bool_fields[:] - if not reversed_bool_fields and hasattr(model, 'REVERSED_BOOL_FIELDS'): - my_reversed_bool_fields = model.REVERSED_BOOL_FIELDS[:] - else: - my_reversed_bool_fields = reversed_bool_fields[:] - if not dated_fields and hasattr(model, 'DATED_FIELDS'): - my_dated_fields = model.DATED_FIELDS[:] - else: - my_dated_fields = dated_fields[:] - if not associated_models and hasattr(model, 'ASSOCIATED_MODELS'): - my_associated_models = model.ASSOCIATED_MODELS[:] - else: - my_associated_models = associated_models[:] - if not relative_session_names and hasattr(model, - 'RELATIVE_SESSION_NAMES'): - my_relative_session_names = model.RELATIVE_SESSION_NAMES[:] - else: - my_relative_session_names = relative_session_names[:] - if not relation_types_prefix and hasattr(model, - 'RELATION_TYPES_PREFIX'): - my_relation_types_prefix = copy(model.RELATION_TYPES_PREFIX) - else: - my_relation_types_prefix = copy(relation_types_prefix) - - fields = [model._meta.get_field(k) - for k in get_all_field_names(model)] - - request_keys = dict([ - (field.name, - field.name + (hasattr(field, 'rel') and field.rel and '__pk' - or '')) - for field in fields]) - for associated_model, key in my_associated_models: - if type(associated_model) in (str, unicode): - if associated_model not in globals(): - continue - associated_model = globals()[associated_model] - associated_fields = [ - associated_model._meta.get_field(k) - for k in get_all_field_names(associated_model)] - request_keys.update( - dict([(key + "__" + field.name, - key + "__" + field.name + - (hasattr(field, 'rel') and field.rel and '__pk' or '')) - for field in associated_fields])) - request_keys.update(my_extra_request_keys) - request_items = request.method == 'POST' and request.POST \ - or request.GET - - # pager - try: - row_nb = int(request_items.get('length')) - except (ValueError, TypeError): - row_nb = DEFAULT_ROW_NUMBER - dct_request_items = {} - - # filter requested fields - for k in request_items: - if k in EXCLUDED_FIELDS: - continue - key = k[:] - if key.startswith('searchprefix_'): - key = key[len('searchprefix_'):] - dct_request_items[key] = request_items[k] - request_items = dct_request_items - - dct = my_base_request - if full == 'shortcut': - dct['cached_label__icontains'] = request.GET.get('term', None) - and_reqs, or_reqs = [], [] - try: - old = 'old' in request_items and int(request_items['old']) - except ValueError: - return HttpResponse('[]', content_type='text/plain') - - # manage relations types - if 'relation_types' not in my_relation_types_prefix: - my_relation_types_prefix['relation_types'] = '' - relation_types = {} - for rtype_key in my_relation_types_prefix: - relation_types[my_relation_types_prefix[rtype_key]] = set() - for k in request_items: - if k.startswith(rtype_key): - relation_types[my_relation_types_prefix[rtype_key]].add( - request_items[k]) - continue - - for k in request_keys: - val = request_items.get(k) - if not val: - continue - req_keys = request_keys[k] - if type(req_keys) not in (list, tuple): - dct[req_keys] = val - continue - # multiple choice target - reqs = Q(**{req_keys[0]: val}) - for req_key in req_keys[1:]: - q = Q(**{req_key: val}) - reqs |= q - and_reqs.append(reqs) - - pinned_search = "" - if 'submited' not in request_items and full != 'shortcut': - # default search - # an item is selected in the default menu - if default_name in request.session and \ - request.session[default_name]: - value = request.session[default_name] - if 'basket-' in value: - try: - dct = {"basket__pk": - request.session[default_name].split('-')[-1]} - pinned_search = unicode(FindBasket.objects.get( - pk=dct["basket__pk"])) - except FindBasket.DoesNotExist: - pass - else: - try: - dct = {"pk": request.session[default_name]} - pinned_search = unicode(model._meta.verbose_name)\ - + u" - " + unicode( - model.objects.get(pk=dct["pk"])) - except model.DoesNotExist: - pass - elif dct == (my_base_request or {}): - # a parent item may be selected in the default menu - for name, key in my_relative_session_names: - if name in request.session and request.session[name] \ - and 'basket-' not in request.session[name] \ - and name in CURRENT_ITEM_KEYS_DICT: - up_model = CURRENT_ITEM_KEYS_DICT[name] - try: - dct.update({key: request.session[name]}) - pinned_search = unicode(up_model._meta.verbose_name)\ - + u" - " + unicode( - up_model.objects.get(pk=dct[key])) - break - except up_model.DoesNotExist: - pass - if (not dct or data_type == 'csv') \ - and func_name in request.session: - dct = request.session[func_name] - else: - request.session[func_name] = dct - for k in (list(my_bool_fields) + list(my_reversed_bool_fields)): - if k in dct: - if dct[k] == u"1": - dct.pop(k) - else: - dct[k] = dct[k] == u"2" and True or False - if k in my_reversed_bool_fields: - dct[k] = not dct[k] - # check also for empty value with image field - field_name = k.split('__')[0] - # TODO: can be improved in later version of Django - try: - c_field = model._meta.get_field(field_name) - if k.endswith('__isnull') and \ - isinstance(c_field, ImageField): - if dct[k]: - or_reqs.append( - (k, {k.split('__')[0] + '__exact': ''})) - else: - dct[k.split('__')[0] + '__regex'] = '.{1}.*' - except FieldDoesNotExist: - pass - for k in my_dated_fields: - if k in dct: - if not dct[k]: - dct.pop(k) - try: - items = dct[k].split('/') - assert len(items) == 3 - dct[k] = datetime.date(*map(lambda x: int(x), - reversed(items)))\ - .strftime('%Y-%m-%d') - except AssertionError: - dct.pop(k) - # manage hierarchic conditions - for req in dct.copy(): - if req.endswith('town__pk') or req.endswith('towns__pk'): - val = dct.pop(req) - reqs = Q(**{req: val}) - base_req = req[:-2] + '__' - req = base_req[:] - for idx in range(HIERARCHIC_LEVELS): - req = req[:-2] + 'parents__pk' - q = Q(**{req: val}) - reqs |= q - req = base_req[:] - for idx in range(HIERARCHIC_LEVELS): - req = req[:-2] + 'children__pk' - q = Q(**{req: val}) - reqs |= q - and_reqs.append(reqs) - continue - - for k_hr in HIERARCHIC_FIELDS: - if type(req) in (list, tuple): - val = dct.pop(req) - q = None - for idx, r in enumerate(req): - if not idx: - q = Q(**{r: val}) - else: - q |= Q(**{r: val}) - and_reqs.append(q) - break - elif req.endswith(k_hr + '__pk'): - val = dct.pop(req) - reqs = Q(**{req: val}) - req = req[:-2] + '__' - for idx in range(HIERARCHIC_LEVELS): - req = req[:-2] + 'parent__pk' - q = Q(**{req: val}) - reqs |= q - and_reqs.append(reqs) - break - dct = _search_manage_search_vector(dct) - query = Q(**dct) - for k, or_req in or_reqs: - alt_dct = dct.copy() - alt_dct.pop(k) - alt_dct.update(or_req) - query |= Q(**alt_dct) - - for rtype_prefix in relation_types: - vals = list(relation_types[rtype_prefix]) - if not vals: - continue - alt_dct = { - rtype_prefix + 'right_relations__relation_type__pk__in': vals} - for k in dct: - val = dct[k] - if rtype_prefix: - # only get conditions related to the object - if rtype_prefix not in k: - continue - # tricky: reconstruct the key to make sense - remove the - # prefix from the key - k = k[0:k.index(rtype_prefix)] + k[ - k.index(rtype_prefix) + len(rtype_prefix):] - if k.endswith('year'): - k += '__exact' - alt_dct[rtype_prefix + 'right_relations__right_record__' + k] =\ - val - if not dct: - # fake condition to trick Django (1.4): without it only the - # alt_dct is managed - query &= Q(pk__isnull=False) - query |= Q(**alt_dct) - for k, or_req in or_reqs: - altor_dct = alt_dct.copy() - altor_dct.pop(k) - for j in or_req: - val = or_req[j] - if j == 'year': - j = 'year__exact' - altor_dct[ - rtype_prefix + 'right_relations__right_record__' + j] =\ - val - query |= Q(**altor_dct) - - if own: - q = models.IshtarUser.objects.filter(user_ptr=request.user) - if q.count(): - query = query & model.get_query_owns(q.all()[0]) - else: - return HttpResponse(EMPTY, content_type='text/plain') - - for and_req in and_reqs: - query = query & and_req - - # manage hierarchic in shortcut menu - if full == 'shortcut': - ASSOCIATED_ITEMS = { - Operation: (File, 'associated_file__pk'), - ContextRecord: (Operation, 'operation__pk'), - Find: (ContextRecord, 'base_finds__context_record__pk'), - } - if model in ASSOCIATED_ITEMS: - upper_model, upper_key = ASSOCIATED_ITEMS[model] - model_name = upper_model.SLUG - current = model_name in request.session \ - and request.session[model_name] - if current: - dct = {upper_key: current} - query &= Q(**dct) - - items = model.objects.filter(query).distinct() - # print(items.query) - - if 'search_vector' in dct: # for serialization - dct['search_vector'] = dct['search_vector'].value - - # table cols - if own_table_cols: - table_cols = own_table_cols - else: - if full: - table_cols = [field.name for field in model._meta.fields - if field.name not in PRIVATE_FIELDS] - table_cols += [field.name for field in model._meta.many_to_many - if field.name not in PRIVATE_FIELDS] - if hasattr(model, 'EXTRA_FULL_FIELDS'): - table_cols += model.EXTRA_FULL_FIELDS - else: - table_cols = model.TABLE_COLS - query_table_cols = [] - for cols in table_cols: - if type(cols) not in (list, tuple): - cols = [cols] - for col in cols: - query_table_cols += col.split('|') - - # contextual (full, simple, etc.) col - contxt = full and 'full' or 'simple' - if hasattr(model, 'CONTEXTUAL_TABLE_COLS') and \ - contxt in model.CONTEXTUAL_TABLE_COLS: - for idx, col in enumerate(table_cols): - if col in model.CONTEXTUAL_TABLE_COLS[contxt]: - query_table_cols[idx] = \ - model.CONTEXTUAL_TABLE_COLS[contxt][col] - if full == 'shortcut': - query_table_cols = ['cached_label'] - table_cols = ['cached_label'] - - # manage sort tables - manual_sort_key = None - - sorts = {} - for k in request_items: - if not k.startswith('order['): - continue - num = int(k.split(']')[0][len("order["):]) - if num not in sorts: - sorts[num] = ['', ''] # sign, col_num - if k.endswith('[dir]'): - order = request_items[k] - sign = order and order == u'desc' and "-" or '' - sorts[num][0] = sign - if k.endswith('[column]'): - sorts[num][1] = request_items[k] - sign = "" - if not sorts and model._meta.ordering: - orders = [k for k in model._meta.ordering] - items = items.order_by(*orders) - else: - orders = [] - for idx in sorted(sorts.keys()): - signe, col_num = sorts[idx] - k = query_table_cols[int(col_num) - 2] # remove id and link col - if k in request_keys: - ks = request_keys[k] - if type(ks) not in (tuple, list): - ks = [ks] - for k in ks: - if k.endswith("__pk"): - k = k[:-len("__pk")] + "__label" - if '__' in k: - k = k.split('__')[0] - orders.append(signe + k) - else: - # not a standard request key - if idx: # not the first - we ignore this sort - continue - sign = signe - manual_sort_key = k - logger.warning( - "**WARN get_item - {}**: manual sort key '{}'".format( - func_name, k)) - break - if not manual_sort_key: - items = items.order_by(*orders) - - # pager management - start, end = 0, None - page_nb = 1 - if row_nb and data_type == "json": - try: - start = int(request_items.get('start')) - page_nb = start / row_nb + 1 - assert page_nb >= 1 - except (TypeError, ValueError, AssertionError): - start = 0 - page_nb = 1 - end = page_nb * row_nb - if full == 'shortcut': - start = 0 - end = 20 - - items_nb = items.count() - if manual_sort_key: - items = items.all() - else: - items = items[start:end] - - datas = [] - if old: - items = [item.get_previous(old) for item in items] - c_ids = [] - for item in items: - # manual deduplicate when distinct is not enough - if not do_not_deduplicate and item.pk in c_ids: - continue - c_ids.append(item.pk) - data = [item.pk] - for keys in query_table_cols: - if type(keys) not in (list, tuple): - keys = [keys] - my_vals = [] - for k in keys: - if hasattr(model, 'EXTRA_REQUEST_KEYS') \ - and k in model.EXTRA_REQUEST_KEYS: - k = model.EXTRA_REQUEST_KEYS[k] - if type(k) in (list, tuple): - k = k[0] - for filtr in ('__icontains', '__contains'): - if k.endswith(filtr): - k = k[:len(k) - len(filtr)] - vals = [item] - # foreign key may be divided by "." or "__" - splitted_k = [] - for ky in k.split('.'): - if '__' in ky: - splitted_k += ky.split('__') - else: - splitted_k.append(ky) - for ky in splitted_k: - new_vals = [] - for val in vals: - if hasattr(val, 'all'): # manage related objects - val = list(val.all()) - for v in val: - v = getattr(v, ky) - new_vals += _get_values(request, v) - elif val: - try: - val = getattr(val, ky) - new_vals += _get_values(request, val) - except AttributeError: - # must be a query key such as "contains" - pass - vals = new_vals - # manage last related objects - if vals and hasattr(vals[0], 'all'): - new_vals = [] - for val in vals: - new_vals += list(val.all()) - vals = new_vals - if not my_vals: - my_vals = [format_val(va) for va in vals] - else: - new_vals = [] - if not vals: - for idx, my_v in enumerate(my_vals): - new_vals.append(u"{}{}{}".format( - my_v, u' - ', '')) - else: - for idx, v in enumerate(vals): - new_vals.append(u"{}{}{}".format( - vals[idx], u' - ', format_val(v))) - my_vals = new_vals[:] - data.append(u" & ".join(my_vals) or u"") - datas.append(data) - if manual_sort_key: - # +1 because the id is added as a first col - idx_col = None - if manual_sort_key in query_table_cols: - idx_col = query_table_cols.index(manual_sort_key) + 1 - else: - for idx, col in enumerate(query_table_cols): - if type(col) in (list, tuple) and \ - manual_sort_key in col: - idx_col = idx + 1 - if idx_col is not None: - datas = sorted(datas, key=lambda x: x[idx_col]) - if sign == '-': - datas = reversed(datas) - datas = list(datas)[start:end] - link_template = "<a class='display_details' href='#' "\ - "onclick='load_window(\"%s\")'>"\ - "<i class=\"fa fa-info-circle\" aria-hidden=\"true\"></i></a>" - link_ext_template = '<a href="{}" target="_blank">{}</a>' - if data_type == "json": - rows = [] - for data in datas: - try: - lnk = link_template % reverse('show-' + default_name, - args=[data[0], '']) - except NoReverseMatch: - logger.warning( - '**WARN "show-' + default_name + '" args (' - + unicode(data[0]) + ") url not available") - lnk = '' - res = {'id': data[0], 'link': lnk} - for idx, value in enumerate(data[1:]): - if value: - table_col = table_cols[idx] - if type(table_col) not in (list, tuple): - table_col = [table_col] - tab_cols = [] - # foreign key may be divided by "." or "__" - for tc in table_col: - if '.' in tc: - tab_cols += tc.split('.') - elif '__' in tc: - tab_cols += tc.split('__') - else: - tab_cols.append(tc) - k = "__".join(tab_cols) - if hasattr(model, 'COL_LINK') and k in model.COL_LINK: - value = link_ext_template.format(value, value) - res[k] = value - if full == 'shortcut' and 'cached_label' in res: - res['value'] = res.pop('cached_label') - rows.append(res) - if full == 'shortcut': - data = json.dumps(rows) - else: - data = json.dumps({ - "recordsTotal": items_nb, - "recordsFiltered": items_nb, - "rows": rows, - "pinned-search": pinned_search, - "page": page_nb, - "total": (items_nb / row_nb + 1) if row_nb else items_nb, - }) - return HttpResponse(data, content_type='text/plain') - elif data_type == "csv": - response = HttpResponse(content_type='text/csv') - n = datetime.datetime.now() - filename = u'%s_%s.csv' % (default_name, - n.strftime('%Y%m%d-%H%M%S')) - response['Content-Disposition'] = 'attachment; filename=%s'\ - % filename - writer = csv.writer(response, **CSV_OPTIONS) - if col_names: - col_names = [name.encode(ENCODING, errors='replace') - for name in col_names] - else: - col_names = [] - for field_name in table_cols: - if type(field_name) in (list, tuple): - field_name = u" & ".join(field_name) - if hasattr(model, 'COL_LABELS') and\ - field_name in model.COL_LABELS: - field = model.COL_LABELS[field_name] - col_names.append(unicode(field).encode(ENCODING)) - continue - else: - try: - field = model._meta.get_field(field_name) - except: - col_names.append(u"".encode(ENCODING)) - logger.warning( - "**WARN get_item - csv export**: no col name " - "for {}\nadd explicit label to " - "COL_LABELS attribute of " - "{}".format(field_name, model)) - continue - col_names.append( - unicode(field.verbose_name).encode(ENCODING)) - writer.writerow(col_names) - for data in datas: - row, delta = [], 0 - # regroup cols with join "|" - for idx, col_name in enumerate(table_cols): - if len(data[1:]) <= idx + delta: - break - val = data[1:][idx + delta].encode( - ENCODING, errors='replace') - if col_name and "|" in col_name[0]: - for delta_idx in range( - len(col_name[0].split('|')) - 1): - delta += 1 - val += data[1:][idx + delta].encode( - ENCODING, errors='replace') - row.append(val) - writer.writerow(row) - return response - return HttpResponse('{}', content_type='text/plain') - - return func - - -def get_by_importer(request, slug, data_type='json', full=False, - force_own=False, **dct): - q = models.ImporterType.objects.filter(slug=slug) - if not q.count(): - res = '' - if data_type == "json": - res = '{}' - return HttpResponse(res, content_type='text/plain') - imp = q.all()[0].get_importer_class() - cols, col_names = [], [] - for formater in imp.LINE_EXPORT_FORMAT: - if not formater: - cols.append('') - col_names.append("") - continue - cols.append(formater.export_field_name) - col_names.append(formater.label) - obj_name = imp.OBJECT_CLS.__name__.lower() - return get_item( - imp.OBJECT_CLS, 'get_' + obj_name, obj_name, own_table_cols=cols - )(request, data_type, full, force_own, col_names=col_names, **dct) - - -def display_item(model, extra_dct=None, show_url=None): - def func(request, pk, **dct): - if show_url: - dct['show_url'] = "/{}{}/".format(show_url, pk) - else: - dct['show_url'] = "/show-{}/{}/".format(model.SLUG, pk) - return render(request, 'ishtar/display_item.html', dct) - return func - - -def show_item(model, name, extra_dct=None): - def func(request, pk, **dct): - allowed, own = models.check_model_access_control(request, model) - if not allowed: - return HttpResponse('', content_type="application/xhtml") - q = model.objects - if own: - query_own = model.get_query_owns(request.user) - if query_own: - q = q.filter(query_own) - try: - item = q.get(pk=pk) - except ObjectDoesNotExist: - return HttpResponse('NOK') - doc_type = 'type' in dct and dct.pop('type') - url_name = u"/".join(reverse('show-' + name, args=['0', ''] - ).split('/')[:-2]) + u"/" - dct['CURRENCY'] = get_current_profile().currency - dct['ENCODING'] = settings.ENCODING - dct['DOT_GENERATION'] = settings.DOT_BINARY and True - dct['current_window_url'] = url_name - date = None - if 'date' in dct: - date = dct.pop('date') - dct['sheet_id'] = "%s-%d" % (name, item.pk) - dct['window_id'] = "%s-%d-%s" % ( - name, item.pk, datetime.datetime.now().strftime('%M%s')) - if hasattr(item, 'history'): - if date: - try: - date = datetime.datetime.strptime(date, - '%Y-%m-%dT%H:%M:%S.%f') - item = item.get_previous(date=date) - assert item is not None - except (ValueError, AssertionError): - return HttpResponse(None, content_type='text/plain') - dct['previous'] = item._previous - dct['next'] = item._next - else: - historized = item.history.all() - if historized: - item.history_date = historized[0].history_date - if len(historized) > 1: - dct['previous'] = historized[1].history_date - dct['item'], dct['item_name'] = item, name - # add context - if extra_dct: - dct.update(extra_dct(request, item)) - context_instance = deepcopy(dct) - context_instance['output'] = 'html' - if hasattr(item, 'history_object'): - filename = item.history_object.associated_filename - else: - filename = item.associated_filename - if doc_type == "odt" and settings.ODT_TEMPLATE: - tpl = loader.get_template('ishtar/sheet_%s.html' % name) - context_instance['output'] = 'ODT' - content = tpl.render(context_instance, request) - try: - tidy_options = {'output-xhtml': 1, 'indent': 1, - 'tidy-mark': 0, 'doctype': 'auto', - 'add-xml-decl': 1, 'wrap': 1} - html, errors = tidy(content, options=tidy_options) - html = html.encode('utf-8').replace(" ", " ") - html = re.sub('<pre([^>]*)>\n', '<pre\\1>', html) - - odt = NamedTemporaryFile() - options = optparse.Values() - options.with_network = True - for k, v in (('input', ''), - ('output', odt.name), - ('template', settings.ODT_TEMPLATE), - ('with_network', True), - ('top_header_level', 1), - ('img_width', '8cm'), - ('img_height', '6cm'), - ('verbose', False), - ('replace_keyword', 'ODT-INSERT'), - ('cut_start', 'ODT-CUT-START'), - ('htmlid', None), - ('url', "#")): - setattr(options, k, v) - odtfile = xhtml2odt.ODTFile(options) - odtfile.open() - odtfile.import_xhtml(html) - odtfile = odtfile.save() - except xhtml2odt.ODTExportError: - return HttpResponse(content, content_type="application/xhtml") - response = HttpResponse( - content_type='application/vnd.oasis.opendocument.text') - response['Content-Disposition'] = 'attachment; filename=%s.odt' % \ - filename - response.write(odtfile) - return response - elif doc_type == 'pdf': - tpl = loader.get_template('ishtar/sheet_%s_pdf.html' % name) - context_instance['output'] = 'PDF' - html = tpl.render(context_instance, request) - font_config = FontConfiguration() - css = CSS(string=''' - @font-face { - font-family: Gentium; - src: url(%s); - } - body{ - font-family: Gentium - } - ''' % (static("gentium/GentiumPlus-R.ttf"))) - css2 = CSS(filename=settings.STATIC_ROOT + '/media/style_basic.css') - pdf = HTML(string=html, base_url=request.build_absolute_uri() - ).write_pdf(stylesheets=[css, css2], - font_config=font_config) - response = HttpResponse(pdf, content_type='application/pdf') - response['Content-Disposition'] = 'attachment; filename=%s.pdf' % \ - filename - return response - else: - tpl = loader.get_template('ishtar/sheet_%s_window.html' % name) - content = tpl.render(context_instance, request) - return HttpResponse(content, content_type="application/xhtml") - return func - - -def revert_item(model): - def func(request, pk, date, **dct): - try: - item = model.objects.get(pk=pk) - date = datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%f') - item.rollback(date) - except (ObjectDoesNotExist, ValueError, HistoryError): - return HttpResponse(None, content_type='text/plain') - return HttpResponse("True", content_type='text/plain') - return func - - def autocomplete_organization(request, orga_type=None): if (not request.user.has_perm('ishtar_common.view_organization', models.Organization) and @@ -1521,31 +631,6 @@ def autocomplete_author(request): return HttpResponse(data, content_type='text/plain') -def new_item(model, frm, many=False): - def func(request, parent_name, limits=''): - model_name = model._meta.object_name - if not check_permission(request, 'add_' + model_name.lower()): - not_permitted_msg = ugettext(u"Operation not permitted.") - return HttpResponse(not_permitted_msg) - dct = {'title': unicode(_(u'New %s' % model_name.lower())), - 'many': many} - if request.method == 'POST': - dct['form'] = frm(request.POST, limits=limits) - if dct['form'].is_valid(): - new_item = dct['form'].save(request.user) - dct['new_item_label'] = unicode(new_item) - dct['new_item_pk'] = new_item.pk - dct['parent_name'] = parent_name - dct['parent_pk'] = parent_name - if dct['parent_pk'] and '_select_' in dct['parent_pk']: - parents = dct['parent_pk'].split('_') - dct['parent_pk'] = "_".join([parents[0]] + parents[2:]) - return render(request, 'window.html', dct) - else: - dct['form'] = frm(limits=limits) - return render(request, 'window.html', dct) - return func - new_person = new_item(models.Person, forms.PersonForm) new_person_noorga = new_item(models.Person, forms.NoOrgaPersonForm) new_organization = new_item(models.Organization, forms.OrganizationForm) @@ -2488,6 +1573,7 @@ class OrganizationPersonEdit(LoginRequiredMixin, UpdateView): show_document = show_item(models.Document, 'document') get_document = get_item(models.Document, 'get_document', 'document') +display_document = display_item(models.Document) document_search_wizard = wizards.SearchWizard.as_view( @@ -2497,11 +1583,16 @@ document_search_wizard = wizards.SearchWizard.as_view( ) -class NewDocumentFormView(IshtarMixin, LoginRequiredMixin, - FormView): +class DocumentFormView(IshtarMixin, LoginRequiredMixin, + CreateView): + page_name = _(u"New Document") form_class = forms.DocumentForm template_name = 'ishtar/form.html' - success_url = 'document_search' + model = models.Document + + def get_success_url(self): + return reverse('display-document', args=[self.object.pk]) + """ class DocumentSelectMixin(IshtarMixin, LoginRequiredMixin, diff --git a/ishtar_common/views_item.py b/ishtar_common/views_item.py new file mode 100644 index 000000000..eef3440bc --- /dev/null +++ b/ishtar_common/views_item.py @@ -0,0 +1,936 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import csv +import datetime +import json +import logging +import optparse +import re +from copy import copy, deepcopy +from tempfile import NamedTemporaryFile + +from django.conf import settings +from django.contrib.postgres.search import SearchQuery +from django.contrib.staticfiles.templatetags.staticfiles import static +from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import reverse, NoReverseMatch +from django.db.models import Q, ImageField +from django.db.models.fields import FieldDoesNotExist +from django.http import HttpResponse +from django.shortcuts import render +from django.template import loader +from django.utils.translation import ugettext, ugettext_lazy as _ +from tidylib import tidy_document as tidy +from unidecode import unidecode +from weasyprint import HTML, CSS +from weasyprint.fonts import FontConfiguration +from xhtml2odt import xhtml2odt + +from ishtar_common.utils import check_model_access_control, CSV_OPTIONS, \ + get_all_field_names +from ishtar_common.models import HistoryError, get_current_profile, \ + PRIVATE_FIELDS +from menus import Menu + +import models +from archaeological_files.models import File +from archaeological_operations.models import Operation +from archaeological_context_records.models import ContextRecord +from archaeological_finds.models import Find, FindBasket, Treatment, \ + TreatmentFile + +logger = logging.getLogger(__name__) + +ENCODING = settings.ENCODING or 'utf-8' + +CURRENT_ITEM_KEYS = (('file', File), + ('operation', Operation), + ('contextrecord', ContextRecord), + ('find', Find), + ('treatmentfile', TreatmentFile), + ('treatment', Treatment)) +CURRENT_ITEM_KEYS_DICT = dict(CURRENT_ITEM_KEYS) + + +def check_permission(request, action_slug, obj_id=None): + MAIN_MENU = Menu(None) + MAIN_MENU.init() + if action_slug not in MAIN_MENU.items: + # TODO + return True + if obj_id: + return MAIN_MENU.items[action_slug].is_available( + request.user, obj_id, session=request.session) + return MAIN_MENU.items[action_slug].can_be_available( + request.user, session=request.session) + + +def new_item(model, frm, many=False): + def func(request, parent_name, limits=''): + model_name = model._meta.object_name + if not check_permission(request, 'add_' + model_name.lower()): + not_permitted_msg = ugettext(u"Operation not permitted.") + return HttpResponse(not_permitted_msg) + dct = {'title': unicode(_(u'New %s' % model_name.lower())), + 'many': many} + if request.method == 'POST': + dct['form'] = frm(request.POST, limits=limits) + if dct['form'].is_valid(): + new_item = dct['form'].save(request.user) + dct['new_item_label'] = unicode(new_item) + dct['new_item_pk'] = new_item.pk + dct['parent_name'] = parent_name + dct['parent_pk'] = parent_name + if dct['parent_pk'] and '_select_' in dct['parent_pk']: + parents = dct['parent_pk'].split('_') + dct['parent_pk'] = "_".join([parents[0]] + parents[2:]) + return render(request, 'window.html', dct) + else: + dct['form'] = frm(limits=limits) + return render(request, 'window.html', dct) + return func + + +def display_item(model, extra_dct=None, show_url=None): + def func(request, pk, **dct): + if show_url: + dct['show_url'] = "/{}{}/".format(show_url, pk) + else: + dct['show_url'] = "/show-{}/{}/".format(model.SLUG, pk) + return render(request, 'ishtar/display_item.html', dct) + return func + + +def show_item(model, name, extra_dct=None): + def func(request, pk, **dct): + allowed, own = check_model_access_control(request, model) + if not allowed: + return HttpResponse('', content_type="application/xhtml") + q = model.objects + if own: + query_own = model.get_query_owns(request.user) + if query_own: + q = q.filter(query_own) + try: + item = q.get(pk=pk) + except ObjectDoesNotExist: + return HttpResponse('NOK') + doc_type = 'type' in dct and dct.pop('type') + url_name = u"/".join(reverse('show-' + name, args=['0', ''] + ).split('/')[:-2]) + u"/" + dct['CURRENCY'] = get_current_profile().currency + dct['ENCODING'] = settings.ENCODING + dct['DOT_GENERATION'] = settings.DOT_BINARY and True + dct['current_window_url'] = url_name + date = None + if 'date' in dct: + date = dct.pop('date') + dct['sheet_id'] = "%s-%d" % (name, item.pk) + dct['window_id'] = "%s-%d-%s" % ( + name, item.pk, datetime.datetime.now().strftime('%M%s')) + if hasattr(item, 'history'): + if date: + try: + date = datetime.datetime.strptime(date, + '%Y-%m-%dT%H:%M:%S.%f') + item = item.get_previous(date=date) + assert item is not None + except (ValueError, AssertionError): + return HttpResponse(None, content_type='text/plain') + dct['previous'] = item._previous + dct['next'] = item._next + else: + historized = item.history.all() + if historized: + item.history_date = historized[0].history_date + if len(historized) > 1: + dct['previous'] = historized[1].history_date + dct['item'], dct['item_name'] = item, name + # add context + if extra_dct: + dct.update(extra_dct(request, item)) + context_instance = deepcopy(dct) + context_instance['output'] = 'html' + if hasattr(item, 'history_object'): + filename = item.history_object.associated_filename + else: + filename = item.associated_filename + if doc_type == "odt" and settings.ODT_TEMPLATE: + tpl = loader.get_template('ishtar/sheet_%s.html' % name) + context_instance['output'] = 'ODT' + content = tpl.render(context_instance, request) + try: + tidy_options = {'output-xhtml': 1, 'indent': 1, + 'tidy-mark': 0, 'doctype': 'auto', + 'add-xml-decl': 1, 'wrap': 1} + html, errors = tidy(content, options=tidy_options) + html = html.encode('utf-8').replace(" ", " ") + html = re.sub('<pre([^>]*)>\n', '<pre\\1>', html) + + odt = NamedTemporaryFile() + options = optparse.Values() + options.with_network = True + for k, v in (('input', ''), + ('output', odt.name), + ('template', settings.ODT_TEMPLATE), + ('with_network', True), + ('top_header_level', 1), + ('img_width', '8cm'), + ('img_height', '6cm'), + ('verbose', False), + ('replace_keyword', 'ODT-INSERT'), + ('cut_start', 'ODT-CUT-START'), + ('htmlid', None), + ('url', "#")): + setattr(options, k, v) + odtfile = xhtml2odt.ODTFile(options) + odtfile.open() + odtfile.import_xhtml(html) + odtfile = odtfile.save() + except xhtml2odt.ODTExportError: + return HttpResponse(content, content_type="application/xhtml") + response = HttpResponse( + content_type='application/vnd.oasis.opendocument.text') + response['Content-Disposition'] = 'attachment; filename=%s.odt' % \ + filename + response.write(odtfile) + return response + elif doc_type == 'pdf': + tpl = loader.get_template('ishtar/sheet_%s_pdf.html' % name) + context_instance['output'] = 'PDF' + html = tpl.render(context_instance, request) + font_config = FontConfiguration() + css = CSS(string=''' + @font-face { + font-family: Gentium; + src: url(%s); + } + body{ + font-family: Gentium + } + ''' % (static("gentium/GentiumPlus-R.ttf"))) + css2 = CSS(filename=settings.STATIC_ROOT + '/media/style_basic.css') + pdf = HTML(string=html, base_url=request.build_absolute_uri() + ).write_pdf(stylesheets=[css, css2], + font_config=font_config) + response = HttpResponse(pdf, content_type='application/pdf') + response['Content-Disposition'] = 'attachment; filename=%s.pdf' % \ + filename + return response + else: + tpl = loader.get_template('ishtar/sheet_%s_window.html' % name) + content = tpl.render(context_instance, request) + return HttpResponse(content, content_type="application/xhtml") + return func + + +def revert_item(model): + def func(request, pk, date, **dct): + try: + item = model.objects.get(pk=pk) + date = datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%f') + item.rollback(date) + except (ObjectDoesNotExist, ValueError, HistoryError): + return HttpResponse(None, content_type='text/plain') + return HttpResponse("True", content_type='text/plain') + return func + + +HIERARCHIC_LEVELS = 5 +HIERARCHIC_FIELDS = ['periods', 'period', 'unit', 'material_types', + 'material_type', 'conservatory_state', 'object_types'] + + +def _get_values(request, val): + if hasattr(val, 'all'): # manage related objects + vals = list(val.all()) + else: + vals = [val] + new_vals = [] + for v in vals: + if callable(v): + v = v() + if hasattr(v, 'url'): + v = request.is_secure() and \ + 'https' or 'http' + '://' + \ + request.get_host() + v.url + new_vals.append(v) + return new_vals + + +def _search_manage_search_vector(dct): + if 'search_vector' in dct: + dct['search_vector'] = SearchQuery( + unidecode(dct['search_vector']), + config=settings.ISHTAR_SEARCH_LANGUAGE + ) + return dct + + +def _format_val(val): + if val is None: + return u"" + if type(val) == bool: + if val: + return unicode(_(u"True")) + else: + return unicode(_(u"False")) + if type(val) == str: + val = val.decode('utf-8') + return unicode(val) + + +DEFAULT_ROW_NUMBER = 10 +# length is used by ajax DataTables requests +EXCLUDED_FIELDS = ['length'] + + +def get_item(model, func_name, default_name, extra_request_keys=[], + base_request=None, bool_fields=[], reversed_bool_fields=[], + dated_fields=[], associated_models=[], relative_session_names=[], + specific_perms=[], own_table_cols=None, relation_types_prefix={}, + do_not_deduplicate=False): + """ + Generic treatment of tables + + :param model: model used for query + :param func_name: name of the function (used for session storage) + :param default_name: key used for default search in session + :param extra_request_keys: default query limitation + :param base_request: + :param bool_fields: + :param reversed_bool_fields: + :param dated_fields: + :param associated_models: + :param relative_session_names: + :param specific_perms: + :param own_table_cols: + :param relation_types_prefix: + :param do_not_deduplicate: duplication of id can occurs on large queryset a + mecanism of deduplication is used. But duplicate ids can be normal (for + instance for record_relations view). + :return: + """ + def func(request, data_type='json', full=False, force_own=False, + col_names=None, **dct): + available_perms = [] + if specific_perms: + available_perms = specific_perms[:] + EMPTY = '' + if 'type' in dct: + data_type = dct.pop('type') + if not data_type: + EMPTY = '[]' + data_type = 'json' + + allowed, own = check_model_access_control(request, model, + available_perms) + if not allowed: + return HttpResponse(EMPTY, content_type='text/plain') + + if force_own: + own = True + if full == 'shortcut' and 'SHORTCUT_SEARCH' in request.session and \ + request.session['SHORTCUT_SEARCH'] == 'own': + own = True + + # get defaults from model + if not extra_request_keys and hasattr(model, 'EXTRA_REQUEST_KEYS'): + my_extra_request_keys = copy(model.EXTRA_REQUEST_KEYS) + else: + my_extra_request_keys = copy(extra_request_keys) + if base_request is None and hasattr(model, 'BASE_REQUEST'): + my_base_request = copy(model.BASE_REQUEST) + elif base_request is not None: + my_base_request = copy(base_request) + else: + my_base_request = {} + if not bool_fields and hasattr(model, 'BOOL_FIELDS'): + my_bool_fields = model.BOOL_FIELDS[:] + else: + my_bool_fields = bool_fields[:] + if not reversed_bool_fields and hasattr(model, 'REVERSED_BOOL_FIELDS'): + my_reversed_bool_fields = model.REVERSED_BOOL_FIELDS[:] + else: + my_reversed_bool_fields = reversed_bool_fields[:] + if not dated_fields and hasattr(model, 'DATED_FIELDS'): + my_dated_fields = model.DATED_FIELDS[:] + else: + my_dated_fields = dated_fields[:] + if not associated_models and hasattr(model, 'ASSOCIATED_MODELS'): + my_associated_models = model.ASSOCIATED_MODELS[:] + else: + my_associated_models = associated_models[:] + if not relative_session_names and hasattr(model, + 'RELATIVE_SESSION_NAMES'): + my_relative_session_names = model.RELATIVE_SESSION_NAMES[:] + else: + my_relative_session_names = relative_session_names[:] + if not relation_types_prefix and hasattr(model, + 'RELATION_TYPES_PREFIX'): + my_relation_types_prefix = copy(model.RELATION_TYPES_PREFIX) + else: + my_relation_types_prefix = copy(relation_types_prefix) + + fields = [model._meta.get_field(k) + for k in get_all_field_names(model)] + + request_keys = dict([ + (field.name, + field.name + (hasattr(field, 'rel') and field.rel and '__pk' + or '')) + for field in fields]) + for associated_model, key in my_associated_models: + if type(associated_model) in (str, unicode): + if associated_model not in globals(): + continue + associated_model = globals()[associated_model] + associated_fields = [ + associated_model._meta.get_field(k) + for k in get_all_field_names(associated_model)] + request_keys.update( + dict([(key + "__" + field.name, + key + "__" + field.name + + (hasattr(field, 'rel') and field.rel and '__pk' or '')) + for field in associated_fields])) + request_keys.update(my_extra_request_keys) + request_items = request.method == 'POST' and request.POST \ + or request.GET + + # pager + try: + row_nb = int(request_items.get('length')) + except (ValueError, TypeError): + row_nb = DEFAULT_ROW_NUMBER + dct_request_items = {} + + # filter requested fields + for k in request_items: + if k in EXCLUDED_FIELDS: + continue + key = k[:] + if key.startswith('searchprefix_'): + key = key[len('searchprefix_'):] + dct_request_items[key] = request_items[k] + request_items = dct_request_items + + dct = my_base_request + if full == 'shortcut': + dct['cached_label__icontains'] = request.GET.get('term', None) + and_reqs, or_reqs = [], [] + try: + old = 'old' in request_items and int(request_items['old']) + except ValueError: + return HttpResponse('[]', content_type='text/plain') + + # manage relations types + if 'relation_types' not in my_relation_types_prefix: + my_relation_types_prefix['relation_types'] = '' + relation_types = {} + for rtype_key in my_relation_types_prefix: + relation_types[my_relation_types_prefix[rtype_key]] = set() + for k in request_items: + if k.startswith(rtype_key): + relation_types[my_relation_types_prefix[rtype_key]].add( + request_items[k]) + continue + + for k in request_keys: + val = request_items.get(k) + if not val: + continue + req_keys = request_keys[k] + if type(req_keys) not in (list, tuple): + dct[req_keys] = val + continue + # multiple choice target + reqs = Q(**{req_keys[0]: val}) + for req_key in req_keys[1:]: + q = Q(**{req_key: val}) + reqs |= q + and_reqs.append(reqs) + + pinned_search = "" + if 'submited' not in request_items and full != 'shortcut': + # default search + # an item is selected in the default menu + if default_name in request.session and \ + request.session[default_name]: + value = request.session[default_name] + if 'basket-' in value: + try: + dct = {"basket__pk": + request.session[default_name].split('-')[-1]} + pinned_search = unicode(FindBasket.objects.get( + pk=dct["basket__pk"])) + except FindBasket.DoesNotExist: + pass + else: + try: + dct = {"pk": request.session[default_name]} + pinned_search = unicode(model._meta.verbose_name) \ + + u" - " + unicode( + model.objects.get(pk=dct["pk"])) + except model.DoesNotExist: + pass + elif dct == (my_base_request or {}): + # a parent item may be selected in the default menu + for name, key in my_relative_session_names: + if name in request.session and request.session[name] \ + and 'basket-' not in request.session[name] \ + and name in CURRENT_ITEM_KEYS_DICT: + up_model = CURRENT_ITEM_KEYS_DICT[name] + try: + dct.update({key: request.session[name]}) + pinned_search = unicode(up_model._meta.verbose_name) \ + + u" - " + unicode( + up_model.objects.get(pk=dct[key])) + break + except up_model.DoesNotExist: + pass + if (not dct or data_type == 'csv') \ + and func_name in request.session: + dct = request.session[func_name] + else: + request.session[func_name] = dct + for k in (list(my_bool_fields) + list(my_reversed_bool_fields)): + if k in dct: + if dct[k] == u"1": + dct.pop(k) + else: + dct[k] = dct[k] == u"2" and True or False + if k in my_reversed_bool_fields: + dct[k] = not dct[k] + # check also for empty value with image field + field_name = k.split('__')[0] + # TODO: can be improved in later version of Django + try: + c_field = model._meta.get_field(field_name) + if k.endswith('__isnull') and \ + isinstance(c_field, ImageField): + if dct[k]: + or_reqs.append( + (k, {k.split('__')[0] + '__exact': ''})) + else: + dct[k.split('__')[0] + '__regex'] = '.{1}.*' + except FieldDoesNotExist: + pass + for k in my_dated_fields: + if k in dct: + if not dct[k]: + dct.pop(k) + try: + items = dct[k].split('/') + assert len(items) == 3 + dct[k] = datetime.date(*map(lambda x: int(x), + reversed(items))) \ + .strftime('%Y-%m-%d') + except AssertionError: + dct.pop(k) + # manage hierarchic conditions + for req in dct.copy(): + if req.endswith('town__pk') or req.endswith('towns__pk'): + val = dct.pop(req) + reqs = Q(**{req: val}) + base_req = req[:-2] + '__' + req = base_req[:] + for idx in range(HIERARCHIC_LEVELS): + req = req[:-2] + 'parents__pk' + q = Q(**{req: val}) + reqs |= q + req = base_req[:] + for idx in range(HIERARCHIC_LEVELS): + req = req[:-2] + 'children__pk' + q = Q(**{req: val}) + reqs |= q + and_reqs.append(reqs) + continue + + for k_hr in HIERARCHIC_FIELDS: + if type(req) in (list, tuple): + val = dct.pop(req) + q = None + for idx, r in enumerate(req): + if not idx: + q = Q(**{r: val}) + else: + q |= Q(**{r: val}) + and_reqs.append(q) + break + elif req.endswith(k_hr + '__pk'): + val = dct.pop(req) + reqs = Q(**{req: val}) + req = req[:-2] + '__' + for idx in range(HIERARCHIC_LEVELS): + req = req[:-2] + 'parent__pk' + q = Q(**{req: val}) + reqs |= q + and_reqs.append(reqs) + break + dct = _search_manage_search_vector(dct) + query = Q(**dct) + for k, or_req in or_reqs: + alt_dct = dct.copy() + alt_dct.pop(k) + alt_dct.update(or_req) + query |= Q(**alt_dct) + + for rtype_prefix in relation_types: + vals = list(relation_types[rtype_prefix]) + if not vals: + continue + alt_dct = { + rtype_prefix + 'right_relations__relation_type__pk__in': vals} + for k in dct: + val = dct[k] + if rtype_prefix: + # only get conditions related to the object + if rtype_prefix not in k: + continue + # tricky: reconstruct the key to make sense - remove the + # prefix from the key + k = k[0:k.index(rtype_prefix)] + k[ + k.index(rtype_prefix) + len(rtype_prefix):] + if k.endswith('year'): + k += '__exact' + alt_dct[rtype_prefix + 'right_relations__right_record__' + k] = \ + val + if not dct: + # fake condition to trick Django (1.4): without it only the + # alt_dct is managed + query &= Q(pk__isnull=False) + query |= Q(**alt_dct) + for k, or_req in or_reqs: + altor_dct = alt_dct.copy() + altor_dct.pop(k) + for j in or_req: + val = or_req[j] + if j == 'year': + j = 'year__exact' + altor_dct[ + rtype_prefix + 'right_relations__right_record__' + j] = \ + val + query |= Q(**altor_dct) + + if own: + q = models.IshtarUser.objects.filter(user_ptr=request.user) + if q.count(): + query = query & model.get_query_owns(q.all()[0]) + else: + return HttpResponse(EMPTY, content_type='text/plain') + + for and_req in and_reqs: + query = query & and_req + + # manage hierarchic in shortcut menu + if full == 'shortcut': + ASSOCIATED_ITEMS = { + Operation: (File, 'associated_file__pk'), + ContextRecord: (Operation, 'operation__pk'), + Find: (ContextRecord, 'base_finds__context_record__pk'), + } + if model in ASSOCIATED_ITEMS: + upper_model, upper_key = ASSOCIATED_ITEMS[model] + model_name = upper_model.SLUG + current = model_name in request.session \ + and request.session[model_name] + if current: + dct = {upper_key: current} + query &= Q(**dct) + + items = model.objects.filter(query).distinct() + # print(items.query) + + if 'search_vector' in dct: # for serialization + dct['search_vector'] = dct['search_vector'].value + + # table cols + if own_table_cols: + table_cols = own_table_cols + else: + if full: + table_cols = [field.name for field in model._meta.fields + if field.name not in PRIVATE_FIELDS] + table_cols += [field.name for field in model._meta.many_to_many + if field.name not in PRIVATE_FIELDS] + if hasattr(model, 'EXTRA_FULL_FIELDS'): + table_cols += model.EXTRA_FULL_FIELDS + else: + table_cols = model.TABLE_COLS + query_table_cols = [] + for cols in table_cols: + if type(cols) not in (list, tuple): + cols = [cols] + for col in cols: + query_table_cols += col.split('|') + + # contextual (full, simple, etc.) col + contxt = full and 'full' or 'simple' + if hasattr(model, 'CONTEXTUAL_TABLE_COLS') and \ + contxt in model.CONTEXTUAL_TABLE_COLS: + for idx, col in enumerate(table_cols): + if col in model.CONTEXTUAL_TABLE_COLS[contxt]: + query_table_cols[idx] = \ + model.CONTEXTUAL_TABLE_COLS[contxt][col] + if full == 'shortcut': + query_table_cols = ['cached_label'] + table_cols = ['cached_label'] + + # manage sort tables + manual_sort_key = None + + sorts = {} + for k in request_items: + if not k.startswith('order['): + continue + num = int(k.split(']')[0][len("order["):]) + if num not in sorts: + sorts[num] = ['', ''] # sign, col_num + if k.endswith('[dir]'): + order = request_items[k] + sign = order and order == u'desc' and "-" or '' + sorts[num][0] = sign + if k.endswith('[column]'): + sorts[num][1] = request_items[k] + sign = "" + if not sorts and model._meta.ordering: + orders = [k for k in model._meta.ordering] + items = items.order_by(*orders) + else: + orders = [] + for idx in sorted(sorts.keys()): + signe, col_num = sorts[idx] + k = query_table_cols[int(col_num) - 2] # remove id and link col + if k in request_keys: + ks = request_keys[k] + if type(ks) not in (tuple, list): + ks = [ks] + for k in ks: + if k.endswith("__pk"): + k = k[:-len("__pk")] + "__label" + if '__' in k: + k = k.split('__')[0] + orders.append(signe + k) + else: + # not a standard request key + if idx: # not the first - we ignore this sort + continue + sign = signe + manual_sort_key = k + logger.warning( + "**WARN get_item - {}**: manual sort key '{}'".format( + func_name, k)) + break + if not manual_sort_key: + items = items.order_by(*orders) + + # pager management + start, end = 0, None + page_nb = 1 + if row_nb and data_type == "json": + try: + start = int(request_items.get('start')) + page_nb = start / row_nb + 1 + assert page_nb >= 1 + except (TypeError, ValueError, AssertionError): + start = 0 + page_nb = 1 + end = page_nb * row_nb + if full == 'shortcut': + start = 0 + end = 20 + + items_nb = items.count() + if manual_sort_key: + items = items.all() + else: + items = items[start:end] + + datas = [] + if old: + items = [item.get_previous(old) for item in items] + c_ids = [] + for item in items: + # manual deduplicate when distinct is not enough + if not do_not_deduplicate and item.pk in c_ids: + continue + c_ids.append(item.pk) + data = [item.pk] + for keys in query_table_cols: + if type(keys) not in (list, tuple): + keys = [keys] + my_vals = [] + for k in keys: + if hasattr(model, 'EXTRA_REQUEST_KEYS') \ + and k in model.EXTRA_REQUEST_KEYS: + k = model.EXTRA_REQUEST_KEYS[k] + if type(k) in (list, tuple): + k = k[0] + for filtr in ('__icontains', '__contains'): + if k.endswith(filtr): + k = k[:len(k) - len(filtr)] + vals = [item] + # foreign key may be divided by "." or "__" + splitted_k = [] + for ky in k.split('.'): + if '__' in ky: + splitted_k += ky.split('__') + else: + splitted_k.append(ky) + for ky in splitted_k: + new_vals = [] + for val in vals: + if hasattr(val, 'all'): # manage related objects + val = list(val.all()) + for v in val: + v = getattr(v, ky) + new_vals += _get_values(request, v) + elif val: + try: + val = getattr(val, ky) + new_vals += _get_values(request, val) + except AttributeError: + # must be a query key such as "contains" + pass + vals = new_vals + # manage last related objects + if vals and hasattr(vals[0], 'all'): + new_vals = [] + for val in vals: + new_vals += list(val.all()) + vals = new_vals + if not my_vals: + my_vals = [_format_val(va) for va in vals] + else: + new_vals = [] + if not vals: + for idx, my_v in enumerate(my_vals): + new_vals.append(u"{}{}{}".format( + my_v, u' - ', '')) + else: + for idx, v in enumerate(vals): + new_vals.append(u"{}{}{}".format( + vals[idx], u' - ', _format_val(v))) + my_vals = new_vals[:] + data.append(u" & ".join(my_vals) or u"") + datas.append(data) + if manual_sort_key: + # +1 because the id is added as a first col + idx_col = None + if manual_sort_key in query_table_cols: + idx_col = query_table_cols.index(manual_sort_key) + 1 + else: + for idx, col in enumerate(query_table_cols): + if type(col) in (list, tuple) and \ + manual_sort_key in col: + idx_col = idx + 1 + if idx_col is not None: + datas = sorted(datas, key=lambda x: x[idx_col]) + if sign == '-': + datas = reversed(datas) + datas = list(datas)[start:end] + link_template = "<a class='display_details' href='#' " \ + "onclick='load_window(\"%s\")'>" \ + "<i class=\"fa fa-info-circle\" aria-hidden=\"true\"></i></a>" + link_ext_template = '<a href="{}" target="_blank">{}</a>' + if data_type == "json": + rows = [] + for data in datas: + try: + lnk = link_template % reverse('show-' + default_name, + args=[data[0], '']) + except NoReverseMatch: + logger.warning( + '**WARN "show-' + default_name + '" args (' + + unicode(data[0]) + ") url not available") + lnk = '' + res = {'id': data[0], 'link': lnk} + for idx, value in enumerate(data[1:]): + if value: + table_col = table_cols[idx] + if type(table_col) not in (list, tuple): + table_col = [table_col] + tab_cols = [] + # foreign key may be divided by "." or "__" + for tc in table_col: + if '.' in tc: + tab_cols += tc.split('.') + elif '__' in tc: + tab_cols += tc.split('__') + else: + tab_cols.append(tc) + k = "__".join(tab_cols) + if hasattr(model, 'COL_LINK') and k in model.COL_LINK: + value = link_ext_template.format(value, value) + res[k] = value + if full == 'shortcut' and 'cached_label' in res: + res['value'] = res.pop('cached_label') + rows.append(res) + if full == 'shortcut': + data = json.dumps(rows) + else: + data = json.dumps({ + "recordsTotal": items_nb, + "recordsFiltered": items_nb, + "rows": rows, + "pinned-search": pinned_search, + "page": page_nb, + "total": (items_nb / row_nb + 1) if row_nb else items_nb, + }) + return HttpResponse(data, content_type='text/plain') + elif data_type == "csv": + response = HttpResponse(content_type='text/csv') + n = datetime.datetime.now() + filename = u'%s_%s.csv' % (default_name, + n.strftime('%Y%m%d-%H%M%S')) + response['Content-Disposition'] = 'attachment; filename=%s' \ + % filename + writer = csv.writer(response, **CSV_OPTIONS) + if col_names: + col_names = [name.encode(ENCODING, errors='replace') + for name in col_names] + else: + col_names = [] + for field_name in table_cols: + if type(field_name) in (list, tuple): + field_name = u" & ".join(field_name) + if hasattr(model, 'COL_LABELS') and \ + field_name in model.COL_LABELS: + field = model.COL_LABELS[field_name] + col_names.append(unicode(field).encode(ENCODING)) + continue + else: + try: + field = model._meta.get_field(field_name) + except: + col_names.append(u"".encode(ENCODING)) + logger.warning( + "**WARN get_item - csv export**: no col name " + "for {}\nadd explicit label to " + "COL_LABELS attribute of " + "{}".format(field_name, model)) + continue + col_names.append( + unicode(field.verbose_name).encode(ENCODING)) + writer.writerow(col_names) + for data in datas: + row, delta = [], 0 + # regroup cols with join "|" + for idx, col_name in enumerate(table_cols): + if len(data[1:]) <= idx + delta: + break + val = data[1:][idx + delta].encode( + ENCODING, errors='replace') + if col_name and "|" in col_name[0]: + for delta_idx in range( + len(col_name[0].split('|')) - 1): + delta += 1 + val += data[1:][idx + delta].encode( + ENCODING, errors='replace') + row.append(val) + writer.writerow(row) + return response + return HttpResponse('{}', content_type='text/plain') + + return func diff --git a/ishtar_common/widgets.py b/ishtar_common/widgets.py index a20d33fc3..6ec0220eb 100644 --- a/ishtar_common/widgets.py +++ b/ishtar_common/widgets.py @@ -458,6 +458,58 @@ class SearchWidget(forms.TextInput): template_name = 'widgets/search_input.html' +class ModelFieldMixin(object): + def to_python(self, value): + if not value: + return + if not self.multiple: + value = [value] + values = [] + for v in value: + if not v: + continue + try: + values.append(self.model.objects.get(pk=v)) + except self.model.DoesNotExist: + raise ValidationError( + unicode( + _(u"{} is not a valid key for {}") + ).format(v, self.model) + ) + if not self.multiple: + return values[0] + return values + + +class ModelChoiceField(ModelFieldMixin, forms.ChoiceField): + def __init__(self, model, multiple=False, *args, **kwargs): + self.model = model + self.multiple = multiple + super(ModelFieldMixin, self).__init__(*args, **kwargs) + + def valid_value(self, value): + if value and getattr(value, 'pk', None) in [v for v, l in self.choices]: + return True + return super(ModelChoiceField, self).valid_value(value) + + +class ModelJQueryAutocompleteField(ModelFieldMixin, forms.CharField): + def __init__(self, model, multiple=False, new=False, long_widget=False, + *args, **kwargs): + self.model = model + self.multiple = multiple + attrs = {} + if long_widget: + attrs['cols'] = True + attrs['full-width'] = True + kwargs['widget'] = JQueryAutoComplete( + reverse_lazy('autocomplete-' + self.model.SLUG), + associated_model=self.model, new=new, multiple=multiple, + attrs=attrs + ) + super(ModelJQueryAutocompleteField, self).__init__(*args, **kwargs) + + class JQueryAutoComplete(forms.TextInput): def __init__(self, source, associated_model=None, options=None, attrs=None, new=False, url_new='', multiple=False, limit=None, @@ -619,7 +671,7 @@ class JQueryAutoComplete(forms.TextInput): class JQueryTown(forms.TextInput): """ - Town fields whith state and department pre-selections + Town fields with state and department pre-selections """ def __init__(self, source, options={}, |