2017-06-13 03:31:10 +02:00
|
|
|
import json
|
2017-06-16 20:27:07 +02:00
|
|
|
from collections import OrderedDict
|
2017-06-12 14:52:08 +02:00
|
|
|
|
|
|
|
from django.apps import apps
|
2017-06-12 13:20:26 +02:00
|
|
|
from django.conf import settings
|
2017-06-20 17:56:20 +02:00
|
|
|
from django.core.serializers.json import DjangoJSONEncoder
|
2017-06-20 13:36:09 +02:00
|
|
|
from django.db import models, transaction
|
2017-06-12 16:59:57 +02:00
|
|
|
from django.db.models import Q
|
2017-06-16 13:00:47 +02:00
|
|
|
from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor, ManyToManyDescriptor
|
2017-06-13 15:31:54 +02:00
|
|
|
from django.urls import reverse
|
2017-06-12 13:20:26 +02:00
|
|
|
from django.utils.translation import ugettext_lazy as _
|
2017-06-12 17:22:38 +02:00
|
|
|
from django.utils.translation import ungettext_lazy
|
2017-06-12 13:20:26 +02:00
|
|
|
|
2017-06-21 13:54:00 +02:00
|
|
|
from c3nav.editor.models.change import Change
|
2017-06-21 14:19:13 +02:00
|
|
|
from c3nav.editor.utils import is_created_pk
|
|
|
|
from c3nav.editor.wrappers import ModelWrapper
|
2017-06-21 13:54:00 +02:00
|
|
|
from c3nav.mapdata.models import LocationSlug
|
|
|
|
from c3nav.mapdata.models.locations import LocationRedirect
|
2017-06-12 22:56:39 +02:00
|
|
|
|
2017-06-12 13:20:26 +02:00
|
|
|
|
|
|
|
class ChangeSet(models.Model):
|
|
|
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_('created'))
|
2017-06-12 16:59:57 +02:00
|
|
|
author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT, verbose_name=_('Author'))
|
2017-06-18 22:14:03 +02:00
|
|
|
session_id = models.CharField(unique=True, null=True, max_length=32)
|
2017-06-12 13:20:26 +02:00
|
|
|
proposed = models.DateTimeField(null=True, verbose_name=_('proposed'))
|
|
|
|
applied = models.DateTimeField(null=True, verbose_name=_('applied'))
|
|
|
|
applied_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT,
|
|
|
|
related_name='applied_changesets', verbose_name=_('applied by'))
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _('Change Set')
|
|
|
|
verbose_name_plural = _('Change Sets')
|
|
|
|
default_related_name = 'changesets'
|
|
|
|
|
2017-06-12 22:56:39 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.default_author = None
|
2017-06-20 15:39:22 +02:00
|
|
|
self.changes_qs = None
|
2017-06-18 15:30:35 +02:00
|
|
|
self.ever_created_objects = {}
|
2017-06-14 02:19:37 +02:00
|
|
|
self.created_objects = {}
|
2017-06-14 00:51:55 +02:00
|
|
|
self.updated_existing = {}
|
2017-06-13 16:36:18 +02:00
|
|
|
self.deleted_existing = {}
|
2017-06-16 19:19:54 +02:00
|
|
|
self.m2m_added = {}
|
|
|
|
self.m2m_removed = {}
|
2017-06-15 17:53:00 +02:00
|
|
|
self._last_change_pk = 0
|
2017-06-13 16:15:28 +02:00
|
|
|
|
2017-06-21 13:31:41 +02:00
|
|
|
"""
|
|
|
|
Get Changesets for Request/Session/User
|
|
|
|
"""
|
|
|
|
@classmethod
|
|
|
|
def qs_base(cls, hide_applied=True):
|
|
|
|
qs = cls.objects.select_related('author')
|
|
|
|
if hide_applied:
|
|
|
|
qs = qs.filter(applied__isnull=True)
|
|
|
|
return qs
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def qs_for_request(cls, request):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Returns a base QuerySet to get only changesets the current user is allowed to see
|
|
|
|
"""
|
2017-06-21 13:31:41 +02:00
|
|
|
qs = cls.qs_base()
|
|
|
|
if request.user.is_authenticated:
|
|
|
|
qs = qs.filter(author=request.user)
|
|
|
|
else:
|
|
|
|
qs = qs.filter(author__isnull=True)
|
|
|
|
return qs
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_for_request(cls, request):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Get the changeset for the current request.
|
|
|
|
If a changeset is associated with the session id, it will be returned.
|
|
|
|
Otherwise, if the user is authenticated, the last created queryset
|
|
|
|
for this user will be returned and the session id will be added to it.
|
|
|
|
If both fails, an empty unsaved changeset will be returned which will
|
|
|
|
be automatically saved when a change is added to it.
|
|
|
|
In any case, the default autor for changes added to the queryset during
|
|
|
|
this request will be set to the current user.
|
|
|
|
"""
|
2017-06-21 13:31:41 +02:00
|
|
|
qs = cls.qs_for_request(request)
|
|
|
|
|
|
|
|
if request.session.session_key is not None:
|
|
|
|
changeset = qs.filter(session_id=request.session.session_key).first()
|
|
|
|
if changeset is not None:
|
|
|
|
changeset.default_author = request.user
|
|
|
|
if changeset.author_id is None and request.user.is_authenticated:
|
|
|
|
changeset.author = request.user
|
|
|
|
changeset.save()
|
|
|
|
return changeset
|
|
|
|
|
|
|
|
new_changeset = cls()
|
|
|
|
new_changeset.request = request
|
|
|
|
|
|
|
|
if request.user.is_authenticated:
|
|
|
|
changeset = qs.filter(Q(author=request.user)).order_by('-created').first()
|
|
|
|
if changeset is not None:
|
|
|
|
if request.session.session_key is None:
|
|
|
|
request.session.save()
|
|
|
|
changeset.session_id = request.session.session_key
|
|
|
|
changeset.save()
|
|
|
|
changeset.default_author = request.user
|
|
|
|
return changeset
|
|
|
|
|
|
|
|
new_changeset.author = request.user
|
|
|
|
|
|
|
|
new_changeset.session_id = request.session.session_key
|
|
|
|
new_changeset.default_author = request.user
|
|
|
|
return new_changeset
|
|
|
|
|
|
|
|
"""
|
|
|
|
Wrap Objects
|
|
|
|
"""
|
|
|
|
def wrap(self, obj, author=None):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Wrap the given object in a changeset wrapper.
|
|
|
|
:param obj: A model, a model instance or the name of a model as a string.
|
|
|
|
:param author: Author to which ne changes will be assigned. This changesets default author (if set) if None.
|
|
|
|
"""
|
2017-06-21 13:31:41 +02:00
|
|
|
self.parse_changes()
|
|
|
|
if author is None:
|
|
|
|
author = self.default_author
|
|
|
|
if author is not None and not author.is_authenticated:
|
|
|
|
author = None
|
|
|
|
if isinstance(obj, str):
|
|
|
|
return ModelWrapper(self, apps.get_model('mapdata', obj), author)
|
|
|
|
if isinstance(obj, type) and issubclass(obj, models.Model):
|
|
|
|
return ModelWrapper(self, obj, author)
|
|
|
|
if isinstance(obj, models.Model):
|
|
|
|
return ModelWrapper(self, type(obj), author).create_wrapped_model_class()(self, obj, author)
|
|
|
|
raise ValueError
|
|
|
|
|
|
|
|
"""
|
|
|
|
Parse Changes
|
|
|
|
"""
|
2017-06-20 15:39:22 +02:00
|
|
|
def relevant_changes(self):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Get all changes of this queryset that have not been discarded and do not restore original data.
|
|
|
|
You should not call this, but instead call parse_changes(), it will store the result in self.changes_qs.
|
|
|
|
"""
|
2017-06-20 18:09:35 +02:00
|
|
|
qs = self.changes.filter(discarded_by__isnull=True).exclude(action='restore')
|
|
|
|
qs = qs.exclude(action='delete', created_object_id__isnull=False)
|
|
|
|
return qs
|
2017-06-20 15:39:22 +02:00
|
|
|
|
|
|
|
def parse_changes(self, get_history=False):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Parse changes of this changeset so they can be reflected when querying data.
|
|
|
|
Only executable once, if changes are added later they are automatically parsed.
|
|
|
|
This method gets automatically called when parsed changes are needed or when adding a new change.
|
|
|
|
The queryset used/created by this method can be found in changes_qs afterwards.
|
|
|
|
:param get_history: Whether to get all changes (True) or only relevant ones (False)
|
|
|
|
"""
|
2017-06-20 15:39:22 +02:00
|
|
|
if self.pk is None or self.changes_qs is not None:
|
2017-06-13 16:22:59 +02:00
|
|
|
return
|
2017-06-20 15:39:22 +02:00
|
|
|
|
|
|
|
if get_history:
|
|
|
|
self.changes_qs = self.changes.all()
|
|
|
|
else:
|
2017-06-20 17:56:20 +02:00
|
|
|
self.changes_qs = self.relevant_changes()
|
2017-06-20 15:39:22 +02:00
|
|
|
|
2017-06-20 17:58:24 +02:00
|
|
|
# noinspection PyTypeChecker
|
2017-06-20 15:39:22 +02:00
|
|
|
for change in self.changes_qs:
|
2017-06-13 16:15:28 +02:00
|
|
|
self._parse_change(change)
|
|
|
|
|
|
|
|
def _parse_change(self, change):
|
2017-06-15 17:53:00 +02:00
|
|
|
self._last_change_pk = change.pk
|
|
|
|
|
2017-06-14 00:51:55 +02:00
|
|
|
model = change.model_class
|
|
|
|
if change.action == 'create':
|
2017-06-18 15:30:35 +02:00
|
|
|
new = {}
|
|
|
|
self.created_objects.setdefault(model, {})[change.pk] = new
|
|
|
|
self.ever_created_objects.setdefault(model, {})[change.pk] = new
|
2017-06-16 12:22:36 +02:00
|
|
|
return
|
2017-06-13 16:36:18 +02:00
|
|
|
elif change.action == 'delete':
|
|
|
|
if change.existing_object_pk is not None:
|
2017-06-14 00:51:55 +02:00
|
|
|
self.deleted_existing.setdefault(model, set()).add(change.existing_object_pk)
|
|
|
|
else:
|
2017-06-16 13:00:47 +02:00
|
|
|
self.created_objects[model].pop(change.created_object_id)
|
2017-06-17 23:37:43 +02:00
|
|
|
self.m2m_added.get(model, {}).pop('c'+str(change.created_object_id), None)
|
|
|
|
self.m2m_removed.get(model, {}).pop('c'+str(change.created_object_id), None)
|
2017-06-14 00:51:55 +02:00
|
|
|
return
|
|
|
|
|
2017-06-16 19:19:54 +02:00
|
|
|
pk = change.obj_pk
|
2017-06-14 02:19:37 +02:00
|
|
|
name = change.field_name
|
2017-06-20 12:16:05 +02:00
|
|
|
|
|
|
|
if change.action == 'restore':
|
|
|
|
if change.existing_object_pk is None:
|
|
|
|
self.created_objects[model][change.created_object_id].pop(name, None)
|
|
|
|
else:
|
|
|
|
self.updated_existing.setdefault(model, {}).setdefault(pk, {}).pop(name, None)
|
|
|
|
|
2017-06-14 00:51:55 +02:00
|
|
|
value = json.loads(change.field_value)
|
2017-06-20 12:16:05 +02:00
|
|
|
if change.action == 'update':
|
|
|
|
if change.existing_object_pk is None:
|
|
|
|
self.created_objects[model][change.created_object_id][name] = value
|
|
|
|
else:
|
|
|
|
self.updated_existing.setdefault(model, {}).setdefault(pk, {})[name] = value
|
2017-06-16 19:19:54 +02:00
|
|
|
|
|
|
|
if change.action == 'm2m_add':
|
|
|
|
m2m_removed = self.m2m_removed.get(model, {}).get(pk, {}).get(name, ())
|
|
|
|
if value in m2m_removed:
|
|
|
|
m2m_removed.remove(value)
|
2017-06-17 23:37:43 +02:00
|
|
|
self.m2m_added.setdefault(model, {}).setdefault(pk, {}).setdefault(name, set()).add(value)
|
2017-06-16 19:19:54 +02:00
|
|
|
elif change.action == 'm2m_remove':
|
|
|
|
m2m_added = self.m2m_added.get(model, {}).get(pk, {}).get(name, ())
|
|
|
|
if value in m2m_added:
|
2017-06-17 23:37:43 +02:00
|
|
|
m2m_added.discard(value)
|
|
|
|
self.m2m_removed.setdefault(model, {}).setdefault(pk, {}).setdefault(name, set()).add(value)
|
2017-06-16 19:19:54 +02:00
|
|
|
|
2017-06-21 13:48:13 +02:00
|
|
|
"""
|
|
|
|
Analyse Changes
|
|
|
|
"""
|
|
|
|
def get_objects(self):
|
|
|
|
if self.changes_qs is None:
|
|
|
|
raise TypeError
|
|
|
|
|
|
|
|
# collect pks of relevant objects
|
|
|
|
object_pks = {}
|
|
|
|
for change in self.changes_qs:
|
|
|
|
object_pks.setdefault(change.model_class, set()).add(change.obj_pk)
|
|
|
|
model = None
|
|
|
|
if change.action == 'update':
|
|
|
|
if change.model_class == LocationRedirect:
|
|
|
|
if change.field_name == 'target':
|
|
|
|
object_pks.setdefault(LocationSlug, set()).add(json.loads(change.field_value))
|
|
|
|
continue
|
|
|
|
elif not change.field_name.startswith('title_'):
|
|
|
|
field = change.model_class._meta.get_field(change.field_name)
|
|
|
|
model = getattr(field, 'related_model', None)
|
|
|
|
if change.action in ('m2m_add', 'm2m_remove'):
|
|
|
|
model = change.model_class._meta.get_field(change.field_name).related_model
|
|
|
|
if model is not None:
|
|
|
|
object_pks.setdefault(model, set()).add(json.loads(change.field_value))
|
|
|
|
|
|
|
|
# retrieve relevant objects
|
|
|
|
objects = {}
|
|
|
|
for model, pks in object_pks.items():
|
|
|
|
created_pks = set(pk for pk in pks if is_created_pk(pk))
|
|
|
|
existing_pks = pks - created_pks
|
|
|
|
model_objects = {}
|
|
|
|
if existing_pks:
|
|
|
|
for obj in model.objects.filter(pk__in=existing_pks):
|
|
|
|
if model == LocationSlug:
|
|
|
|
obj = obj.get_child()
|
|
|
|
model_objects[obj.pk] = obj
|
|
|
|
if created_pks:
|
|
|
|
for pk in created_pks:
|
|
|
|
model_objects[pk] = self.get_created_object(model, pk, allow_deleted=True)._obj
|
|
|
|
objects[model] = model_objects
|
|
|
|
|
|
|
|
return objects
|
|
|
|
|
2017-06-21 13:31:41 +02:00
|
|
|
"""
|
|
|
|
Lookup changes and created objects
|
|
|
|
"""
|
2017-06-21 19:13:23 +02:00
|
|
|
def get_changed_values(self, model: models.Model, name: str) -> tuple:
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Get all changes values for a specific field on existing models
|
|
|
|
:param model: model class
|
|
|
|
:param name: field name
|
|
|
|
:return: returns a dictionary with primary keys as keys and new values as values
|
|
|
|
"""
|
2017-06-15 17:53:00 +02:00
|
|
|
r = tuple((pk, values[name]) for pk, values in self.updated_existing.get(model, {}).items() if name in values)
|
|
|
|
return r
|
|
|
|
|
2017-06-18 15:30:35 +02:00
|
|
|
def get_created_object(self, model, pk, author=None, get_foreign_objects=False, allow_deleted=False):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Gets a created model instance.
|
|
|
|
:param model: model class
|
|
|
|
:param pk: primary key
|
|
|
|
:param author: overwrite default author for changes made to that model
|
|
|
|
:param get_foreign_objects: whether to fetch foreign objects and not just set their id to field.attname
|
|
|
|
:param allow_deleted: return created objects that have already been deleted (needs get_history=True)
|
|
|
|
:return: a wrapped model instance
|
|
|
|
"""
|
2017-06-18 01:04:07 +02:00
|
|
|
if is_created_pk(pk):
|
|
|
|
pk = pk[1:]
|
|
|
|
pk = int(pk)
|
2017-06-16 13:00:47 +02:00
|
|
|
self.parse_changes()
|
|
|
|
if issubclass(model, ModelWrapper):
|
|
|
|
model = model._obj
|
2017-06-18 15:30:35 +02:00
|
|
|
|
|
|
|
objects = self.ever_created_objects if allow_deleted else self.created_objects
|
|
|
|
|
|
|
|
objects = tuple(obj for obj in ((submodel, objects.get(submodel, {}).get(pk, None))
|
|
|
|
for submodel in ModelWrapper.get_submodels(model)) if obj[1] is not None)
|
|
|
|
if not objects:
|
|
|
|
raise model.DoesNotExist
|
|
|
|
if len(objects) > 1:
|
|
|
|
raise model.MultipleObjectsReturned
|
|
|
|
|
|
|
|
model, data = objects[0]
|
|
|
|
|
2017-06-16 13:00:47 +02:00
|
|
|
obj = model()
|
2017-06-18 15:30:35 +02:00
|
|
|
obj.pk = 'c' + str(pk)
|
2017-06-18 21:27:51 +02:00
|
|
|
if hasattr(model._meta.pk, 'related_model'):
|
|
|
|
setattr(obj, model._meta.pk.related_model._meta.pk.attname, obj.pk)
|
|
|
|
obj._state.adding = False
|
2017-06-18 15:30:35 +02:00
|
|
|
|
|
|
|
for name, value in data.items():
|
2017-06-16 13:08:14 +02:00
|
|
|
if name.startswith('title_'):
|
2017-06-18 18:09:47 +02:00
|
|
|
if value:
|
|
|
|
obj.titles[name[6:]] = value
|
2017-06-16 13:08:14 +02:00
|
|
|
continue
|
|
|
|
|
2017-06-16 13:00:47 +02:00
|
|
|
class_value = getattr(model, name)
|
2017-06-16 13:08:14 +02:00
|
|
|
|
2017-06-16 13:00:47 +02:00
|
|
|
if isinstance(class_value, ManyToManyDescriptor):
|
|
|
|
continue
|
|
|
|
|
|
|
|
if isinstance(class_value, ForwardManyToOneDescriptor):
|
2017-06-16 16:03:51 +02:00
|
|
|
field = class_value.field
|
|
|
|
setattr(obj, field.attname, value)
|
2017-06-18 23:17:08 +02:00
|
|
|
if is_created_pk(value):
|
|
|
|
setattr(obj, class_value.cache_name, self.get_created_object(field.related_model, value))
|
2017-06-18 18:33:20 +02:00
|
|
|
elif get_foreign_objects:
|
2017-06-16 16:03:51 +02:00
|
|
|
setattr(obj, class_value.cache_name, self.wrap(field.related_model.objects.get(pk=value)))
|
2017-06-16 13:00:47 +02:00
|
|
|
continue
|
|
|
|
|
2017-06-16 13:08:26 +02:00
|
|
|
setattr(obj, name, model._meta.get_field(name).to_python(value))
|
2017-06-16 13:00:47 +02:00
|
|
|
return self.wrap(obj, author=author)
|
|
|
|
|
2017-06-21 18:00:12 +02:00
|
|
|
def get_created_pks(self, model) -> set:
|
|
|
|
"""
|
|
|
|
Returns a set with the primary keys of created objects from this model
|
|
|
|
"""
|
2017-06-16 16:03:51 +02:00
|
|
|
if issubclass(model, ModelWrapper):
|
|
|
|
model = model._obj
|
|
|
|
return set(self.created_objects.get(model, {}).keys())
|
|
|
|
|
2017-06-21 13:31:41 +02:00
|
|
|
"""
|
|
|
|
add changes
|
|
|
|
"""
|
2017-06-12 22:56:39 +02:00
|
|
|
def _new_change(self, author, **kwargs):
|
2017-06-18 22:14:03 +02:00
|
|
|
if self.pk is None:
|
|
|
|
if self.session_id is None:
|
|
|
|
try:
|
|
|
|
# noinspection PyUnresolvedReferences
|
|
|
|
session = self.request.session
|
|
|
|
if session.session_key is None:
|
|
|
|
session.save()
|
|
|
|
self.session_id = session.session_key
|
|
|
|
except AttributeError:
|
|
|
|
pass # ok, so lets keep it this way
|
|
|
|
self.save()
|
2017-06-13 16:22:59 +02:00
|
|
|
self.parse_changes()
|
2017-06-12 22:56:39 +02:00
|
|
|
change = Change(changeset=self)
|
|
|
|
change.changeset_id = self.pk
|
2017-06-13 03:31:10 +02:00
|
|
|
author = self.default_author if author is None else author
|
2017-06-18 18:53:54 +02:00
|
|
|
if author is not None and author.is_authenticated:
|
2017-06-12 22:56:39 +02:00
|
|
|
change.author = author
|
|
|
|
for name, value in kwargs.items():
|
|
|
|
setattr(change, name, value)
|
2017-06-13 03:31:10 +02:00
|
|
|
change.save()
|
2017-06-13 16:25:01 +02:00
|
|
|
self._parse_change(change)
|
2017-06-12 22:56:39 +02:00
|
|
|
return change
|
|
|
|
|
2017-06-13 03:31:10 +02:00
|
|
|
def add_create(self, obj, author=None):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Creates a new object in this changeset. Called when a new ModelInstanceWrapper is saved.
|
|
|
|
"""
|
2017-06-13 03:31:10 +02:00
|
|
|
change = self._new_change(author=author, action='create', model_class=type(obj._obj))
|
|
|
|
obj.pk = 'c%d' % change.pk
|
2017-06-12 22:56:39 +02:00
|
|
|
|
2017-06-13 14:21:01 +02:00
|
|
|
def _add_value(self, action, obj, name, value, author=None):
|
2017-06-20 17:56:20 +02:00
|
|
|
return self._new_change(author=author, action=action, obj=obj, field_name=name,
|
|
|
|
field_value=json.dumps(value, ensure_ascii=False, cls=DjangoJSONEncoder))
|
2017-06-12 22:56:39 +02:00
|
|
|
|
2017-06-13 14:21:01 +02:00
|
|
|
def add_update(self, obj, name, value, author=None):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Update a models field value. Called when a ModelInstanceWrapper is saved.
|
|
|
|
"""
|
2017-06-20 13:36:09 +02:00
|
|
|
with transaction.atomic():
|
|
|
|
change = self._add_value('update', obj, name, value, author)
|
2017-06-20 14:23:00 +02:00
|
|
|
change.other_changes().filter(field_name=name).update(discarded_by=change)
|
2017-06-20 13:36:09 +02:00
|
|
|
return change
|
2017-06-13 14:21:01 +02:00
|
|
|
|
2017-06-20 13:36:09 +02:00
|
|
|
def add_restore(self, obj, name, author=None):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Restore a models field value (= remove it from the changeset).
|
|
|
|
"""
|
2017-06-20 13:36:09 +02:00
|
|
|
with transaction.atomic():
|
|
|
|
change = self._new_change(author=author, action='restore', obj=obj, field_name=name)
|
2017-06-20 14:23:00 +02:00
|
|
|
change.other_changes().filter(field_name=name).update(discarded_by=change)
|
2017-06-20 13:36:09 +02:00
|
|
|
return change
|
2017-06-20 12:16:05 +02:00
|
|
|
|
2017-06-13 14:21:01 +02:00
|
|
|
def add_m2m_add(self, obj, name, value, author=None):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Add an object to a m2m relation. Called by ManyRelatedManagerWrapper.
|
|
|
|
"""
|
2017-06-20 13:36:09 +02:00
|
|
|
with transaction.atomic():
|
|
|
|
change = self._add_value('m2m_add', obj, name, value, author)
|
2017-06-20 14:23:00 +02:00
|
|
|
change.other_changes().filter(field_name=name, field_value=change.field_value).update(discarded_by=change)
|
2017-06-20 13:36:09 +02:00
|
|
|
return change
|
2017-06-13 14:21:01 +02:00
|
|
|
|
|
|
|
def add_m2m_remove(self, obj, name, value, author=None):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Remove an object from a m2m reltation. Called by ManyRelatedManagerWrapper.
|
|
|
|
"""
|
2017-06-20 13:36:09 +02:00
|
|
|
with transaction.atomic():
|
|
|
|
change = self._add_value('m2m_remove', obj, name, value, author)
|
2017-06-20 14:23:00 +02:00
|
|
|
change.other_changes().filter(field_name=name, field_value=change.field_value).update(discarded_by=change)
|
2017-06-20 13:36:09 +02:00
|
|
|
return change
|
2017-06-13 14:21:01 +02:00
|
|
|
|
2017-06-13 03:39:12 +02:00
|
|
|
def add_delete(self, obj, author=None):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Delete an object. Called by ModelInstanceWrapper.delete().
|
|
|
|
"""
|
2017-06-20 13:36:09 +02:00
|
|
|
with transaction.atomic():
|
|
|
|
change = self._new_change(author=author, action='delete', obj=obj)
|
2017-06-20 14:23:00 +02:00
|
|
|
change.other_changes().update(discarded_by=change)
|
2017-06-20 13:36:09 +02:00
|
|
|
return change
|
2017-06-12 22:56:39 +02:00
|
|
|
|
2017-06-21 13:31:41 +02:00
|
|
|
"""
|
|
|
|
Methods for display
|
|
|
|
"""
|
|
|
|
@property
|
|
|
|
def changes_count(self):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Get the number of relevant changes. Does not need a query if changes are already parsed.
|
|
|
|
"""
|
2017-06-21 13:31:41 +02:00
|
|
|
if self.changes_qs is None:
|
|
|
|
return self.relevant_changes().exclude(model_name='LocationRedirect', action='update').count()
|
|
|
|
|
|
|
|
result = 0
|
|
|
|
|
|
|
|
for model, objects in self.created_objects.items():
|
|
|
|
result += len(objects)
|
|
|
|
if model == LocationRedirect:
|
|
|
|
continue
|
|
|
|
result += sum(len(values) for values in objects.values())
|
|
|
|
|
|
|
|
for objects in self.updated_existing.values():
|
|
|
|
result += sum(len(values) for values in objects.values())
|
|
|
|
|
|
|
|
result += sum(len(objs) for objs in self.deleted_existing.values())
|
|
|
|
|
|
|
|
for m2m in self.m2m_added, self.m2m_removed:
|
|
|
|
for objects in m2m.values():
|
|
|
|
for obj in objects.values():
|
|
|
|
result += sum(len(values) for values in obj.values())
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
@property
|
|
|
|
def count_display(self):
|
2017-06-21 18:00:12 +02:00
|
|
|
"""
|
|
|
|
Get “%d changes” display text.
|
|
|
|
"""
|
2017-06-21 13:31:41 +02:00
|
|
|
if self.pk is None:
|
|
|
|
return _('No changes')
|
|
|
|
return ungettext_lazy('%(num)d change', '%(num)d changes', 'num') % {'num': self.changes_count}
|
|
|
|
|
|
|
|
@property
|
|
|
|
def title(self):
|
|
|
|
if self.pk is None:
|
|
|
|
return ''
|
|
|
|
return _('Changeset #%d') % self.pk
|
|
|
|
|
|
|
|
@property
|
|
|
|
def cache_key(self):
|
|
|
|
if self.pk is None:
|
|
|
|
return None
|
|
|
|
return str(self.pk)+'-'+str(self._last_change_pk)
|
|
|
|
|
|
|
|
def get_absolute_url(self):
|
|
|
|
if self.pk is None:
|
|
|
|
return ''
|
|
|
|
return reverse('editor.changesets.detail', kwargs={'pk': self.pk})
|
|
|
|
|
2017-06-16 20:27:07 +02:00
|
|
|
def serialize(self):
|
|
|
|
return OrderedDict((
|
|
|
|
('id', self.pk),
|
|
|
|
('author', self.author_id),
|
|
|
|
('created', None if self.created is None else self.created.isoformat()),
|
|
|
|
('proposed', None if self.proposed is None else self.proposed.isoformat()),
|
|
|
|
('applied', None if self.applied is None else self.applied.isoformat()),
|
|
|
|
('applied_by', None if self.applied_by_id is None else self.applied_by_id),
|
|
|
|
('changes', tuple(change.serialize() for change in self.changes.all())),
|
|
|
|
))
|