Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ jobs:
pip install -e .
pip install -U -r dev-requirements.txt
pip install -U Django~=${{ matrix.django_version }}
- name: isort
run: |
isort tracking_fields --profile black --skip migrations
- name: Lint with black
run: |
black tracking_fields --exclude "migrations"
- name: Lint with flake8
run: |
flake8 --ignore=E501,W504 tracking_fields
Expand Down
1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ flake8-tidy-imports
pycodestyle
coveralls
argparse
black
# Sort and lint imports
isort
118 changes: 65 additions & 53 deletions tracking_fields/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,126 +6,139 @@
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _

from tracking_fields.models import TrackingEvent, TrackedFieldModification
from tracking_fields.models import TrackedFieldModification, TrackingEvent


class TrackedObjectMixinAdmin(admin.ModelAdmin):
"""
Use this mixin to add a "Tracking" button
next to history one on tracked object
"""

class Meta:
abstract = True
change_form_template = 'tracking_fields/admin/change_form_object.html'

def change_view(self, request, object_id, form_url='', extra_context=None):
change_form_template = "tracking_fields/admin/change_form_object.html"

def change_view(self, request, object_id, form_url="", extra_context=None):
extra_context = extra_context or {}
if object_id is not None:
extra_context['tracking_opts'] = TrackingEvent._meta
extra_context["tracking_opts"] = TrackingEvent._meta
opts = self.model._meta
content_type = ContentType.objects.get(
app_label=opts.app_label,
model=opts.model_name,
)
extra_context['tracking_value'] = quote(u'{0}:{1}'.format(
content_type.pk, object_id
))
extra_context["tracking_value"] = quote(
"{0}:{1}".format(content_type.pk, object_id)
)
return super(TrackedObjectMixinAdmin, self).change_view(
request, object_id, form_url, extra_context
)


class TrackerEventListFilter(admin.SimpleListFilter):
""" Hidden filter used to get history of a particular object. """
"""Hidden filter used to get history of a particular object."""

title = _("Object")
parameter_name = 'object'
template = 'tracking_fields/admin/filter.html' # Empty template
parameter_name = "object"
template = "tracking_fields/admin/filter.html" # Empty template

def lookups(self, request, model_admin):
qs = model_admin.get_queryset(request)
objects = qs.values('object_content_type', 'object_id',)
objects = qs.values(
"object_content_type",
"object_id",
)
lookups = {}
for obj in objects:
value = u'{0}:{1}'.format(
obj['object_content_type'], obj['object_id']
)
value = "{0}:{1}".format(obj["object_content_type"], obj["object_id"])
lookups[value] = value
return [(lookup[0], lookup[1]) for lookup in lookups.items()]

def queryset(self, request, queryset):
if self.value() is None:
return queryset
value = self.value().split(':')
return queryset.filter(
object_content_type_id=value[0],
object_id=value[1]
)
value = self.value().split(":")
return queryset.filter(object_content_type_id=value[0], object_id=value[1])


class TrackerEventUserFilter(admin.SimpleListFilter):
""" Filter on users. """
"""Filter on users."""

title = _("User")
parameter_name = 'user'
parameter_name = "user"

def lookups(self, request, model_admin):
qs = model_admin.get_queryset(request)
users = qs.values('user_content_type', 'user_id',)
users = qs.values(
"user_content_type",
"user_id",
)
lookups = {}
for user in users:
if user['user_content_type'] is None:
if user["user_content_type"] is None:
continue
value = u'{0}:{1}'.format(
user['user_content_type'], user['user_id']
)
value = "{0}:{1}".format(user["user_content_type"], user["user_id"])
try:
user_obj = (
ContentType.objects.get_for_id(user['user_content_type'])
.get_object_for_this_type(pk=user['user_id'])
)
lookups[value] = getattr(user_obj, 'username', str(user_obj))
user_obj = ContentType.objects.get_for_id(
user["user_content_type"]
).get_object_for_this_type(pk=user["user_id"])
lookups[value] = getattr(user_obj, "username", str(user_obj))
except ObjectDoesNotExist:
lookups[value] = f"<id={user['user_id']}>"
return [(lookup[0], lookup[1]) for lookup in lookups.items()]

def queryset(self, request, queryset):
if self.value() is None:
return queryset
value = self.value().split(':')
value = self.value().split(":")
if len(value) == 2:
return queryset.filter(
user_content_type_id=value[0],
user_id=value[1]
)
return queryset.filter(user_content_type_id=value[0], user_id=value[1])
return queryset


class TrackedFieldModificationAdmin(admin.TabularInline):
can_delete = False
model = TrackedFieldModification
readonly_fields = ('field', 'old_value', 'new_value',)
readonly_fields = (
"field",
"old_value",
"new_value",
)

def has_add_permission(self, request, obj=None):
return False


class TrackingEventAdmin(admin.ModelAdmin):
date_hierarchy = 'date'
list_display = ('date', 'action', 'object', 'object_repr')
list_filter = ('action', TrackerEventUserFilter, TrackerEventListFilter,)
search_fields = ('object_repr', 'user_repr',)
readonly_fields = (
'date', 'action', 'object', 'object_repr', 'user', 'user_repr',
date_hierarchy = "date"
list_display = ("date", "action", "object", "object_repr")
list_filter = (
"action",
TrackerEventUserFilter,
TrackerEventListFilter,
)
inlines = (
TrackedFieldModificationAdmin,
search_fields = (
"object_repr",
"user_repr",
)
readonly_fields = (
"date",
"action",
"object",
"object_repr",
"user",
"user_repr",
)
change_list_template = 'tracking_fields/admin/change_list_event.html'
inlines = (TrackedFieldModificationAdmin,)
change_list_template = "tracking_fields/admin/change_list_event.html"

def changelist_view(self, request, extra_context=None):
""" Get object currently tracked and add a button to get back to it """
"""Get object currently tracked and add a button to get back to it"""
extra_context = extra_context or {}
if 'object' in request.GET.keys():
value = request.GET['object'].split(':')
if "object" in request.GET.keys():
value = request.GET["object"].split(":")
content_type = get_object_or_404(
ContentType,
id=value[0],
Expand All @@ -134,10 +147,9 @@ def changelist_view(self, request, extra_context=None):
content_type.model_class(),
id=value[1],
)
extra_context['tracked_object'] = tracked_object
extra_context['tracked_object_opts'] = tracked_object._meta
return super(TrackingEventAdmin, self).changelist_view(
request, extra_context)
extra_context["tracked_object"] = tracked_object
extra_context["tracked_object_opts"] = tracked_object._meta
return super(TrackingEventAdmin, self).changelist_view(request, extra_context)


admin.site.register(TrackingEvent, TrackingEventAdmin)
67 changes: 33 additions & 34 deletions tracking_fields/decorators.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from __future__ import unicode_literals

from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.db.models import ManyToManyField
from django.db.models.signals import (
post_init, post_save, pre_delete, m2m_changed
)
from django.db.models.signals import m2m_changed, post_init, post_save, pre_delete
from django.urls import reverse

from tracking_fields.tracking import (
tracking_init, tracking_save, tracking_delete, tracking_m2m
tracking_delete,
tracking_init,
tracking_m2m,
tracking_save,
)


Expand All @@ -33,16 +34,16 @@ def _add_signals_to_cls(cls):


def _track_class_related_field(cls, field):
""" Track a field on a related model """
"""Track a field on a related model"""
# field = field on current model
# related_field = field on related model
(field, related_field) = field.split('__', 1)
(field, related_field) = field.split("__", 1)
field_obj = cls._meta.get_field(field)
related_cls = field_obj.remote_field.model
related_name = field_obj.remote_field.get_accessor_name()

if not hasattr(related_cls, '_tracked_related_fields'):
setattr(related_cls, '_tracked_related_fields', {})
if not hasattr(related_cls, "_tracked_related_fields"):
setattr(related_cls, "_tracked_related_fields", {})
if related_field not in related_cls._tracked_related_fields.keys():
related_cls._tracked_related_fields[related_field] = []

Expand All @@ -58,9 +59,7 @@ def _track_class_related_field(cls, field):
# ...
# }

related_cls._tracked_related_fields[related_field].append(
(field, related_name)
)
related_cls._tracked_related_fields[related_field].append((field, related_name))
_add_signals_to_cls(related_cls)
# Detect m2m fields changes
if isinstance(related_cls._meta.get_field(related_field), ManyToManyField):
Expand All @@ -72,8 +71,8 @@ def _track_class_related_field(cls, field):


def _track_class_field(cls, field):
""" Track a field on the current model """
if '__' in field:
"""Track a field on the current model"""
if "__" in field:
_track_class_related_field(cls, field)
return
# Will raise FieldDoesNotExist if there is an error
Expand All @@ -88,9 +87,9 @@ def _track_class_field(cls, field):


def _track_class(cls, fields):
""" Track fields on the specified model """
"""Track fields on the specified model"""
# Small tests to ensure everything is all right
assert not getattr(cls, '_is_tracked', False)
assert not getattr(cls, "_is_tracked", False)

for field in fields:
_track_class_field(cls, field)
Expand All @@ -101,37 +100,37 @@ def _track_class(cls, fields):
cls._is_tracked = True
# Do not directly track related fields (tracked on related model)
# or m2m fields (tracked by another signal)
cls._tracked_fields = [
field for field in fields
if '__' not in field
]
cls._tracked_fields = [field for field in fields if "__" not in field]


def _add_get_tracking_url(cls):
""" Add a method to get the tracking url of an object. """
"""Add a method to get the tracking url of an object."""

def get_tracking_url(self):
""" return url to tracking view in admin panel """
url = reverse('admin:tracking_fields_trackingevent_changelist')
object_id = '{0}%3A{1}'.format(
ContentType.objects.get_for_model(self).pk,
self.pk
"""return url to tracking view in admin panel"""
url = reverse("admin:tracking_fields_trackingevent_changelist")
object_id = "{0}%3A{1}".format(
ContentType.objects.get_for_model(self).pk, self.pk
)
return '{0}?object={1}'.format(url, object_id)
if not hasattr(cls, 'get_tracking_url'):
setattr(cls, 'get_tracking_url', get_tracking_url)
return "{0}?object={1}".format(url, object_id)

if not hasattr(cls, "get_tracking_url"):
setattr(cls, "get_tracking_url", get_tracking_url)


def track(*fields):
"""
Decorator used to track changes on Model's fields.
Decorator used to track changes on Model's fields.

:Example:
>>> @track('name')
... class Human(models.Model):
... name = models.CharField(max_length=30)
:Example:
>>> @track('name')
... class Human(models.Model):
... name = models.CharField(max_length=30)
"""

def inner(cls):
_track_class(cls, fields)
_add_get_tracking_url(cls)
return cls

return inner
Loading