#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2010-2017 É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. import csv import datetime import json import logging import unicodedata import unicodecsv from django.conf import settings from django.contrib.auth import logout from django.contrib.auth.decorators import login_required from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse, NoReverseMatch 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.utils.decorators import method_decorator from django.utils.translation import ugettext, ugettext_lazy as _ from django.views.generic import ListView, UpdateView, TemplateView from django.views.generic.edit import CreateView, DeleteView, FormView, \ UpdateView from extra_views import ModelFormSetView from markdown import markdown import models from archaeological_context_records.models import ContextRecord from archaeological_files.forms import DashboardForm as DashboardFormFile from archaeological_files.models import File from archaeological_finds.forms import DashboardTreatmentForm, \ DashboardTreatmentFileForm 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.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 from views_item import CURRENT_ITEM_KEYS, check_permission, display_item, \ get_item, new_item, show_item logger = logging.getLogger(__name__) def status(request): return HttpResponse('OK') def index(request): """ Main page """ dct = {'warnings': []} if settings.PROJECT_SLUG == 'default': dct['warnings'].append(_( u"PROJECT_SLUG is set to \"default\". Change it in your " u"local_settings (or ask your admin to do it).")) profile = get_current_profile() if profile.slug == 'default': dct['warnings'].append(_( u"The slug of your current profile is set to \"default\". Change it " u"on the administration page (or ask your admin to do it).")) image = get_random_item_image_link(request) if hasattr(profile, 'homepage') and profile.homepage: dct['homepage'] = markdown(profile.homepage) if '{random_image}' in dct['homepage']: dct['homepage'] = dct['homepage'].replace( '{random_image}', image) else: dct['random_image'] = image try: return render(request, 'index.html', dct) except NoReverseMatch: # probably rights exception (rights revoked) logout(request) return render(request, 'index.html', dct) person_search_wizard = wizards.SearchWizard.as_view( [('general-person_search', forms.PersonFormSelection)], label=_(u"Person search"), url_name='person_search',) 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') def person_modify(request, pk): person_modification_wizard(request) wizards.PersonModifWizard.session_set_value( request, 'selec-person_modification', 'pk', pk, reset=True) return redirect(reverse('person_modification', kwargs={'step': 'identity-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_search_wizard = wizards.SearchWizard.as_view( [('general-organization_search', forms.OrganizationFormSelection)], label=_(u"Organization search"), url_name='organization_search',) 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') def organization_modify(request, pk): organization_modification_wizard(request) wizards.OrganizationModifWizard.session_set_value( request, 'selec-organization_modification', 'pk', pk, reset=True) return redirect( reverse('organization_modification', kwargs={'step': 'identity-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_wizard_steps = [ ('selec-account_management', forms.PersonUserFormSelection), ('account-account_management', forms.AccountForm), ('profile-account_management', forms.ProfileFormset), ('final-account_management', forms.FinalAccountForm)] account_management_wizard = wizards.AccountWizard.as_view( account_wizard_steps, label=_(u"Account management"), url_name='account_management',) account_deletion_wizard = wizards.IshtarUserDeletionWizard.as_view( [('selec-account_deletion', forms.AccountFormSelection), ('final-account_deletion', FinalDeleteForm)], label=_(u"Account deletion"), url_name='account_deletion',) def get_autocomplete_generic(model, extra=None): if not extra: extra = {'available': True} def func(request): q = request.GET.get('term') query = Q(**extra) for q in q.split(' '): if not q: continue query = query & Q(label__icontains=q) limit = 20 objects = model.objects.filter(query)[:limit] get_label = lambda x: x.full_label() if hasattr(x, 'full_label') \ else unicode(x) data = json.dumps([{'id': obj.pk, 'value': get_label(obj)} for obj in objects]) return HttpResponse(data, content_type='text/plain') return func def hide_shortcut_menu(request): request.session['SHORTCUT_SHOW'] = 'off' return HttpResponse('OK', content_type='text/plain') def show_shortcut_menu(request): request.session['SHORTCUT_SHOW'] = 'on' return HttpResponse('OK', content_type='text/plain') def activate_all_search(request): request.session['SHORTCUT_SEARCH'] = 'all' return HttpResponse('OK', content_type='text/plain') def activate_own_search(request): request.session['SHORTCUT_SEARCH'] = 'own' return HttpResponse('OK', content_type='text/plain') def activate_advanced_shortcut_menu(request): if not hasattr(request.user, 'ishtaruser'): return HttpResponse('KO', content_type='text/plain') request.user.ishtaruser.advanced_shortcut_menu = True request.user.ishtaruser.save() return HttpResponse('OK', content_type='text/plain') def activate_simple_shortcut_menu(request): if not hasattr(request.user, 'ishtaruser'): return HttpResponse('KO', content_type='text/plain') request.user.ishtaruser.advanced_shortcut_menu = False request.user.ishtaruser.save() return HttpResponse('OK', content_type='text/plain') def shortcut_menu(request): profile = get_current_profile() CURRENT_ITEMS = [] if profile.files: CURRENT_ITEMS.append((_(u"Archaeological file"), File)) CURRENT_ITEMS.append((_(u"Operation"), Operation)) if profile.context_record: CURRENT_ITEMS.append((_(u"Context record"), ContextRecord)) if profile.find: CURRENT_ITEMS.append((_(u"Find"), Find)) if profile.warehouse: CURRENT_ITEMS.append((_(u"Treatment request"), TreatmentFile)) CURRENT_ITEMS.append((_(u"Treatment"), Treatment)) if hasattr(request.user, 'ishtaruser') and \ request.user.ishtaruser.advanced_shortcut_menu: dct = { 'current_menu': [], 'menu': [], 'SHORTCUT_SEARCH': request.session['SHORTCUT_SEARCH'] if 'SHORTCUT_SEARCH' in request.session else 'own', 'SHORTCUT_SHOW': request.session['SHORTCUT_SHOW'] if 'SHORTCUT_SHOW' in request.session else 'on' } current_selected_labels = [] for lbl, model in CURRENT_ITEMS: model_name = model.SLUG current = model_name in request.session \ and request.session[model_name] if current: try: item = model.objects.get(pk=int(current)) item_label = shortify(unicode(item), 60) current_selected_labels.append(item_label) except model.DoesNotExist: pass dct['menu'].append(( lbl, model_name, current or 0, JQueryAutoComplete( reverse('get-' + model.SLUG + '-shortcut'), model).render( model.SLUG + '-shortcut', value=current, attrs={'id': 'current_' + model.SLUG}))) dct['current_selected_labels'] = current_selected_labels return render( request, 'ishtar/blocks/advanced_shortcut_menu.html', dct ) dct = { 'current_menu': [], 'SHORTCUT_SHOW': request.session['SHORTCUT_SHOW'] if 'SHORTCUT_SHOW' in request.session else 'off' } current_selected_labels = [] current_selected_item = {} labels = {} for lbl, model in CURRENT_ITEMS: new_selected_item = None model_name = model.SLUG cls = '' current = model_name in request.session and request.session[model_name] items = [] current_items = [] labels[model_name] = {} for item, shortmenu_class in model.get_owns( request.user, menu_filtr=current_selected_item, limit=100, values=['id', 'cached_label'], get_short_menu_class=True): pk = unicode(item['id']) if shortmenu_class == 'basket': pk = "basket-" + pk # prevent duplicates if pk in current_items: continue current_items.append(pk) selected = pk == current item_label = shortify(item['cached_label'], 60) if selected: cls = shortmenu_class new_selected_item = pk labels[model_name][str(pk)] = item_label items.append((pk, item_label, selected, shortmenu_class)) # selected is not in owns - add it to the list if not new_selected_item and current: try: item = model.objects.get(pk=int(current)) new_selected_item = item.pk item_label = shortify(unicode(item), 60) labels[model_name][str(item.pk)] = item_label items.append((item.pk, item_label, True, item.get_short_menu_class(item.pk))) except (model.DoesNotExist, ValueError): pass if items: dct['current_menu'].append((lbl, model_name, cls, items)) if new_selected_item: current_selected_item[model_name] = new_selected_item if str(new_selected_item) in labels[model_name]: current_selected_labels.append( labels[model_name][str(new_selected_item)]) dct['current_selected_labels'] = current_selected_labels return render(request, 'ishtar/blocks/shortcut_menu.html', dct) def get_current_items(request): currents = {} for key, model in CURRENT_ITEM_KEYS: currents[key] = None if key in request.session and request.session[key]: try: currents[key] = model.objects.get(pk=int(request.session[key])) except (ValueError, model.DoesNotExist): continue return currents def unpin(request, item_type): if item_type not in ('find', 'contextrecord', 'operation', 'file', 'treatment', 'treatmentfile'): logger.warning("unpin unknow type: {}".format(item_type)) return HttpResponse('nok') request.session['treatment'] = '' if item_type == 'treatment': return HttpResponse('ok') request.session['treatmentfile'] = '' if item_type == 'treatmentfile': return HttpResponse('ok') request.session['find'] = '' if item_type == 'find': return HttpResponse('ok') request.session['contextrecord'] = '' if item_type == 'contextrecord': return HttpResponse('ok') request.session['operation'] = '' if item_type == 'operation': return HttpResponse('ok') request.session['file'] = '' if item_type == 'file': return HttpResponse('ok') def update_current_item(request, item_type=None, pk=None): if not item_type or not pk: if not request.is_ajax() and not request.method == 'POST': raise Http404 item_type = request.POST['item'] if 'value' in request.POST and 'item' in request.POST: request.session[item_type] = request.POST['value'] else: request.session[item_type] = str(pk) request.session['SHORTCUT_SEARCH'] = 'all' currents = get_current_items(request) # re-init when descending item are not relevant if item_type == 'file' and currents['file'] and currents['operation'] and \ currents['operation'].associated_file != currents['file']: request.session["operation"] = '' currents['operation'] = None if item_type in ('operation', 'file') and currents['contextrecord'] and \ (not request.session.get("operation", None) or currents['contextrecord'].operation != currents['operation']): request.session["contextrecord"] = '' currents['contextrecord'] = None from archaeological_finds.models import Find if item_type in ('contextrecord', 'operation', 'file') and \ currents['find'] and \ (not request.session.get("contextrecord", None) or not Find.objects.filter( downstream_treatment__isnull=True, base_finds__context_record__pk=request.session["contextrecord"], pk=currents['find'].pk).count()): request.session["find"] = '' currents['find'] = None # re-init ascending item with relevant values if item_type == "find" and currents['find']: from archaeological_context_records.models import ContextRecord q = ContextRecord.objects.filter( base_finds__find__pk=currents['find'].pk) if q.count(): currents['contextrecord'] = q.all()[0] request.session['contextrecord'] = str( currents['contextrecord'].pk) if item_type in ("find", 'contextrecord') and currents['contextrecord']: currents['operation'] = currents['contextrecord'].operation request.session['operation'] = str(currents['operation'].pk) if item_type in ("find", 'contextrecord', 'operation') and \ currents['operation']: currents['file'] = currents['operation'].associated_file request.session['file'] = str(currents['file'].pk) if currents['file'] \ else None return HttpResponse('ok') 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, attached_to=None, is_ishtar_user=None): return autocomplete_person( request, person_types=person_types, attached_to=attached_to, is_ishtar_user=is_ishtar_user, permissive=True) def autocomplete_person(request, person_types=None, attached_to=None, is_ishtar_user=None, permissive=False): all_items = request.user.has_perm('ishtar_common.view_person', models.Person) own_items = False if not all_items: own_items = request.user.has_perm('ishtar_common.view_own_person', models.Person) if not all_items and not own_items or not request.GET.get('term'): return HttpResponse('[]', content_type='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(' '): qu = (Q(name__icontains=q) | Q(surname__icontains=q) | Q(email__icontains=q) | Q(attached_to__name__icontains=q)) if permissive: qu = qu | Q(raw_name__icontains=q) query = query & qu if attached_to: query = query & Q(attached_to__pk__in=attached_to.split('_')) 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) if own_items: query &= models.Person.get_query_owns(request.user) 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, content_type='text/plain') def autocomplete_department(request): if not request.GET.get('term'): return HttpResponse('[]', content_type='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, content_type='text/plain') def autocomplete_town(request): if not request.GET.get('term'): return HttpResponse(content_type='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 |= Q(numero_insee__istartswith=q) 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, content_type='text/plain') def autocomplete_advanced_town(request, department_id=None, state_id=None): if not request.GET.get('term'): return HttpResponse(content_type='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) if not department_id: extra = extra | Q(departement__label__istartswith=q) query = query & extra if department_id: query = query & Q(departement__number__iexact=department_id) if state_id: query = query & Q(departement__state__number__iexact=state_id) limit = 20 towns = models.Town.objects.filter(query)[:limit] result = [] for town in towns: val = town.name if hasattr(town, 'numero_insee'): val += " (%s)" % town.numero_insee result.append({'id': town.pk, 'value': val}) data = json.dumps(result) return HttpResponse(data, content_type='text/plain') def department_by_state(request, state_id=''): if not state_id: data = [] else: departments = models.Department.objects.filter(state__number=state_id) data = json.dumps([{'id': department.pk, 'number': department.number, 'value': unicode(department)} for department in departments]) return HttpResponse(data, content_type='text/plain') def autocomplete_organization(request, orga_type=None): if (not request.user.has_perm('ishtar_common.view_organization', models.Organization) and not request.user.has_perm('ishtar_common.view_own_organization', models.Organization) and not request.user.ishtaruser.has_right( 'person_search', session=request.session)): return HttpResponse('[]', content_type='text/plain') if not request.GET.get('term'): return HttpResponse('[]', content_type='text/plain') q = request.GET.get('term') query = Q() for q in q.split(' '): extra = Q(name__icontains=q) query = query & extra if orga_type: try: typs = [int(tp) for tp in orga_type.split('_') if tp] typ = models.OrganizationType.objects.filter(pk__in=typs).all() query = query & Q(organization_type__in=typ) except (ValueError, ObjectDoesNotExist): pass limit = 15 organizations = models.Organization.objects.filter(query)[:limit] data = json.dumps([{'id': org.pk, 'value': unicode(org)} for org in organizations]) return HttpResponse(data, content_type='text/plain') def autocomplete_author(request): if not request.user.has_perm('ishtar_common.view_author', models.Author)\ and not request.user.has_perm('ishtar_common.view_own_author', models.Author): return HttpResponse('[]', content_type='text/plain') if not request.GET.get('term'): return HttpResponse('[]', content_type='text/plain') q = request.GET.get('term') query = Q() for q in q.split(' '): extra = Q(person__name__icontains=q) | \ Q(person__surname__icontains=q) | \ Q(person__email__icontains=q) | \ Q(author_type__label__icontains=q) query = query & extra limit = 15 authors = models.Author.objects.filter(query)[:limit] data = json.dumps([{'id': author.pk, 'value': unicode(author)} for author in authors]) return HttpResponse(data, content_type='text/plain') 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) show_organization = show_item(models.Organization, 'organization') get_organization = get_item(models.Organization, 'get_organization', 'organization') new_author = new_item(models.Author, forms.AuthorForm) show_person = show_item(models.Person, 'person') get_person = get_item(models.Person, 'get_person', 'person') get_person_for_account = get_item( models.Person, 'get_person', 'person', own_table_cols=models.Person.TABLE_COLS_ACCOUNT) get_ishtaruser = get_item(models.IshtarUser, 'get_ishtaruser', 'ishtaruser') def action(request, action_slug, obj_id=None, *args, **kwargs): """ Action management """ if not check_permission(request, action_slug, obj_id): not_permitted_msg = ugettext(u"Operation not permitted.") return HttpResponse(not_permitted_msg) request.session['CURRENT_ACTION'] = action_slug dct = {} globals_dct = globals() if action_slug in globals_dct: return globals_dct[action_slug](request, dct, obj_id, *args, **kwargs) return render(request, 'index.html', dct) def dashboard_main(request, dct, obj_id=None, *args, **kwargs): """ Main dashboard """ app_list = [] profile = models.get_current_profile() if profile.files: app_list.append((_(u"Archaeological files"), 'files')) app_list.append((_(u"Operations"), 'operations')) if profile.context_record: app_list.append((_(u"Context records"), 'contextrecords')) if profile.find: app_list.append((_(u"Finds"), 'finds')) if profile.warehouse: app_list.append((_(u"Treatment requests"), 'treatmentfiles')) app_list.append((_(u"Treatments"), 'treatments')) dct = {'app_list': app_list} return render(request, 'ishtar/dashboards/dashboard_main.html', dct) DASHBOARD_FORMS = { 'files': DashboardFormFile, 'operations': DashboardFormOpe, 'treatments': DashboardTreatmentForm, 'treatmentfiles': DashboardTreatmentFileForm } def dashboard_main_detail(request, item_name): """ Specific tab of the main dashboard """ if item_name == 'users': dct = {'ishtar_users': models.UserDashboard()} return render( request, 'ishtar/dashboards/dashboard_main_detail_users.html', dct) form = None slicing, date_source, fltr, show_detail = 'year', None, {}, False profile = models.get_current_profile() has_form = (item_name == 'files' and profile.files) \ or item_name == 'operations' \ or (item_name in ('treatmentfiles', 'treatments') and profile.warehouse) if has_form: slicing = 'month' if item_name in DASHBOARD_FORMS: if request.method == 'POST': form = DASHBOARD_FORMS[item_name](request.POST) if form.is_valid(): slicing = form.cleaned_data['slicing'] fltr = form.get_filter() if hasattr(form, 'get_date_source'): date_source = form.get_date_source() if hasattr(form, 'get_show_detail'): show_detail = form.get_show_detail() else: form = DASHBOARD_FORMS[item_name]() lbl, dashboard = None, None dashboard_kwargs = {} if has_form: dashboard_kwargs = {'slice': slicing, 'fltr': fltr, 'show_detail': show_detail} # date_source is only relevant when the form has set one if date_source: dashboard_kwargs['date_source'] = date_source if item_name == 'files' and profile.files: lbl, dashboard = (_(u"Archaeological files"), models.Dashboard(File, **dashboard_kwargs)) elif item_name == 'operations': from archaeological_operations.models import Operation lbl, dashboard = (_(u"Operations"), models.Dashboard(Operation, **dashboard_kwargs)) elif item_name == 'contextrecords' and profile.context_record: lbl, dashboard = ( _(u"Context records"), models.Dashboard(ContextRecord, slice=slicing, fltr=fltr)) elif item_name == 'finds' and profile.find: lbl, dashboard = (_(u"Finds"), models.Dashboard(Find, slice=slicing, fltr=fltr)) elif item_name == 'treatmentfiles' and profile.warehouse: lbl, dashboard = ( _(u"Treatment requests"), models.Dashboard(TreatmentFile, **dashboard_kwargs)) elif item_name == 'treatments' and profile.warehouse: if 'date_source' not in dashboard_kwargs: dashboard_kwargs['date_source'] = 'start' lbl, dashboard = ( _(u"Treatments"), models.Dashboard(Treatment, **dashboard_kwargs)) if not lbl: raise Http404 dct = {'lbl': lbl, 'dashboard': dashboard, 'item_name': item_name.replace('-', '_'), 'VALUE_QUOTE': '' if slicing == "year" else "'", 'form': form, 'slicing': slicing} n = datetime.datetime.now() dct['unique_id'] = dct['item_name'] + "_" + \ '%d_%d_%d' % (n.minute, n.second, n.microsecond) return render(request, 'ishtar/dashboards/dashboard_main_detail.html', dct) def reset_wizards(request): # dynamically execute each reset_wizards of each ishtar app for app in settings.INSTALLED_APPS: if app == 'ishtar_common': # no need for infinite recursion continue try: module = __import__(app) except ImportError: continue if hasattr(module, 'views') and hasattr(module.views, 'reset_wizards'): module.views.reset_wizards(request) return redirect(reverse('start')) ITEM_PER_PAGE = 20 def merge_action(model, form, key): def merge(request, page=1): current_url = key + '_merge' if not page: page = 1 page = int(page) FormSet = modelformset_factory( model.merge_candidate.through, form=form, formset=forms.MergeFormSet, extra=0) q = model.merge_candidate.through.objects context = {'current_url': current_url, 'current_page': page, 'max_page': q.count() / ITEM_PER_PAGE} if page < context["max_page"]: context['next_page'] = page + 1 if page > 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(request, 'ishtar/merge_' + key + '.html', context) return merge person_merge = merge_action(models.Person, forms.MergePersonForm, 'person') organization_merge = merge_action( models.Organization, forms.MergeOrganizationForm, 'organization' ) class IshtarMixin(object): page_name = u"" def get_context_data(self, **kwargs): context = super(IshtarMixin, self).get_context_data(**kwargs) context['page_name'] = self.page_name return context class LoginRequiredMixin(object): @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): 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 ProfileEdit(LoginRequiredMixin, FormView): template_name = 'ishtar/forms/profile.html' form_class = forms.ProfilePersonForm def dispatch(self, request, *args, **kwargs): if kwargs.get('pk'): try: profile = models.UserProfile.objects.get( pk=kwargs['pk'], person__ishtaruser__user_ptr=request.user) except models.UserProfile.DoesNotExist: # cannot edit a profile that is not yours... return redirect(reverse('index')) current_changed = False # the profile edited became the current profile if not profile.current: current_changed = True profile.current = True # force post-save in case of many current profile profile.save() if current_changed: clean_session_cache(request.session) return super(ProfileEdit, self).dispatch(request, *args, **kwargs) def get_success_url(self): return reverse('profile') def get_form_kwargs(self): kwargs = super(ProfileEdit, self).get_form_kwargs() kwargs['user'] = self.request.user return kwargs def get_context_data(self, **kwargs): data = super(ProfileEdit, self).get_context_data(**kwargs) data['page_name'] = _(u"Current profile") return data def form_valid(self, form): form.save(self.request.session) return HttpResponseRedirect(self.get_success_url()) class DisplayItemView(IshtarMixin, LoginRequiredMixin, TemplateView): template_name = 'ishtar/display_item.html' def get_context_data(self, *args, **kwargs): data = super(DisplayItemView, self).get_context_data(*args, **kwargs) pk = unicode(kwargs.get('pk')) + '/' item_url = '/show-' + kwargs.get('item_type') data['show_url'] = item_url + "/" + pk return data class GlobalVarEdit(IshtarMixin, AdminLoginRequiredMixin, ModelFormSetView): template_name = 'ishtar/formset.html' model = models.GlobalVar extra = 1 can_delete = True page_name = _(u"Global variables") fields = ['slug', 'value', 'description'] class NewImportView(IshtarMixin, LoginRequiredMixin, CreateView): template_name = 'ishtar/form.html' model = models.Import form_class = forms.NewImportForm page_name = _(u"New import") def get_success_url(self): return reverse('current_imports') def get_form_kwargs(self): kwargs = super(NewImportView, self).get_form_kwargs() kwargs['user'] = self.request.user return kwargs def form_valid(self, form): user = models.IshtarUser.objects.get(pk=self.request.user.pk) self.object = form.save(user=user) return HttpResponseRedirect(self.get_success_url()) class ImportListView(IshtarMixin, LoginRequiredMixin, ListView): template_name = 'ishtar/import_list.html' model = models.Import page_name = _(u"Current imports") current_url = 'current_imports' def get_queryset(self): q = self.model.objects.exclude(state='AC') if self.request.user.is_superuser: return q.order_by('-pk') user = models.IshtarUser.objects.get(pk=self.request.user.pk) return q.filter(user=user).order_by('-pk') def post(self, request, *args, **kwargs): for field in request.POST: if not field.startswith('import-action-') or \ not request.POST[field]: continue # prevent forged forms try: imprt = models.Import.objects.get(pk=int(field.split('-')[-1])) except (models.Import.DoesNotExist, ValueError): continue if not self.request.user.is_superuser: # user can only edit his own imports user = models.IshtarUser.objects.get(pk=self.request.user.pk) if imprt.user != user: continue action = request.POST[field] if action == 'D': return HttpResponseRedirect(reverse('import_delete', kwargs={'pk': imprt.pk})) elif action == 'A': imprt.initialize(user=self.request.user.ishtaruser) elif action == 'I': if settings.USE_BACKGROUND_TASK: imprt.delayed_importation(request.session.session_key) else: imprt.importation() elif action == 'CH': if settings.USE_BACKGROUND_TASK: imprt.delayed_check_modified(request.session.session_key) else: imprt.check_modified() elif action == 'IS': if imprt.current_line is None: imprt.current_line = imprt.skip_lines imprt.save() return HttpResponseRedirect( reverse('import_step_by_step', args=[imprt.pk, imprt.current_line + 1]) ) elif action == 'AC': imprt.archive() return HttpResponseRedirect(reverse(self.current_url)) def get_context_data(self, **kwargs): dct = super(ImportListView, self).get_context_data(**kwargs) dct['autorefresh_available'] = settings.USE_BACKGROUND_TASK return dct class ImportStepByStepView(IshtarMixin, LoginRequiredMixin, TemplateView): template_name = 'ishtar/import_step_by_step.html' page_name = _(u"Import step by step") current_url = 'import_step_by_step' def get_import(self): try: self.imprt_obj = models.Import.objects.get( pk=int(self.kwargs['pk']) ) except (models.Import.DoesNotExist, ValueError): raise Http404 if not self.request.user.is_superuser: # user can only edit his own imports user = models.IshtarUser.objects.get(pk=self.request.user.pk) if self.imprt_obj.user != user: raise Http404 if not hasattr(self, 'current_line_number'): self.current_line_number = int(self.kwargs['line_number']) - 1 def update_csv(self, request): prefix = 'col-' submited_line = [(int(k[len(prefix):]), request.POST[k]) for k in request.POST if k.startswith(prefix)] updated_line = [value for line, value in sorted(submited_line)] filename = self.imprt_obj.imported_file.path with open(filename, 'r') as f: reader = unicodecsv.reader( f, encoding=self.imprt_obj.encoding) lines = [] for idx, line in enumerate(reader): if idx == self.current_line_number: line = updated_line line = [v.encode(self.imprt_obj.encoding, errors='replace') for v in line] lines.append(line) with open(filename, 'w') as f: writer = csv.writer(f, **CSV_OPTIONS) writer.writerows(lines) def import_line(self, request, *args, **kwargs): try: self.imprt, data = self.imprt_obj.importation( line_to_process=self.current_line_number, return_importer_and_data=True ) except IOError as e: self.errors = [e.message] return super(ImportStepByStepView, self).get(request, *args, **kwargs) if self.imprt_obj.get_number_of_lines() >= self.current_line_number: self.current_line_number += 1 else: self.current_line_number = None self.imprt_obj.current_line = self.current_line_number self.imprt_obj.save() return self.current_line_number def post(self, request, *args, **kwargs): if not request.POST or request.POST.get('valid', None) not in ( 'change-csv', 'import', 'change-page'): return self.get(request, *args, **kwargs) self.get_import() if request.POST.get('valid') == 'change-page': return HttpResponseRedirect( reverse('import_step_by_step', args=[self.imprt_obj.pk, request.POST.get('line-to-go', None)])) if request.POST.get('valid') == 'change-csv': self.update_csv(request) return self.get(request, *args, **kwargs) if not self.import_line(request, *args, **kwargs): return HttpResponseRedirect(reverse('current_imports')) else: return HttpResponseRedirect( reverse('import_step_by_step', args=[self.imprt_obj.pk, self.current_line_number + 1])) def get(self, request, *args, **kwargs): self.get_import() self.imprt = None self.errors, self.new_data = None, None try: self.imprt, data = self.imprt_obj.importation( simulate=True, line_to_process=self.current_line_number, return_importer_and_data=True ) except IOError as e: self.errors = [None, None, e.message] return super(ImportStepByStepView, self).get(request, *args, **kwargs) if not data or not data[0]: self.errors = self.imprt.errors if not self.errors: self.errors = [("", "", _(u"No data provided"))] else: self.new_data = data[:] return super(ImportStepByStepView, self).get(request, *args, **kwargs) def get_pagination(self, dct): pagination_step = 5 only_modified = not self.kwargs.get('all_pages', False) dct['all'] = not only_modified dct['import_url'] = 'import_step_by_step' if only_modified else \ 'import_step_by_step_all' line_nb = self.imprt_obj.get_number_of_lines() total_line_nb = self.imprt_obj.skip_lines + line_nb delta = 0 already_imported = [] if self.imprt_obj.imported_line_numbers: already_imported = self.imprt_obj.imported_line_numbers.split(',') changes = [] if self.imprt_obj.changed_line_numbers: changes = self.imprt_obj.changed_line_numbers.split(',') dct['page_is_last'] = self.current_line_number == line_nb # label, number, enabled, is_imported, has_changes dct['page_numbers'] = [] # first pass for the delta current = 0 for idx in range(self.imprt_obj.skip_lines, total_line_nb): imported = str(idx) in already_imported changed = str(idx) in changes if only_modified and (imported or not changed): continue current += 1 if idx == self.current_line_number - 1: delta = int(current / pagination_step) current, has_next, previous = 0, False, None for idx in range(self.imprt_obj.skip_lines, total_line_nb): if current >= ((delta + 1) * pagination_step): has_next = idx break imported = str(idx) in already_imported changed = str(idx) in changes if only_modified and (imported or not changed): continue current += 1 if current <= (delta * pagination_step): previous = idx continue nb = idx + 1 dct['page_numbers'].append((nb, nb, True, imported, changed)) if previous: dct['page_numbers'].insert(0, (_(u"Previous"), previous + 1, True, False, True) ) else: dct['page_numbers'].insert(0, (_(u"Previous"), self.imprt_obj.skip_lines, False, False, True) ) if has_next: dct['page_numbers'].append((_(u"Next"), has_next + 1, True, False, True)) else: dct['page_numbers'].append((_(u"Next"), total_line_nb, False, False, True)) def get_context_data(self, **kwargs): dct = super(ImportStepByStepView, self).get_context_data(**kwargs) dct['import'] = self.imprt_obj dct['line_number_displayed'] = self.current_line_number + 1 dct['line_is_imported'] = self.imprt_obj.line_is_imported( self.current_line_number) self.get_pagination(dct) dct['errors'] = self.errors if self.errors: if self.imprt.current_csv_line: headers = [f.label if f else _(u"Not imported") for f in self.imprt.get_formaters()] dct['values'] = zip( range(1, len(headers) + 1), headers, self.imprt.current_csv_line ) return dct headers, self.path_to_column, interpreted_values = [], {}, [] for idx, formater in enumerate(self.imprt.get_formaters()): if not formater: headers.append(_(u"Not imported")) interpreted_values.append(u"–") continue lbl = formater.label if formater.comment: lbl += u' ' headers.append(lbl) field_name = formater.field_name[0] if formater.export_field_name: field_name = formater.export_field_name[0] value = self.new_data[0].copy() field_name_tuple = field_name.split(u'__') # associate each path level to this column while field_name_tuple: current_field_name = u'__'.join(field_name_tuple) if current_field_name not in self.path_to_column: self.path_to_column[current_field_name] = [] self.path_to_column[current_field_name].append(idx) field_name_tuple.pop() for idx_split, key in enumerate(field_name.split('__')): if isinstance(value, dict) and key in value: value = value[key] elif not idx_split: # no key reach value = None else: break value = self.get_value(value) interpreted_values.append(value) dct['values'] = zip( range(1, len(headers) + 1), headers, self.imprt.current_csv_line, interpreted_values ) new_objects = {} for path, cls, new_dct in self.imprt.new_objects: # transform path to explicit label label = self.transform_path_to_label(self.imprt.OBJECT_CLS, path) created_dict = {} for k, val in new_dct.items(): if val in ('', None, [], [None]): continue created_dict[k] = val # check if it is not previously created dct_key = [] for key, value in created_dict.items(): if isinstance(value, list): value = tuple(sorted(value)) dct_key.append((key, value)) key = (cls, tuple(sorted(dct_key))) if key in new_objects: # regroup it new_objects[key][0].append(label) continue # values - transform key to explicit label value_dct = self.transform_keys_to_label(path, cls, created_dict) for k in value_dct.keys(): value_dct[k] = self.get_value(value_dct[k]) new_objects[key] = ([label], cls, value_dct) dct['new_objects'] = [ [u" – ".join(lbls), cls, new_dct] for lbls, cls, new_dct in new_objects.values() ] updated_objects = [] main_changed = False for path, obj, values, updated_values in self.imprt.updated_objects: # transform path to explicit label label = self.transform_path_to_label(self.imprt.OBJECT_CLS, path) # transform key into explicit label values = self.transform_keys_to_label(path, obj.__class__, values) # add current values and changed bool old_and_updated = {} for k in updated_values.keys(): current_value = getattr(obj, k) updated_value = updated_values[k] if hasattr(current_value, 'all'): current_value = list(current_value.all()) changed = False for v in updated_value: if v not in current_value: changed = True current_value = self.list_to_html(current_value) updated_value = self.list_to_html(updated_value) else: changed = current_value != updated_value current_value = self.get_value(current_value) updated_value = self.get_value(updated_value) main_changed |= changed old_and_updated[k] = [current_value, updated_value, changed] # transform key into explicit label old_and_updated = self.transform_keys_to_label(path, obj.__class__, old_and_updated) updated_objects.append((label, obj, values, old_and_updated)) dct['have_change'] = main_changed or self.imprt.new_objects if dct["have_change"]: self.imprt_obj.add_changed_line(self.current_line_number) else: self.imprt_obj.remove_changed_line(self.current_line_number) dct['updated_objects'] = [] dct['matched_objects'] = [] for path, obj, values, old_and_updated in updated_objects: if old_and_updated: dct['updated_objects'].append( (path, obj, values, old_and_updated) ) else: dct['matched_objects'].append( (path, obj, values) ) dct['ambiguous_objects'] = self.imprt.ambiguous_objects dct['not_find_objects'] = self.imprt.not_find_objects return dct def transform_path_to_label(self, cls, path): label = u" > ".join( unicode(l) for l in get_field_labels_from_path(cls, path) ) if not label: label = unicode(cls._meta.verbose_name) return label def transform_keys_to_label(self, path, cls, dct): value_dct = {} for k in dct: label = get_field_labels_from_path(cls, [k]) if label: label = unicode(label[0]) else: label = k concat_path = u"__".join(list(path) + [k]) if concat_path in self.path_to_column: for col in self.path_to_column[concat_path]: col += 1 label += u" "\ u" {} {} "\ u"".format(col, _(u"Col. "), col) value_dct[label] = dct[k] return value_dct def list_to_html(self, lst): if not lst: return _(u"* empty *") return u"" def get_value(self, item): if hasattr(item, 'SHOW_URL'): return u"{}{}".format(unicode(item), link_to_window(item)) if hasattr(item, 'explicit_label'): return item.explicit_label if item in (None, [], [None]): return _(u"* empty *") if isinstance(item, list): return self.list_to_html(item) return unicode(item) class ImportListTableView(ImportListView): template_name = 'ishtar/import_table.html' current_url = 'current_imports_table' def get_context_data(self, **kwargs): dct = super(ImportListTableView, self).get_context_data(**kwargs) dct['AJAX'] = True dct['MESSAGES'] = [] request = self.request if 'messages' in request.session and \ request.session['messages']: for message, message_type in request.session['messages']: dct['MESSAGES'].append((message, message_type)) request.session['messages'] = [] if 'current_import_id' in request.session and \ request.session['current_import_id']: dct['refreshed_pks'] = request.session.pop('current_import_id') return dct class ImportOldListView(ImportListView): page_name = _(u"Old imports") current_url = 'old_imports' def get_queryset(self): q = self.model.objects.filter(state='AC') if self.request.user.is_superuser: return q.order_by('-creation_date') user = models.IshtarUser.objects.get(pk=self.request.user.pk) return q.filter(user=user).order_by('-creation_date') class ImportLinkView(IshtarMixin, LoginRequiredMixin, ModelFormSetView): template_name = 'ishtar/formset_import_match.html' model = models.TargetKey page_name = _(u"Link unmatched items") extra = 0 form_class = forms.TargetKeyForm formset_class = forms.TargetKeyFormset def get_formset_kwargs(self): kwargs = super(ImportLinkView, self).get_formset_kwargs() kwargs['user'] = self.request.user return kwargs def get_queryset(self): return self.model.objects.filter( is_set=False, associated_import=self.kwargs['pk']) def get_success_url(self): return reverse('import_link_unmatched', args=[self.kwargs['pk']]) class ImportDeleteView(IshtarMixin, LoginRequiredMixin, DeleteView): template_name = 'ishtar/import_delete.html' model = models.Import page_name = _(u"Delete import") def get_success_url(self): return reverse('current_imports') class PersonCreate(LoginRequiredMixin, CreateView): model = models.Person form_class = forms.BasePersonForm template_name = 'ishtar/person_form.html' def get_success_url(self): return reverse('person_edit', args=[self.object.pk]) class PersonEdit(LoginRequiredMixin, UpdateView): model = models.Person form_class = forms.BasePersonForm template_name = 'ishtar/person_form.html' def get_success_url(self): return reverse('person_edit', args=[self.object.pk]) class ManualMergeMixin(object): def form_valid(self, form): self.items = form.get_items() return super(ManualMergeMixin, self).form_valid(form) def get_success_url(self): return reverse( self.redir_url, args=[u"_".join([str(item.pk) for item in self.items])]) class PersonManualMerge(ManualMergeMixin, IshtarMixin, LoginRequiredMixin, FormView): form_class = forms.PersonMergeFormSelection template_name = 'ishtar/form.html' page_name = _(u"Merge persons") current_url = 'person-manual-merge' redir_url = 'person_manual_merge_items' class ManualMergeItemsMixin(object): def get_form_kwargs(self): kwargs = super(ManualMergeItemsMixin, self).get_form_kwargs() kwargs['items'] = self.kwargs['pks'].split('_') return kwargs def form_valid(self, form): self.item = form.merge() return super(ManualMergeItemsMixin, self).form_valid(form) def get_success_url(self): return reverse('display-item', args=[self.item_type, self.item.pk]) class PersonManualMergeItems( ManualMergeItemsMixin, IshtarMixin, LoginRequiredMixin, FormView): form_class = forms.PersonMergeIntoForm template_name = 'ishtar/form.html' page_name = _(u"Select the main person") current_url = 'person-manual-merge-items' item_type = 'person' class OrgaManualMerge(ManualMergeMixin, IshtarMixin, LoginRequiredMixin, FormView): form_class = forms.OrgaMergeFormSelection template_name = 'ishtar/form.html' page_name = _(u"Merge organization") current_url = 'orga-manual-merge' redir_url = 'orga_manual_merge_items' class OrgaManualMergeItems( ManualMergeItemsMixin, IshtarMixin, LoginRequiredMixin, FormView): form_class = forms.OrgaMergeIntoForm template_name = 'ishtar/form.html' page_name = _(u"Select the main organization") current_url = 'orga-manual-merge-items' item_type = 'organization' class OrganizationCreate(LoginRequiredMixin, CreateView): model = models.Organization form_class = forms.BaseOrganizationForm template_name = 'ishtar/organization_form.html' form_prefix = "orga" def get_form_kwargs(self): kwargs = super(OrganizationCreate, self).get_form_kwargs() if hasattr(self.form_class, 'form_prefix'): kwargs.update({'prefix': self.form_class.form_prefix}) return kwargs def get_success_url(self): return reverse('organization_edit', args=[self.object.pk]) class OrganizationEdit(LoginRequiredMixin, UpdateView): model = models.Organization form_class = forms.BaseOrganizationForm template_name = 'ishtar/organization_form.html' def get_form_kwargs(self): kwargs = super(OrganizationEdit, self).get_form_kwargs() if hasattr(self.form_class, 'form_prefix'): kwargs.update({'prefix': self.form_class.form_prefix}) return kwargs def get_success_url(self): return reverse('organization_edit', args=[self.object.pk]) class OrganizationPersonCreate(LoginRequiredMixin, CreateView): model = models.Person form_class = forms.BaseOrganizationPersonForm template_name = 'ishtar/organization_person_form.html' relative_label = _("Corporation manager") def get_context_data(self, *args, **kwargs): data = super(OrganizationPersonCreate, self).get_context_data(*args, **kwargs) data['relative_label'] = self.relative_label return data def get_success_url(self): return reverse('organization_person_edit', args=[self.object.pk]) class OrganizationPersonEdit(LoginRequiredMixin, UpdateView): model = models.Person form_class = forms.BaseOrganizationPersonForm template_name = 'ishtar/organization_person_form.html' relative_label = _("Corporation manager") def get_context_data(self, *args, **kwargs): data = super(OrganizationPersonEdit, self).get_context_data(*args, **kwargs) data['relative_label'] = self.relative_label return data def get_success_url(self): return reverse('organization_person_edit', args=[self.object.pk]) # documents 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( [('selec-document_search', forms.DocumentFormSelection)], label=_(u"Document: search"), url_name='search-document', ) class DocumentFormMixin(IshtarMixin, LoginRequiredMixin): form_class = forms.DocumentForm template_name = 'ishtar/forms/document.html' model = models.Document def get_success_url(self): return reverse('display-document', args=[self.object.pk]) class DocumentCreateView(DocumentFormMixin, CreateView): page_name = _(u"Document creation") def get_form_kwargs(self): kwargs = super(DocumentCreateView, self).get_form_kwargs() initial = kwargs.get('initial', {}) for related_key in models.Document.RELATED_MODELS_ALT: model = models.Document._meta.get_field(related_key).related_model if model.SLUG in self.request.GET: try: item = model.objects.get(pk=self.request.GET[model.SLUG]) except model.DoesNotExist: continue initial[related_key] = str(item.pk) if initial: kwargs['initial'] = initial return kwargs class DocumentSelectView(IshtarMixin, LoginRequiredMixin, FormView): form_class = forms.DocumentFormSelection template_name = 'ishtar/form.html' redir_url = 'edit-document' def form_valid(self, form): self.pk = form.cleaned_data['pk'] return super(DocumentSelectView, self).form_valid(form) def get_success_url(self): return reverse(self.redir_url, args=[self.pk]) class DocumentEditView(DocumentFormMixin, UpdateView): page_name = _(u"Document modification") def get_form_kwargs(self): kwargs = super(DocumentEditView, self).get_form_kwargs() try: document = models.Document.objects.get(pk=self.kwargs.get('pk')) assert check_permission(self.request, 'document/edit', document.pk) except (AssertionError, models.Document.DoesNotExist): raise Http404() initial = {} for k in self.form_class.base_fields.keys() + \ models.Document.RELATED_MODELS: value = getattr(document, k) if hasattr(value, 'all'): value = ",".join([str(v.pk) for v in value.all()]) if hasattr(value, 'pk'): value = value.pk initial[k] = value kwargs['initial'] = initial return kwargs document_deletion_steps = [ ('selec-document_deletion', forms.DocumentFormSelection), ('final-document_deletion', FinalDeleteForm) ] document_deletion_wizard = wizards.DocumentDeletionWizard.as_view( document_deletion_steps, label=_(u"Document deletion"), url_name='document_deletion',)