| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618 |
- from crispy_forms.utils import TEMPLATE_PACK
- from django.contrib.contenttypes.fields import GenericRelation
- from django.contrib.contenttypes.models import ContentType
- from django.core.exceptions import PermissionDenied
- from django.db import models
- from django.db.models.query import QuerySet
- from django.forms.models import model_to_dict
- from django.http import HttpResponseRedirect
- from django.shortcuts import get_object_or_404
- from django.template.response import TemplateResponse
- from django.utils import six
- from django.utils.encoding import force_text, smart_text
- from django.utils.safestring import mark_safe
- from django.utils.text import capfirst
- from django.utils.translation import ugettext as _
- from xadmin.layout import Field, render_field
- from xadmin.plugins.inline import Inline
- from xadmin.plugins.actions import BaseActionView
- from xadmin.plugins.inline import InlineModelAdmin
- from xadmin.sites import site
- from xadmin.util import unquote, quote, model_format_dict, is_related_field2
- from xadmin.views import BaseAdminPlugin, ModelAdminView, CreateAdminView, UpdateAdminView, DetailAdminView, ModelFormAdminView, DeleteAdminView, ListAdminView
- from xadmin.views.base import csrf_protect_m, filter_hook
- from xadmin.views.detail import DetailAdminUtil
- from reversion.models import Revision, Version
- from reversion.revisions import is_active, register, is_registered, set_comment, create_revision, set_user
- from contextlib import contextmanager
- from functools import partial
- def _autoregister(admin, model, follow=None):
- """Registers a model with reversion, if required."""
- if model._meta.proxy:
- raise RegistrationError("Proxy models cannot be used with django-reversion, register the parent class instead")
- if not is_registered(model):
- follow = follow or []
- for parent_cls, field in model._meta.parents.items():
- follow.append(field.name)
- _autoregister(admin, parent_cls)
- register(model, follow=follow, format=admin.reversion_format)
- def _register_model(admin, model):
- if not hasattr(admin, 'reversion_format'):
- admin.reversion_format = 'json'
- if not is_registered(model):
- inline_fields = []
- for inline in getattr(admin, 'inlines', []):
- inline_model = inline.model
- if getattr(inline, 'generic_inline', False):
- ct_field = getattr(inline, 'ct_field', 'content_type')
- ct_fk_field = getattr(inline, 'ct_fk_field', 'object_id')
- for field in model._meta.many_to_many:
- if isinstance(field, GenericRelation) \
- and field.rel.to == inline_model \
- and field.object_id_field_name == ct_fk_field \
- and field.content_type_field_name == ct_field:
- inline_fields.append(field.name)
- _autoregister(admin, inline_model)
- else:
- fk_name = getattr(inline, 'fk_name', None)
- if not fk_name:
- for field in inline_model._meta.fields:
- if isinstance(field, (models.ForeignKey, models.OneToOneField)) and issubclass(model, field.remote_field.model):
- fk_name = field.name
- _autoregister(admin, inline_model, follow=[fk_name])
- if not inline_model._meta.get_field(fk_name).remote_field.is_hidden():
- accessor = inline_model._meta.get_field(fk_name).remote_field.get_accessor_name()
- inline_fields.append(accessor)
- _autoregister(admin, model, inline_fields)
- def register_models(admin_site=None):
- if admin_site is None:
- admin_site = site
- for model, admin in admin_site._registry.items():
- if getattr(admin, 'reversion_enable', False):
- _register_model(admin, model)
- @contextmanager
- def do_create_revision(request):
- with create_revision():
- set_user(request.user)
- yield
- class ReversionPlugin(BaseAdminPlugin):
- # The serialization format to use when registering models with reversion.
- reversion_format = "json"
- # Whether to ignore duplicate revision data.
- ignore_duplicate_revisions = False
- reversion_enable = False
- def init_request(self, *args, **kwargs):
- return self.reversion_enable
- def do_post(self, __):
- def _method():
- self.revision_context_manager.set_user(self.user)
- comment = ''
- admin_view = self.admin_view
- if isinstance(admin_view, CreateAdminView):
- comment = _(u"Initial version.")
- elif isinstance(admin_view, UpdateAdminView):
- comment = _(u"Change version.")
- elif isinstance(admin_view, RevisionView):
- comment = _(u"Revert version.")
- elif isinstance(admin_view, RecoverView):
- comment = _(u"Rercover version.")
- elif isinstance(admin_view, DeleteAdminView):
- comment = _(u"Deleted %(verbose_name)s.") % {
- "verbose_name": self.opts.verbose_name}
- self.revision_context_manager.set_comment(comment)
- return __()
- return _method
- def post(self, __, request, *args, **kwargs):
- with do_create_revision(request):
- return __()
- # Block Views
- def block_top_toolbar(self, context, nodes):
- recoverlist_url = self.admin_view.model_admin_url('recoverlist')
- nodes.append(mark_safe('<div class="btn-group"><a class="btn btn-default btn-sm" href="%s"><i class="fa fa-trash-o"></i> %s</a></div>' % (recoverlist_url, _(u"Recover"))))
- def block_nav_toggles(self, context, nodes):
- obj = getattr(
- self.admin_view, 'org_obj', getattr(self.admin_view, 'obj', None))
- if obj:
- revisionlist_url = self.admin_view.model_admin_url(
- 'revisionlist', quote(obj.pk))
- nodes.append(mark_safe('<a href="%s" class="navbar-toggle pull-right"><i class="fa fa-calendar"></i></a>' % revisionlist_url))
- def block_nav_btns(self, context, nodes):
- obj = getattr(
- self.admin_view, 'org_obj', getattr(self.admin_view, 'obj', None))
- if obj:
- revisionlist_url = self.admin_view.model_admin_url(
- 'revisionlist', quote(obj.pk))
- nodes.append(mark_safe('<a href="%s" class="btn btn-default"><i class="fa fa-calendar"></i> <span>%s</span></a>' % (revisionlist_url, _(u'History'))))
- # action revision
- class ActionRevisionPlugin(BaseAdminPlugin):
- reversion_enable = False
- def init_request(self, *args, **kwargs):
- return self.reversion_enable
- def do_action(self, __, queryset):
- with do_create_revision(self.request):
- return __()
- class BaseReversionView(ModelAdminView):
- # The serialization format to use when registering models with reversion.
- reversion_format = "json"
- # Whether to ignore duplicate revision data.
- ignore_duplicate_revisions = False
- # If True, then the default ordering of object_history and recover lists will be reversed.
- history_latest_first = False
- reversion_enable = False
- def init_request(self, *args, **kwargs):
- if not self.has_change_permission() and not self.has_add_permission():
- raise PermissionDenied
- def _order_version_queryset(self, queryset):
- """Applies the correct ordering to the given version queryset."""
- if self.history_latest_first:
- return queryset.order_by("-pk")
- return queryset.order_by("pk")
- class RecoverListView(BaseReversionView):
- recover_list_template = None
- def get_context(self):
- context = super(RecoverListView, self).get_context()
- opts = self.opts
- deleted = self._order_version_queryset(Version.objects.get_deleted(self.model))
- context.update({
- "opts": opts,
- "app_label": opts.app_label,
- "model_name": capfirst(opts.verbose_name),
- "title": _("Recover deleted %(name)s") % {"name": force_text(opts.verbose_name_plural)},
- "deleted": deleted,
- "changelist_url": self.model_admin_url("changelist"),
- })
- return context
- @csrf_protect_m
- def get(self, request, *args, **kwargs):
- context = self.get_context()
- return TemplateResponse(
- request, self.recover_list_template or self.get_template_list(
- "views/recover_list.html"),
- context)
- class RevisionListView(BaseReversionView):
- object_history_template = None
- revision_diff_template = None
- def _reversion_order_version_queryset(self, queryset):
- """Applies the correct ordering to the given version queryset."""
- if not self.history_latest_first:
- queryset = queryset.order_by("pk")
- return queryset
- def get_context(self):
- context = super(RevisionListView, self).get_context()
- opts = self.opts
- action_list = [
- {
- "revision": version.revision,
- "url": self.model_admin_url('revision', quote(version.object_id), version.id),
- "version": version
- }
- for version
- in self._reversion_order_version_queryset(Version.objects.get_for_object_reference(
- self.model,
- self.obj.pk,
- ).select_related("revision__user"))
- ]
- context.update({
- 'title': _('Change history: %s') % force_text(self.obj),
- 'action_list': action_list,
- 'model_name': capfirst(force_text(opts.verbose_name_plural)),
- 'object': self.obj,
- 'app_label': opts.app_label,
- "changelist_url": self.model_admin_url("changelist"),
- "update_url": self.model_admin_url("change", self.obj.pk),
- 'opts': opts,
- })
- return context
- def get(self, request, object_id, *args, **kwargs):
- object_id = unquote(object_id)
- self.obj = self.get_object(object_id)
- if not self.has_change_permission(self.obj):
- raise PermissionDenied
- return self.get_response()
- def get_response(self):
- context = self.get_context()
- return TemplateResponse(self.request, self.object_history_template or
- self.get_template_list('views/model_history.html'), context)
- def get_version_object(self, version):
- obj_version = version._object_version
- obj = obj_version.object
- obj._state.db = self.obj._state.db
- for field_name, pks in obj_version.m2m_data.items():
- f = self.opts.get_field(field_name)
- if f.rel and isinstance(f.rel, models.ManyToManyRel):
- setattr(obj, f.name, f.rel.to._default_manager.get_query_set(
- ).filter(pk__in=pks).all())
- detail = self.get_model_view(DetailAdminUtil, self.model, obj)
- return obj, detail
- def post(self, request, object_id, *args, **kwargs):
- object_id = unquote(object_id)
- self.obj = self.get_object(object_id)
- if not self.has_change_permission(self.obj):
- raise PermissionDenied
- params = self.request.POST
- if 'version_a' not in params or 'version_b' not in params:
- self.message_user(_("Must select two versions."), 'error')
- return self.get_response()
- version_a_id = params['version_a']
- version_b_id = params['version_b']
- if version_a_id == version_b_id:
- self.message_user(
- _("Please select two different versions."), 'error')
- return self.get_response()
- version_a = get_object_or_404(Version, pk=version_a_id)
- version_b = get_object_or_404(Version, pk=version_b_id)
- diffs = []
- obj_a, detail_a = self.get_version_object(version_a)
- obj_b, detail_b = self.get_version_object(version_b)
- for f in (self.opts.fields + self.opts.many_to_many):
- if is_related_field2(f):
- label = f.opts.verbose_name
- else:
- label = f.verbose_name
- value_a = f.value_from_object(obj_a)
- value_b = f.value_from_object(obj_b)
- is_diff = value_a != value_b
- if type(value_a) in (list, tuple) and type(value_b) in (list, tuple) \
- and len(value_a) == len(value_b) and is_diff:
- is_diff = False
- for i in xrange(len(value_a)):
- if value_a[i] != value_a[i]:
- is_diff = True
- break
- if type(value_a) is QuerySet and type(value_b) is QuerySet:
- is_diff = list(value_a) != list(value_b)
- diffs.append((label, detail_a.get_field_result(
- f.name).val, detail_b.get_field_result(f.name).val, is_diff))
- context = super(RevisionListView, self).get_context()
- context.update({
- 'object': self.obj,
- 'opts': self.opts,
- 'version_a': version_a,
- 'version_b': version_b,
- 'revision_a_url': self.model_admin_url('revision', quote(version_a.object_id), version_a.id),
- 'revision_b_url': self.model_admin_url('revision', quote(version_b.object_id), version_b.id),
- 'diffs': diffs
- })
- return TemplateResponse(
- self.request, self.revision_diff_template or self.get_template_list('views/revision_diff.html'),
- context)
- @filter_hook
- def get_media(self):
- return super(RevisionListView, self).get_media() + self.vendor('xadmin.plugin.revision.js', 'xadmin.form.css')
- class BaseRevisionView(ModelFormAdminView):
- @filter_hook
- def get_revision(self):
- return self.version.field_dict
- @filter_hook
- def get_form_datas(self):
- datas = {"instance": self.org_obj, "initial": self.get_revision()}
- if self.request_method == 'post':
- datas.update(
- {'data': self.request.POST, 'files': self.request.FILES})
- return datas
- @filter_hook
- def get_context(self):
- context = super(BaseRevisionView, self).get_context()
- context.update({
- 'object': self.org_obj
- })
- return context
- @filter_hook
- def get_media(self):
- return super(BaseRevisionView, self).get_media() + self.vendor('xadmin.plugin.revision.js')
- class DiffField(Field):
- def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
- html = ''
- for field in self.fields:
- html += ('<div class="diff_field" rel="tooltip"><textarea class="org-data" style="display:none;">%s</textarea>%s</div>' %
- (_('Current: %s') % self.attrs.pop('orgdata', ''), render_field(field, form, form_style, context, template_pack=template_pack, attrs=self.attrs)))
- return html
- class RevisionView(BaseRevisionView):
- revision_form_template = None
- def init_request(self, object_id, version_id):
- self.detail = self.get_model_view(
- DetailAdminView, self.model, object_id)
- self.org_obj = self.detail.obj
- self.version = get_object_or_404(
- Version, pk=version_id, object_id=smart_text(self.org_obj.pk))
- self.prepare_form()
- def get_form_helper(self):
- helper = super(RevisionView, self).get_form_helper()
- diff_fields = {}
- version_data = self.version.field_dict
- for f in self.opts.fields:
- fvalue = f.value_from_object(self.org_obj)
- vvalue = version_data.get(f.name, None)
- if fvalue is None and vvalue == '':
- vvalue = None
- if is_related_field2(f):
- vvalue = version_data.get(f.name + '_' + f.rel.get_related_field().name, None)
- if fvalue != vvalue:
- diff_fields[f.name] = self.detail.get_field_result(f.name).val
- for k, v in diff_fields.items():
- helper[k].wrap(DiffField, orgdata=v)
- return helper
- @filter_hook
- def get_context(self):
- context = super(RevisionView, self).get_context()
- context["title"] = _(
- "Revert %s") % force_text(self.model._meta.verbose_name)
- return context
- @filter_hook
- def get_response(self):
- context = self.get_context()
- context.update(self.kwargs or {})
- form_template = self.revision_form_template
- return TemplateResponse(
- self.request, form_template or self.get_template_list(
- 'views/revision_form.html'),
- context)
- @filter_hook
- def post_response(self):
- self.message_user(_('The %(model)s "%(name)s" was reverted successfully. You may edit it again below.') %
- {"model": force_text(self.opts.verbose_name), "name": smart_text(self.new_obj)}, 'success')
- return HttpResponseRedirect(self.model_admin_url('change', self.new_obj.pk))
- class RecoverView(BaseRevisionView):
- recover_form_template = None
- def init_request(self, version_id):
- if not self.has_change_permission() and not self.has_add_permission():
- raise PermissionDenied
- self.version = get_object_or_404(Version, pk=version_id)
- self.org_obj = self.version._object_version.object
- self.prepare_form()
- @filter_hook
- def get_context(self):
- context = super(RecoverView, self).get_context()
- context["title"] = _("Recover %s") % self.version.object_repr
- return context
- @filter_hook
- def get_response(self):
- context = self.get_context()
- context.update(self.kwargs or {})
- form_template = self.recover_form_template
- return TemplateResponse(
- self.request, form_template or self.get_template_list(
- 'views/recover_form.html'),
- context)
- @filter_hook
- def post_response(self):
- self.message_user(_('The %(model)s "%(name)s" was recovered successfully. You may edit it again below.') %
- {"model": force_text(self.opts.verbose_name), "name": smart_text(self.new_obj)}, 'success')
- return HttpResponseRedirect(self.model_admin_url('change', self.new_obj.pk))
- class InlineDiffField(Field):
- def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
- html = ''
- instance = form.instance
- if not instance.pk:
- return super(InlineDiffField, self).render(form, form_style, context)
- initial = form.initial
- opts = instance._meta
- detail = form.detail
- for field in self.fields:
- f = opts.get_field(field)
- f_html = render_field(field, form, form_style, context,
- template_pack=template_pack, attrs=self.attrs)
- if f.value_from_object(instance) != initial.get(field, None):
- current_val = detail.get_field_result(f.name).val
- html += ('<div class="diff_field" rel="tooltip"><textarea class="org-data" style="display:none;">%s</textarea>%s</div>'
- % (_('Current: %s') % current_val, f_html))
- else:
- html += f_html
- return html
- # inline hack plugin
- class InlineRevisionPlugin(BaseAdminPlugin):
- def get_related_versions(self, obj, version, formset):
- """Retreives all the related Version objects for the given FormSet."""
- object_id = obj.pk
- # Get the fk name.
- try:
- fk_name = formset.fk.name + '_' + formset.fk.rel.get_related_field().name
- except AttributeError:
- # This is a GenericInlineFormset, or similar.
- fk_name = formset.ct_fk_field.name
- # Look up the revision data.
- revision_versions = version.revision.version_set.all()
- related_versions = dict([(related_version.object_id, related_version)
- for related_version in revision_versions
- if ContentType.objects.get_for_id(related_version.content_type_id).model_class() == formset.model
- and smart_text(related_version.field_dict[fk_name]) == smart_text(object_id)])
- return related_versions
- def _hack_inline_formset_initial(self, revision_view, formset):
- """Hacks the given formset to contain the correct initial data."""
- # Now we hack it to push in the data from the revision!
- initial = []
- related_versions = self.get_related_versions(
- revision_view.org_obj, revision_view.version, formset)
- formset.related_versions = related_versions
- for related_obj in formset.queryset:
- if smart_text(related_obj.pk) in related_versions:
- initial.append(
- related_versions.pop(smart_text(related_obj.pk)).field_dict)
- else:
- initial_data = model_to_dict(related_obj)
- initial_data["DELETE"] = True
- initial.append(initial_data)
- for related_version in related_versions.values():
- initial_row = related_version.field_dict
- pk_name = ContentType.objects.get_for_id(
- related_version.content_type_id).model_class()._meta.pk.name
- del initial_row[pk_name]
- initial.append(initial_row)
- # Reconstruct the forms with the new revision data.
- formset.initial = initial
- formset.forms = [formset._construct_form(
- n) for n in xrange(len(initial))]
- # Hack the formset to force a save of everything.
- def get_changed_data(form):
- return [field.name for field in form.fields]
- for form in formset.forms:
- form.has_changed = lambda: True
- form._get_changed_data = partial(get_changed_data, form=form)
- def total_form_count_hack(count):
- return lambda: count
- formset.total_form_count = total_form_count_hack(len(initial))
- if self.request.method == 'GET' and formset.helper and formset.helper.layout:
- helper = formset.helper
- cls_str = str if six.PY3 else basestring
- helper.filter(cls_str).wrap(InlineDiffField)
- fake_admin_class = type(str('%s%sFakeAdmin' % (self.opts.app_label, self.opts.model_name)), (object, ), {'model': self.model})
- for form in formset.forms:
- instance = form.instance
- if instance.pk:
- form.detail = self.get_view(
- DetailAdminUtil, fake_admin_class, instance)
- def instance_form(self, formset, **kwargs):
- admin_view = self.admin_view.admin_view
- if hasattr(admin_view, 'version') and hasattr(admin_view, 'org_obj'):
- self._hack_inline_formset_initial(admin_view, formset)
- return formset
- class VersionInline(object):
- model = Version
- extra = 0
- style = 'accordion'
- class ReversionAdmin(object):
- model_icon = 'fa fa-exchange'
- list_display = ('__str__', 'date_created', 'user', 'comment')
- list_display_links = ('__str__',)
- list_filter = ('date_created', 'user')
- inlines = [VersionInline]
- site.register(Revision, ReversionAdmin)
- site.register_modelview(
- r'^recover/$', RecoverListView, name='%s_%s_recoverlist')
- site.register_modelview(
- r'^recover/([^/]+)/$', RecoverView, name='%s_%s_recover')
- site.register_modelview(
- r'^([^/]+)/revision/$', RevisionListView, name='%s_%s_revisionlist')
- site.register_modelview(
- r'^([^/]+)/revision/([^/]+)/$', RevisionView, name='%s_%s_revision')
- site.register_plugin(ReversionPlugin, ListAdminView)
- site.register_plugin(ReversionPlugin, ModelFormAdminView)
- site.register_plugin(ReversionPlugin, DeleteAdminView)
- site.register_plugin(InlineRevisionPlugin, InlineModelAdmin)
- site.register_plugin(ActionRevisionPlugin, BaseActionView)
|