diff --git a/taiga/projects/history/migrations/0002_historyentry_is_hidden.py b/taiga/projects/history/migrations/0002_historyentry_is_hidden.py new file mode 100644 index 00000000..37f37530 --- /dev/null +++ b/taiga/projects/history/migrations/0002_historyentry_is_hidden.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('history', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='historyentry', + name='is_hidden', + field=models.BooleanField(default=False), + preserve_default=True, + ), + ] diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index 40cb5448..300213a4 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -48,8 +48,6 @@ class HistoryEntry(models.Model): user = JsonField(blank=True, default=None, null=True) created_at = models.DateTimeField(default=timezone.now) type = models.SmallIntegerField(choices=HISTORY_TYPE_CHOICES) - is_snapshot = models.BooleanField(default=False) - key = models.CharField(max_length=255, null=True, default=None, blank=True) # Stores the last diff @@ -68,9 +66,15 @@ class HistoryEntry(models.Model): delete_comment_date = models.DateTimeField(null=True, blank=True, default=None) delete_comment_user = JsonField(blank=True, default=None, null=True) - @cached_property - def is_comment(self): - return self.type == HistoryType.comment + # Flag for mark some history entries as + # hidden. Hidden history entries are important + # for save but not important to preview. + # Order fields are the good example of this fields. + is_hidden = models.BooleanField(default=False) + + # Flag for mark some history entries as complete + # snapshot. The rest are partial snapshot. + is_snapshot = models.BooleanField(default=False) @cached_property def owner(self): diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 209e21c9..10b63bdf 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -55,6 +55,13 @@ _freeze_impl_map = {} # Dict containing registred containing with their values implementation. _values_impl_map = {} +# Not important fields for models (history entries with only +# this fields are marked as hidden). +_not_important_fields = { + "userstories.userstory": frozenset(["backlog_order", "sprint_order", "kanban_order"]), + "tasks.task": frozenset(["us_order", "taskboard_order"]), +} + log = logging.getLogger("taiga.history") @@ -130,7 +137,30 @@ def freeze_model_instance(obj:object) -> FrozenObj: key = make_key_from_model_object(obj) impl_fn = _freeze_impl_map[typename] - return FrozenObj(key, impl_fn(obj)) + snapshot = impl_fn(obj) + assert isinstance(snapshot, dict), "freeze handlers should return always a dict" + + return FrozenObj(key, snapshot) + + +def is_hidden_snapshot(obj:FrozenDiff) -> bool: + """ + Check if frozen object is considered + hidden or not. + """ + content_type, pk = obj.key.rsplit(":", 1) + snapshot_fields = frozenset(obj.diff.keys()) + + if content_type not in _not_important_fields: + return False + + nfields = _not_important_fields[content_type] + result = snapshot_fields - nfields + + if len(result) == 0: + return True + + return False def make_diff(oldobj:FrozenObj, newobj:FrozenObj) -> FrozenDiff: @@ -235,20 +265,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): else: raise RuntimeError("Unexpected condition") - kwargs = { - "user": {"pk": user_id, "name": user_name}, - "key": key, - "type": entry_type, - "comment": "", - "comment_html": "", - "diff": None, - "values": None, - "snapshot": None, - "is_snapshot": False, - } - fdiff = make_diff(old_fobj, new_fobj) - fvals = make_diff_values(typename, fdiff) # If diff and comment are empty, do # not create empty history entry @@ -258,21 +275,29 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): return None - kwargs.update({ + fvals = make_diff_values(typename, fdiff) + is_hidden = is_hidden_snapshot(fdiff) + + kwargs = { + "user": {"pk": user_id, "name": user_name}, + "key": key, + "type": entry_type, "snapshot": fdiff.snapshot if need_real_snapshot else None, - "is_snapshot": need_real_snapshot, + "diff": fdiff.diff, "values": fvals, "comment": comment, - "diff": fdiff.diff, "comment_html": mdrender(obj.project, comment), - }) + "is_hidden": is_hidden, + "is_snapshot": need_real_snapshot, + } return entry_model.objects.create(**kwargs) # High level query api -def get_history_queryset_by_model_instance(obj:object, types=(HistoryType.change,)): +def get_history_queryset_by_model_instance(obj:object, types=(HistoryType.change,), + include_hidden=False): """ Get one page of history for specified object. """ @@ -280,6 +305,9 @@ def get_history_queryset_by_model_instance(obj:object, types=(HistoryType.change history_entry_model = get_model("history", "HistoryEntry") qs = history_entry_model.objects.filter(key=key, type__in=types) + if not include_hidden: + qs = qs.filter(is_hidden=False) + return qs.order_by("created_at") diff --git a/tests/integration/test_project_history.py b/tests/integration/test_project_history.py index 341153f2..0631d338 100644 --- a/tests/integration/test_project_history.py +++ b/tests/integration/test_project_history.py @@ -51,6 +51,7 @@ def test_take_two_snapshots_with_changes(): qs_all = HistoryEntry.objects.all() qs_created = qs_all.filter(type=HistoryType.create) + qs_hidden = qs_all.filter(is_hidden=True) assert qs_all.count() == 0 @@ -62,6 +63,7 @@ def test_take_two_snapshots_with_changes(): services.take_snapshot(issue, user=issue.owner) assert qs_all.count() == 2 assert qs_created.count() == 1 + assert qs_hidden.count() == 0 def test_take_two_snapshots_without_changes(): @@ -69,6 +71,7 @@ def test_take_two_snapshots_without_changes(): qs_all = HistoryEntry.objects.all() qs_created = qs_all.filter(type=HistoryType.create) + qs_hidden = qs_all.filter(is_hidden=True) assert qs_all.count() == 0 @@ -79,7 +82,7 @@ def test_take_two_snapshots_without_changes(): assert qs_all.count() == 1 assert qs_created.count() == 1 - + assert qs_hidden.count() == 0 def test_take_snapshot_from_deleted_object(): issue = f.IssueFactory.create() @@ -174,3 +177,23 @@ def test_issue_resource_history_test(client): assert qs_created.count() == 1 assert qs_changed.count() == 0 assert qs_deleted.count() == 1 + + +def test_take_hidden_snapshot(): + task = f.TaskFactory.create() + + qs_all = HistoryEntry.objects.all() + qs_hidden = qs_all.filter(is_hidden=True) + + assert qs_all.count() == 0 + + # Two snapshots with modification should + # generate two snapshots. + services.take_snapshot(task, user=task.owner) + task.us_order = 3 + task.save() + + services.take_snapshot(task, user=task.owner) + assert qs_all.count() == 2 + assert qs_hidden.count() == 1 +