#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2010-2014 Étienne Loks # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # See the file COPYING for details. try: import tidy except: from tidylib import tidy_document as tidy import re import csv import json import datetime import optparse import cStringIO as StringIO from tempfile import NamedTemporaryFile import ho.pisa as pisa import unicodedata from extra_views import ModelFormSetView from django.conf import settings from django.contrib.auth import logout from django.contrib.auth.decorators import login_required from django.core import serializers from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse, NoReverseMatch from django.db.models import Q, F, ImageField from django.http import HttpResponse, Http404 from django.shortcuts import render_to_response, redirect from django.template import RequestContext, loader from django.utils.decorators import method_decorator from django.utils.translation import ugettext, ugettext_lazy as _ from xhtml2odt import xhtml2odt from menus import menu from archaeological_operations.forms import DashboardForm as DashboardFormOpe from ishtar_common.forms import FinalForm, FinalDeleteForm from ishtar_common import forms_common as forms from ishtar_common import wizards import models CSV_OPTIONS = {'delimiter':';', 'quotechar':'"', 'quoting':csv.QUOTE_ALL} ENCODING = settings.ENCODING or 'utf-8' def index(request): """ Main page """ dct = {} try: return render_to_response('index.html', dct, context_instance=RequestContext(request)) except NoReverseMatch: # probably rights exception (rights revoked) logout(request) return render_to_response('index.html', dct, context_instance=RequestContext(request)) person_creation_wizard = wizards.PersonWizard.as_view([ ('identity-person_creation', forms.SimplePersonForm), ('person_type-person_creation', forms.PersonTypeForm), ('final-person_creation', FinalForm)], label=_(u"New person"), url_name='person_creation') person_modification_wizard = wizards.PersonModifWizard.as_view([ ('selec-person_modification', forms.PersonFormSelection), ('identity-person_modification', forms.SimplePersonForm), ('person_type-person_creation', forms.PersonTypeForm), ('final-person_modification', FinalForm)], label=_(u"Person modification"), url_name='person_modification') person_deletion_wizard = wizards.PersonDeletionWizard.as_view([ ('selec-person_deletion', forms.PersonFormSelection), ('final-person_deletion', FinalDeleteForm)], label=_(u"Person deletion"), url_name='person_deletion',) organization_creation_wizard = wizards.OrganizationWizard.as_view([ ('identity-organization_creation', forms.OrganizationForm), ('final-organization_creation', FinalForm)], label=_(u"New organization"), url_name='organization_creation') organization_modification_wizard = wizards.OrganizationModifWizard.as_view([ ('selec-organization_modification', forms.OrganizationFormSelection), ('identity-organization_modification', forms.OrganizationForm), ('final-organization_modification', FinalForm)], label=_(u"Organization modification"), url_name='organization_modification') organization_deletion_wizard = wizards.OrganizationDeletionWizard.as_view([ ('selec-organization_deletion', forms.OrganizationFormSelection), ('final-organization_deletion', FinalDeleteForm)], label=_(u"Organization deletion"), url_name='organization_deletion',) account_management_wizard = wizards.AccountWizard.as_view([ ('selec-account_management', forms.PersonFormSelection), ('account-account_management', forms.AccountForm), ('final-account_management', forms.FinalAccountForm)], label=_(u"Account management"), url_name='account_management',) def update_current_item(request): if not request.is_ajax() and not request.method == 'POST': raise Http404 if 'value' in request.POST and 'item' in request.POST: request.session[request.POST['item']] = request.POST['value'] return HttpResponse('ok') def check_permission(request, action_slug, obj_id=None): if action_slug not in menu.items: #! TODO return True if obj_id: return menu.items[action_slug].is_available(request.user, obj_id) return menu.items[action_slug].can_be_available(request.user) def autocomplete_person(request, person_types=None, is_ishtar_user=None): if not request.user.has_perm('ishtar_common.view_person', models.Person) and \ not request.user.has_perm('ishtar_common.view_own_person', models.Person) \ and not request.user.ishtaruser.has_right('person_search'): return HttpResponse(mimetype='text/plain') if not request.GET.get('term'): return HttpResponse(mimetype='text/plain') q = request.GET.get('term') limit = request.GET.get('limit', 20) try: limit = int(limit) except ValueError: return HttpResponseBadRequest() query = Q() for q in q.split(' '): query = query & (Q(name__icontains=q) | Q(surname__icontains=q) | \ Q(email__icontains=q) | Q(attached_to__name__icontains=q)) if person_types and unicode(person_types) != '0': try: typs = [int(tp) for tp in person_types.split('_') if tp] typ = models.PersonType.objects.filter(pk__in=typs).all() query = query & Q(person_types__in=typ) except (ValueError, ObjectDoesNotExist): pass if is_ishtar_user: query = query & Q(ishtaruser__isnull=False) limit = 20 persons = models.Person.objects.filter(query)[:limit] data = json.dumps([{'id':person.pk, 'value':unicode(person)} for person in persons if person]) return HttpResponse(data, mimetype='text/plain') def autocomplete_department(request): if not request.GET.get('term'): return HttpResponse(mimetype='text/plain') q = request.GET.get('term') q = unicodedata.normalize("NFKD", q).encode('ascii','ignore') query = Q() for q in q.split(' '): extra = (Q(label__icontains=q) | Q(number__istartswith=q)) query = query & extra limit = 20 departments = models.Department.objects.filter(query)[:limit] data = json.dumps([{'id':department.pk, 'value':unicode(department)} for department in departments]) return HttpResponse(data, mimetype='text/plain') def autocomplete_town(request): if not request.GET.get('term'): return HttpResponse(mimetype='text/plain') q = request.GET.get('term') q = unicodedata.normalize("NFKD", q).encode('ascii','ignore') query = Q() for q in q.split(' '): extra = Q(name__icontains=q) if settings.COUNTRY == 'fr': extra = (extra | Q(numero_insee__istartswith=q) | \ Q(departement__label__istartswith=q)) query = query & extra limit = 20 towns = models.Town.objects.filter(query)[:limit] data = json.dumps([{'id':town.pk, 'value':unicode(town)} for town in towns]) return HttpResponse(data, mimetype='text/plain') from types import NoneType def format_val(val): if type(val) == NoneType: return u"" if type(val) == bool: if val: return unicode(_(u"True")) else: return unicode(_(u"False")) return unicode(val) HIERARCHIC_LEVELS = 5 HIERARCHIC_FIELDS = ['periods', 'period', 'unit', 'material_type', 'conservatory_state'] PRIVATE_FIELDS = ('id', 'history_modifier', 'order') def get_item(model, func_name, default_name, extra_request_keys=[], base_request={}, bool_fields=[], reversed_bool_fields=[], dated_fields=[], associated_models=[], relative_session_names={}, specific_perms=[]): """ Generic treatment of tables """ def func(request, data_type='json', full=False, **dct): # check rights own = True # more restrictive by default allowed = False for perm, lbl in model._meta.permissions: # if not specific any perm is relevant (read right) if specific_perms and perm not in specific_perms: continue if request.user.has_perm(model._meta.app_label + '.' + perm) \ or (request.user.is_authenticated() and request.user.ishtaruser.has_right(perm)): allowed = True if "_own_" not in perm: own = False break # max right reach EMPTY, mimetype = '', 'text/plain' if 'type' in dct: data_type = dct.pop('type') if data_type == 'csv': mimetype = 'text/csv' if not data_type: EMPTY = '[]' data_type = 'json' if not allowed: return HttpResponse(EMPTY, mimetype='text/plain') fields = [model._meta.get_field_by_name(k)[0] for k in model._meta.get_all_field_names()] 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 associated_models: associated_fields = [associated_model._meta.get_field_by_name(k)[0] for k in associated_model._meta.get_all_field_names()] 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(extra_request_keys) request_items = request.method == 'POST' and request.POST or request.GET dct = base_request.copy() and_reqs, or_reqs = [], [] try: old = 'old' in request_items and int(request_items['old']) except ValueError: return HttpResponse('[]', mimetype='text/plain') for k in request_keys: q = request_items.get(k) if not q: continue dct[request_keys[k]] = q if not dct and 'submited' not in request_items: if default_name in request.session and \ request.session[default_name]: dct = {"pk":request.session[default_name]} else: for name in relative_session_names.keys(): if name in request.session and request.session[name]: k = relative_session_names[name] dct = {k:request.session[name]} break 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(bool_fields) + list(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 reversed_bool_fields: dct[k] = not dct[k] # check also for empty value with image field c_field = model._meta.get_field(k.split('__')[0]) 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}.*' for k in 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(): 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 | 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 xrange(HIERARCHIC_LEVELS): req = req[:-2] + 'parent__pk' q = Q(**{req:val}) reqs = reqs | q and_reqs.append(reqs) break query = Q(**dct) if own: query = query & model.get_query_owns(request.user) for k, or_req in or_reqs: alt_dct = dct.copy() alt_dct.pop(k) alt_dct.update(or_req) query = query | Q(**alt_dct) for and_req in and_reqs: query = query & and_req items = model.objects.filter(query).distinct() q = request_items.get('sidx') # table cols table_cols = full and [field.name for field in model._meta.fields if field.name not in PRIVATE_FIELDS] \ or model.TABLE_COLS # manage sort tables manual_sort_key = None order = request_items.get('sord') sign = order and order == u'desc' and "-" or '' if q and q in request_keys: ks = request_keys[q] if type(ks) not in (list, tuple): ks = [ks] orders = [] for k in ks: if k.endswith("__pk"): k = k[:-len("__pk")] + "__label" if '__' in k: k = k.split('__')[0] orders.append(sign+k) items = items.order_by(*orders) elif q: for k in table_cols: if k.endswith(q): manual_sort_key = k break if not manual_sort_key and model._meta.ordering: orders = [sign+k for k in model._meta.ordering] items = items.order_by(*orders) # pager management start, end = 0, None page_nb = 1 try: row_nb = int(request_items.get('rows')) except (ValueError, TypeError): row_nb = None if row_nb: try: page_nb = int(request_items.get('page')) assert page_nb >= 1 except (ValueError, AssertionError): pass start = (page_nb-1)*row_nb end = page_nb*row_nb 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] for item in items: data = [item.pk] for k in table_cols: vals = [item] for ky in k.split('.'): new_vals = [] for val in vals: if hasattr(val, 'all'): # manage related objects val = list(val.all()) for v in val: new_vals.append(getattr(v, ky)) elif val: new_vals.append(getattr(val, ky)) vals = new_vals if vals and hasattr(vals[0], 'all'): # manage last related objects new_vals = [] for val in vals: new_vals += list(val.all()) vals = new_vals data.append(", ".join([format_val(v) for v in vals]) or u"") datas.append(data) if manual_sort_key: # +1 because the id is added as a first col idx_col = table_cols.index(manual_sort_key) + 1 datas = sorted(datas, key=lambda x:x[idx_col]) if sign == '-': datas = reversed(datas) datas = list(datas)[start:end] link_template = "%s" % \ (unicode(_("Details"))) if data_type == "json": rows = [] for data in datas: try: lnk = link_template % reverse('show-'+default_name, args=[data[0], '']) except NoReverseMatch: print '"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: res[table_cols[idx].split('.')[-1]] = value rows.append(res) data = json.dumps({ "records":items_nb, "rows":rows, "page":page_nb, "total":(items_nb/row_nb + 1) if row_nb else items_nb, }) return HttpResponse(data, mimetype='text/plain') elif data_type == "csv": response = HttpResponse(mimetype='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) col_names = [] for field_name in table_cols: try: field = model._meta.get_field(field_name) except: col_names.append(u"".encode(ENCODING)) continue col_names.append(unicode(field.verbose_name).encode(ENCODING)) writer.writerow(col_names) for data in datas: writer.writerow([val.encode(ENCODING) for val in data[1:]]) return response return HttpResponse('{}', mimetype='text/plain') return func def show_item(model, name): def func(request, pk, **dct): try: item = model.objects.get(pk=pk) except ObjectDoesNotExist: return HttpResponse(None) doc_type = 'type' in dct and dct.pop('type') url_name = u"/".join(reverse('show-'+name, args=['0', ''] ).split('/')[:-2]) + u"/" dct['current_window_url'] = url_name date = 'date' in dct and dct.pop('date') 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 != None except (ValueError, AssertionError): return HttpResponse(None, mimetype='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 context_instance = RequestContext(request) context_instance.update(dct) n = datetime.datetime.now() filename = "" if hasattr(item, 'history_object'): filename = item.history_object.associated_filename else: filename = item.associated_filename if doc_type == "odt" and settings.XHTML2ODT_PATH and \ settings.ODT_TEMPLATE: tpl = loader.get_template('ishtar/sheet_%s.html' % name) content = tpl.render(context_instance) try: tidy_options = dict(output_xhtml=1, add_xml_decl=1, indent=1, tidy_mark=0, output_encoding='utf8', doctype='auto', wrap=0, char_encoding='utf8') html = str(tidy.parseString(content.encode('utf-8'), **tidy_options)) html = html.replace(" ", " ") html = re.sub(']*)>\n', '', 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, ex: return HttpResponse(content, content_type="application/xhtml") response = HttpResponse( mimetype='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) content = tpl.render(context_instance) result = StringIO.StringIO() html = content.encode('utf-8') html = html.replace(" 1: context['previous_page'] = page - 1 item_nb = page*ITEM_PER_PAGE item_nb_1 = item_nb + ITEM_PER_PAGE from_key = 'from_' + key to_key = 'to_' + key queryset = q.all().order_by(from_key + '__name')[item_nb:item_nb_1] FormSet.from_key = from_key FormSet.to_key = to_key if request.method == 'POST': context['formset'] = FormSet(request.POST, queryset=queryset) if context['formset'].is_valid(): context['formset'].merge() return redirect(reverse(current_url, kwargs={'page':page})) else: context['formset'] = FormSet(queryset=queryset) return render_to_response('ishtar/merge_'+key+'.html', context, context_instance=RequestContext(request)) return merge person_merge = merge_action(models.Person, forms.MergePersonForm, 'person') organization_merge = merge_action(models.Organization, forms.MergeOrganizationForm, 'organization') class LoginRequiredMixin(object): @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs) if kwargs.get('pk') and not self.request.user.is_staff and \ not str(kwargs['pk']) == str(self.request.user.company.pk): return redirect(reverse('index')) return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs) class AdminLoginRequiredMixin(LoginRequiredMixin): def dispatch(self, request, *args, **kwargs): if not request.user.is_staff: return redirect(reverse('start')) return super(AdminLoginRequiredMixin, self).dispatch(request, *args, **kwargs) class GlobalVarEdit(AdminLoginRequiredMixin, ModelFormSetView): template_name = 'ishtar/formset.html' model = models.GlobalVar extra = 1 can_delete = True fields = ['slug', 'value', 'description']