diff --git a/aleksis/core/migrations/0005_timestamped_activity_notification.py b/aleksis/core/migrations/0005_timestamped_activity_notification.py new file mode 100644 index 0000000000000000000000000000000000000000..611c55196dc8c703662380e7c73ca0ae20a42059 --- /dev/null +++ b/aleksis/core/migrations/0005_timestamped_activity_notification.py @@ -0,0 +1,35 @@ +# Generated by Django 3.1.3 on 2020-11-16 13:04 + +from django.db import migrations, models +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_add_permissions_for_group_stats'), + ] + + operations = [ + migrations.AddField( + model_name='activity', + name='created', + field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), + ), + migrations.AddField( + model_name='activity', + name='modified', + field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), + ), + migrations.AddField( + model_name='notification', + name='created', + field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), + ), + migrations.AddField( + model_name='notification', + name='modified', + field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), + ), + ] diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py index e3f36630b169e912ceea50e8b759f4f078459b36..e44caa020bdb6fb1814a3b7e10de95d72915b26b 100644 --- a/aleksis/core/mixins.py +++ b/aleksis/core/mixins.py @@ -19,7 +19,6 @@ from django.views.generic import CreateView, UpdateView from django.views.generic.edit import DeleteView, ModelFormMixin import reversion -from easyaudit.models import CRUDEvent from guardian.admin import GuardedModelAdmin from jsonstore.fields import IntegerField, JSONFieldMixin from material.base import Layout, LayoutNode @@ -54,8 +53,7 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): This base model ensures all objects in AlekSIS apps fulfill the following properties: - * crud_events property to retrieve easyaudit's CRUD event log - * created_at and updated_at properties based n CRUD events + * `versions` property to retrieve all versions of the model from reversion * Allow injection of fields and code from AlekSIS apps to extend model functionality. @@ -109,50 +107,30 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): pass @property - def crud_events(self) -> QuerySet: - """Get all CRUD events connected to this object from easyaudit.""" - content_type = ContentType.objects.get_for_model(self) + def versions(self) -> List[Tuple[str, Tuple[Any, Any]]]: + """Get all versions of this object from django-reversion. - return CRUDEvent.objects.filter( - object_id=self.pk, content_type=content_type - ).select_related("user", "user__person") + Includes diffs to previous version. + """ + versions = reversion.models.Version.objects.get_for_object(self) - @property - def crud_event_create(self) -> Optional[CRUDEvent]: - """Return create event of this object.""" - return self.crud_events.filter(event_type=CRUDEvent.CREATE).latest("datetime") + versions_with_changes = [] + for i, version in enumerate(versions): + diff = {} + if i > 0: + prev_version = versions[i - 1] - @property - def crud_event_update(self) -> Optional[CRUDEvent]: - """Return last event of this object.""" - return self.crud_events.latest("datetime") + for k, val in version.field_dict.items(): + prev_val = prev_version.field_dict.get(k, None) + if prev_val != val: + diff[k] = (prev_val, val) - @property - def created_at(self) -> Optional[datetime]: - """Determine creation timestamp from CRUD log.""" - if self.crud_event_create: - return self.crud_event_create.datetime + versions_with_changes.append((version, diff)) - @property - def updated_at(self) -> Optional[datetime]: - """Determine last timestamp from CRUD log.""" - if self.crud_event_update: - return self.crud_event_update.datetime + return versions_with_changes extended_data = JSONField(default=dict, editable=False) - @property - def created_by(self) -> Optional[models.Model]: - """Determine user who created this object from CRUD log.""" - if self.crud_event_create: - return self.crud_event_create.user - - @property - def updated_by(self) -> Optional[models.Model]: - """Determine user who last updated this object from CRUD log.""" - if self.crud_event_update: - return self.crud_event_update.user - extended_data = JSONField(default=dict, editable=False) @classmethod diff --git a/aleksis/core/models.py b/aleksis/core/models.py index 78bda3bd1068fd441f1fbd50f4bd133e95f0a8dc..c99820e5123f1a2d114c41533a9ee9a6628827d3 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -21,6 +21,7 @@ from django.utils.translation import gettext_lazy as _ import jsonstore from cache_memoize import cache_memoize from dynamic_preferences.models import PerInstancePreferenceModel +from model_utils.models import TimeStampedModel from phonenumber_field.modelfields import PhoneNumberField from polymorphic.models import PolymorphicModel @@ -440,7 +441,7 @@ class PersonGroupThrough(ExtensibleModel): setattr(self, field_name, field_instance) -class Activity(ExtensibleModel): +class Activity(ExtensibleModel, TimeStampedModel): """Activity of a user to trace some actions done in AlekSIS in displayable form.""" user = models.ForeignKey( @@ -460,7 +461,7 @@ class Activity(ExtensibleModel): verbose_name_plural = _("Activities") -class Notification(ExtensibleModel): +class Notification(ExtensibleModel, TimeStampedModel): """Notification to submit to a user.""" sender = models.CharField(max_length=100, verbose_name=_("Sender")) diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index a04471a0084c16919a0395c2df4ff25cc46aea9f..cd92f28e4a3ef71bcb6220711db196452641d3cc 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -64,7 +64,6 @@ INSTALLED_APPS = [ "dbbackup", "settings_context_processor", "sass_processor", - "easyaudit", "django_any_js", "django_yarnpkg", "django_tables2", @@ -129,7 +128,6 @@ MIDDLEWARE = [ "impersonate.middleware.ImpersonateMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "easyaudit.middleware.easyaudit.EasyAuditMiddleware", "maintenance_mode.middleware.MaintenanceModeMiddleware", "aleksis.core.util.middlewares.EnsurePersonMiddleware", "django_prometheus.middleware.PrometheusAfterMiddleware", diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html index 683adca900d46a8ba50790829db4d3ecd0ba2716..bcf0bf1b4180756d53f1519b8ee2371ee3e4e658 100644 --- a/aleksis/core/templates/core/index.html +++ b/aleksis/core/templates/core/index.html @@ -48,7 +48,7 @@ <span class="badge new primary-color">{{ activity.app }}</span> <span class="title">{{ activity.title }}</span> <p> - <i class="material-icons left">access_time</i> {{ activity.created_at }} + <i class="material-icons left">access_time</i> {{ activity.created }} </p> <p> {{ activity.description }} @@ -71,7 +71,7 @@ <span class="badge new primary-color">{{ notification.app }}</span> <span class="title">{{ notification.title }}</span> <p> - <i class="material-icons left">access_time</i> {{ notification.created_at }} + <i class="material-icons left">access_time</i> {{ notification.created }} </p> <p> {{ notification.description }} diff --git a/aleksis/core/templates/core/partials/crud_events.html b/aleksis/core/templates/core/partials/crud_events.html index 50e4f73cab492804b163b7ddfe0dda08b9e267b7..b0edf14d154690e33a2fac523f5fa38429fcdb5e 100644 --- a/aleksis/core/templates/core/partials/crud_events.html +++ b/aleksis/core/templates/core/partials/crud_events.html @@ -1,62 +1,32 @@ {% load i18n data_helpers %} -<ul class="collection"> - {% for event in obj.crud_events %} - {% if no_m2m and event.event_type == event.M2M_CHANGE or event.event_type == event.M2M_CHANGE_REV %} - {% else %} - <li class="collection-item"> - <strong> - {% if event.event_type == event.CREATE %} - {% blocktrans with person=event.user.person %} - Created by {{ person }} - {% endblocktrans %} - {% elif event.event_type == event.UPDATE %} - {% blocktrans with person=event.user.person %} - Updated by {{ person }} - {% endblocktrans %} - {% elif event.event_type == event.DELETE %} - {% blocktrans with person=event.user.person %} - Deleted by {{ person }} - {% endblocktrans %} - {% elif event.event_type == event.M2M_CHANGE %} - {% blocktrans with person=event.user.person %} - Updated by {{ person }} - {% endblocktrans %} - {% elif event.event_type == event.M2M_CHANGE_REV %} - {% blocktrans with person=event.user.person %} - Updated by {{ person }} - {% endblocktrans %} - {% endif %} - </strong> - - <div class="left" style="margin-right: 10px;"> - {% if event.event_type == event.CREATE %} - <i class="material-icons">add_circle</i> - {% elif event.event_type == event.UPDATE %} - <i class="material-icons">edit</i> - {% elif event.event_type == event.DELETE %} - <i class="material-icons">delete</i> - {% elif event.event_type == event.M2M_CHANGE %} - <i class="material-icons">edit</i> - {% elif event.event_type == event.M2M_CHANGE_REV %} - <i class="material-icons">edit</i> - {% endif %} - </div> - <div class="right"> - {{ event.datetime }} - </div> - {% parse_json event.changed_fields as changed_fields %} - {% if changed_fields %} - <ul> - {% for field, change in changed_fields.items %} - {% verbose_name event.content_type.app_label event.content_type.model field as verbose_name %} - <li> - {{ verbose_name }}: <s>{{ change.0 }}</s> → {{ change.1 }} - </li> - {% endfor %} - </ul> +<div class="collection"> + {% for version in obj.versions %} + <div class="collection-item"> + <div class="left" style="margin-right: 10px;"> + {% if forloop.first %} + <i class="material-icons">add_circle</i> + {% else %} + <i class="material-icons">edit</i> {% endif %} - </li> - {% endif %} + </div> + <strong> + {{ version.0.revision.get_comment }} + {% trans "Changed by" %} {% firstof version.0.revision.user.person _("Unknown") %} + </strong> + <div class="right"> + {{ version.0.revision.date_created }} + </div> + {% if version.1 %} + <ul> + {% for field, change in version.1.items %} + {% verbose_name version.0.content_type.app_label version.0.content_type.model field as verbose_name %} + <li> + {{ verbose_name }}: <s>{{ change.0 }}</s> → {{ change.1 }} + </li> + {% endfor %} + </ul> + {% endif %} + </div> {% endfor %} -</ul> +</div> diff --git a/aleksis/core/templates/templated_email/notification.email b/aleksis/core/templates/templated_email/notification.email index 8e61fd0df28b5bc6342c891921831e147e9cc8d9..0e5d9bdee3c8bd9ec3df5c9d008ae68d1f274c23 100644 --- a/aleksis/core/templates/templated_email/notification.email +++ b/aleksis/core/templates/templated_email/notification.email @@ -15,7 +15,7 @@ {% trans "More information" %} → {{ notification.link }} {% endif %} - {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created_at %} + {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created %} Sent by {{ trans_sender }} at {{ trans_created_at }} {% endblocktrans %} @@ -37,7 +37,7 @@ </blockquote> <p> - {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created_at %} + {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created %} Sent by {{ trans_sender }} at {{ trans_created_at }} {% endblocktrans %} </p> diff --git a/aleksis/core/views.py b/aleksis/core/views.py index fc1b0ecb8fdc577d9edb951afe3b25bab1cf9a54..c6df78ecb19284975d339ddedd5439f7515bd7ad 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -17,6 +17,7 @@ from haystack.inputs import AutoQuery from haystack.query import SearchQuerySet from haystack.views import SearchView from health_check.views import MainView +from reversion import set_user from rules.contrib.views import PermissionRequiredMixin, permission_required from .filters import GroupFilter, PersonFilter @@ -308,6 +309,7 @@ def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse if request.method == "POST": if edit_person_form.is_valid(): with reversion.create_revision(): + set_user(request.user) edit_person_form.save(commit=True) messages.success(request, _("The person has been saved.")) @@ -344,6 +346,7 @@ def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: if request.method == "POST": if edit_group_form.is_valid(): with reversion.create_revision(): + set_user(request.user) group = edit_group_form.save(commit=True) messages.success(request, _("The group has been saved.")) @@ -543,6 +546,7 @@ def delete_person(request: HttpRequest, id_: int) -> HttpResponse: person = objectgetter_optional(Person)(request, id_) with reversion.create_revision(): + set_user(request.user) person.save() person.delete() @@ -556,6 +560,7 @@ def delete_group(request: HttpRequest, id_: int) -> HttpResponse: """View to delete an group.""" group = objectgetter_optional(Group)(request, id_) with reversion.create_revision(): + set_user(request.user) group.save() group.delete() diff --git a/poetry.lock b/poetry.lock index 94c890d7af8bd0f1b95e28052d208fd621ffdc6d..bf84fb1291e9e0661d08cbbb2629bb2275d0f0b0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -122,21 +122,6 @@ PyYAML = ">=3.13" six = ">=1.10.0" stevedore = ">=1.20.0" -[[package]] -name = "beautifulsoup4" -version = "4.9.3" -description = "Screen-scraping library" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""} - -[package.extras] -html5lib = ["html5lib"] -lxml = ["lxml"] - [[package]] name = "billiard" version = "3.6.3.0" @@ -524,18 +509,6 @@ django = ">=1.11" persisting-theory = ">=0.2.1" six = "*" -[[package]] -name = "django-easy-audit" -version = "1.3.1a1" -description = "Yet another Django audit log app, hopefully the simplest one." -category = "main" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -beautifulsoup4 = "*" -django = ">=2.2,<3.2" - [[package]] name = "django-favicon-plus-reloaded" version = "1.0.4" @@ -703,6 +676,17 @@ python-versions = "*" [package.dependencies] django = "*" +[[package]] +name = "django-model-utils" +version = "4.0.0" +description = "Django model mixins and utilities" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Django = ">=2.0.1" + [[package]] name = "django-otp" version = "1.0.2" @@ -1808,14 +1792,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "soupsieve" -version = "2.0.1" -description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" -optional = false -python-versions = ">=3.5" - [[package]] name = "spdx-license-list" version = "0.5.1" @@ -2170,11 +2146,6 @@ bandit = [ {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"}, ] -beautifulsoup4 = [ - {file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"}, - {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"}, - {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"}, -] billiard = [ {file = "billiard-3.6.3.0-py3-none-any.whl", hash = "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede"}, {file = "billiard-3.6.3.0.tar.gz", hash = "sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a"}, @@ -2327,10 +2298,6 @@ django-dynamic-preferences = [ {file = "django-dynamic-preferences-1.10.1.tar.gz", hash = "sha256:e4b2bb7b2563c5064ba56dd76441c77e06b850ff1466a386a1cd308909a6c7de"}, {file = "django_dynamic_preferences-1.10.1-py2.py3-none-any.whl", hash = "sha256:9419fa925fd2cbb665269ae72059eb3058bf080913d853419b827e4e7a141902"}, ] -django-easy-audit = [ - {file = "django-easy-audit-1.3.1a1.tar.gz", hash = "sha256:1aaa7f19a5a6d7f31698661b061e662df50d2506e0828a1cfb681a95c3b34fea"}, - {file = "django_easy_audit-1.3.1a1-py3-none-any.whl", hash = "sha256:64448dce510673939825b6d5dec674f6c2ac069ab4b4b95cff7f3f796da7c786"}, -] django-favicon-plus-reloaded = [ {file = "django-favicon-plus-reloaded-1.0.4.tar.gz", hash = "sha256:90c761c636a338e6e9fb1d086649d82095085f92cff816c9cf074607f28c85a5"}, {file = "django_favicon_plus_reloaded-1.0.4-py3-none-any.whl", hash = "sha256:26e4316d41328a61ced52c7fc0ead795f0eb194d6a30311c34a9833c6fe30a7c"}, @@ -2390,6 +2357,10 @@ django-menu-generator = [ django-middleware-global-request = [ {file = "django-middleware-global-request-0.1.2.tar.gz", hash = "sha256:f6490759bc9f7dbde4001709554e29ca715daf847f2222914b4e47117dca9313"}, ] +django-model-utils = [ + {file = "django-model-utils-4.0.0.tar.gz", hash = "sha256:adf09e5be15122a7f4e372cb5a6dd512bbf8d78a23a90770ad0983ee9d909061"}, + {file = "django_model_utils-4.0.0-py2.py3-none-any.whl", hash = "sha256:9cf882e5b604421b62dbe57ad2b18464dc9c8f963fc3f9831badccae66c1139c"}, +] django-otp = [ {file = "django-otp-1.0.2.tar.gz", hash = "sha256:f523fb9dec420f28a29d3e2ad72ac06f64588956ed4f2b5b430d8e957ebb8287"}, {file = "django_otp-1.0.2-py3-none-any.whl", hash = "sha256:8ba5ab9bd2738c7321376c349d7cce49cf4404e79f6804e0a3cc462a91728e18"}, @@ -2966,10 +2937,6 @@ snowballstemmer = [ {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, ] -soupsieve = [ - {file = "soupsieve-2.0.1-py3-none-any.whl", hash = "sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55"}, - {file = "soupsieve-2.0.1.tar.gz", hash = "sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232"}, -] spdx-license-list = [ {file = "spdx_license_list-0.5.1-py3-none-any.whl", hash = "sha256:32f1401e0077b46ba8b3d9c648b6503ef1d49c41aab51aa13816be2dde3b4a13"}, {file = "spdx_license_list-0.5.1.tar.gz", hash = "sha256:64cb5de37724c64cdeccafa2ae68667ff8ccdb7b688f51c1c2be82d7ebe3a112"}, diff --git a/pyproject.toml b/pyproject.toml index 866a9f3c9e700be11c9e5013537e58c48dfc1d6f..f600af4628071fcbb4b9cbc728e213d70724f4e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,6 @@ python = "^3.7" Django = "^3.0" django-any-js = "^1.0" django-debug-toolbar = "^2.0" -django-easy-audit = {version ="^1.2rc1", allow-prereleases = true} django-middleware-global-request = "^0.1.2" django-menu-generator = "^1.0.4" django-tables2 = "^2.1" @@ -92,6 +91,7 @@ psutil = "^5.7.0" celery-progress = "^0.0.14" django-prometheus = "^2.1.0" importlib-metadata = {version = "^2.0.0", python = "<3.9"} +django-model-utils = "^4.0.0" [tool.poetry.extras] ldap = ["django-auth-ldap"]