summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
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
commit52f6b37f1a1deac66f0b84c466be6c8dab277514 (patch)
tree2e9d8c696298f89e33e713d4eaf2a4c1c48af3b5
parente7418c19b122c5ac0505ad2be5350068d3bf6f6b (diff)
downloadIshtar-52f6b37f1a1deac66f0b84c466be6c8dab277514.tar.bz2
Ishtar-52f6b37f1a1deac66f0b84c466be6c8dab277514.zip
Document form - refactoring (refs #4107)
-rw-r--r--archaeological_context_records/views.py5
-rw-r--r--archaeological_files/views.py2
-rw-r--r--archaeological_finds/views.py6
-rw-r--r--archaeological_operations/views.py2
-rw-r--r--archaeological_warehouse/views.py2
-rw-r--r--ishtar_common/context_processors.py2
-rw-r--r--ishtar_common/forms_common.py35
-rw-r--r--ishtar_common/models.py61
-rw-r--r--ishtar_common/templates/ishtar/sheet_document.html4
-rw-r--r--ishtar_common/templatetags/window_field.py24
-rw-r--r--ishtar_common/urls.py8
-rw-r--r--ishtar_common/utils.py46
-rw-r--r--ishtar_common/views.py1003
-rw-r--r--ishtar_common/views_item.py936
-rw-r--r--ishtar_common/widgets.py54
15 files changed, 1166 insertions, 1024 deletions
diff --git a/archaeological_context_records/views.py b/archaeological_context_records/views.py
index 787b76b31..3fd393a4d 100644
--- a/archaeological_context_records/views.py
+++ b/archaeological_context_records/views.py
@@ -30,8 +30,9 @@ import models
from archaeological_operations.views import site_extra_context
from forms import *
from ishtar_common.utils import put_session_message
-from ishtar_common.views import get_item, show_item, revert_item, \
- IshtarMixin, LoginRequiredMixin, display_item
+from ishtar_common.views import IshtarMixin, LoginRequiredMixin
+from ishtar_common.views_item import display_item, get_item, show_item, \
+ revert_item
from ishtar_common.wizards import SearchWizard
from wizards import *
diff --git a/archaeological_files/views.py b/archaeological_files/views.py
index 86fa83ee4..1ca8d41ac 100644
--- a/archaeological_files/views.py
+++ b/archaeological_files/views.py
@@ -26,7 +26,7 @@ from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.utils.translation import ugettext_lazy as _
-from ishtar_common.views import get_item, show_item, revert_item
+from ishtar_common.views_item import get_item, show_item, revert_item
from archaeological_operations.models import Operation
import models
diff --git a/archaeological_finds/views.py b/archaeological_finds/views.py
index 2dc599807..850151578 100644
--- a/archaeological_finds/views.py
+++ b/archaeological_finds/views.py
@@ -34,8 +34,10 @@ from archaeological_operations.wizards import AdministrativeActDeletionWizard
from forms import *
from ishtar_common.forms import FinalForm
from ishtar_common.models import IshtarUser, get_current_profile
-from ishtar_common.views import get_item, show_item, display_item, \
- revert_item, get_autocomplete_generic, IshtarMixin, LoginRequiredMixin
+from ishtar_common.views import get_autocomplete_generic, IshtarMixin, \
+ LoginRequiredMixin
+from ishtar_common.views_item import display_item, get_item, show_item, \
+ revert_item
from ishtar_common.wizards import SearchWizard
from wizards import *
diff --git a/archaeological_operations/views.py b/archaeological_operations/views.py
index 5c5a3e89b..165dfd635 100644
--- a/archaeological_operations/views.py
+++ b/archaeological_operations/views.py
@@ -50,7 +50,7 @@ from ishtar_common.forms import ClosingDateFormSelection, FinalForm, \
FinalDeleteForm
from ishtar_common.models import get_current_profile, IshtarSiteProfile
from ishtar_common.utils import put_session_message, check_rights_condition
-from ishtar_common.views import get_item, show_item, revert_item, new_item
+from ishtar_common.views_item import get_item, show_item, revert_item, new_item
from ishtar_common.wizards import SearchWizard
diff --git a/archaeological_warehouse/views.py b/archaeological_warehouse/views.py
index c801fbae6..ec160cbce 100644
--- a/archaeological_warehouse/views.py
+++ b/archaeological_warehouse/views.py
@@ -26,7 +26,7 @@ from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from forms import *
-from ishtar_common.views import get_item, new_item, show_item
+from ishtar_common.views_item import get_item, show_item, new_item
from ishtar_common.wizards import SearchWizard
from wizards import *
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("&nbsp;", "&#160;")
- 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("&nbsp;", "&#160;")
+ 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={},