diff --git a/apiserver/apiserver/api/models.py b/apiserver/apiserver/api/models.py index 5508a91..5142399 100644 --- a/apiserver/apiserver/api/models.py +++ b/apiserver/apiserver/api/models.py @@ -131,10 +131,11 @@ class MetaInfo(models.Model): class HistoryIndex(models.Model): content_type = models.ForeignKey(ContentType, null=True, on_delete=models.SET_NULL) object_id = models.PositiveIntegerField() - content_object = GenericForeignKey('content_type', 'object_id') + history = GenericForeignKey('content_type', 'object_id') owner_id = models.PositiveIntegerField() owner_name = models.TextField() + object_name = models.TextField() history_user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) history_date = models.DateTimeField() history_type = models.TextField() diff --git a/apiserver/apiserver/api/permissions.py b/apiserver/apiserver/api/permissions.py index 452c865..e33ed4a 100644 --- a/apiserver/apiserver/api/permissions.py +++ b/apiserver/apiserver/api/permissions.py @@ -41,6 +41,13 @@ class ReadOnly(BasePermission): def has_object_permission(self, request, view, obj): return bool(request.method in SAFE_METHODS) +class IsAdmin(BasePermission): + def has_permission(self, request, view): + return bool( + request.user + and is_admin_director(request.user) + ) + class IsAdminOrReadOnly(BasePermission): def has_permission(self, request, view): return bool( diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py index 0ba38ba..ba08126 100644 --- a/apiserver/apiserver/api/serializers.py +++ b/apiserver/apiserver/api/serializers.py @@ -396,3 +396,17 @@ class MyPasswordChangeSerializer(PasswordChangeSerializer): raise ValidationError(dict(non_field_errors='Problem connecting to LDAP server: set.')) super().save() + + +class HistoryChangeSerializer(serializers.ModelSerializer): + class Meta: + model = models.HistoryChange + fields = ['field', 'old', 'new'] + +class HistorySerializer(serializers.ModelSerializer): + changes = HistoryChangeSerializer(many=True) + history_user = serializers.StringRelatedField() + + class Meta: + model = models.HistoryIndex + fields = '__all__' diff --git a/apiserver/apiserver/api/signals.py b/apiserver/apiserver/api/signals.py index 11f9ad1..54087f0 100644 --- a/apiserver/apiserver/api/signals.py +++ b/apiserver/apiserver/api/signals.py @@ -10,6 +10,9 @@ from .permissions import is_admin_director def get_object_owner(obj): full_name = lambda member: member.first_name + ' ' + member.last_name + if obj.__class__.__name__ == 'Member': + return full_name(obj), obj.id + if getattr(obj, 'user', False): return full_name(obj.user.member), obj.user.member.id @@ -35,27 +38,45 @@ def post_create_historical_record_callback( using, **kwargs): - changes = history_instance.diff_against(history_instance.prev_record).changes + history_type = history_instance.get_history_type_display() + object_name = instance.__class__.__name__ - if changes: + if object_name in ['User']: return + + if history_type == 'Changed': + changes = history_instance.diff_against(history_instance.prev_record).changes + else: + changes = [] + + # it's possible for changes to be empty if model saved with no diff + if changes or history_type in ['Created', 'Deleted']: owner = get_object_owner(instance) index = models.HistoryIndex.objects.create( - content_object=history_instance, - owner_name=owner[0], + history=history_instance, owner_id=owner[1], + owner_name=owner[0], + object_name=object_name, history_user=history_user, history_date=history_instance.history_date, - history_type=history_instance.get_history_type_display(), + history_type=history_type, revert_url=history_instance.revert_url(), is_system=bool(history_user == None), is_admin=is_admin_director(history_user), ) + change_old = change.old or '' + change_new = change.new or '' + + if len(change_old) > 200: + change_old = change_old[:200] + '... [truncated]' + if len(change_new) > 200: + change_new = change_new[:200] + '... [truncated]' + for change in changes: models.HistoryChange.objects.create( index=index, field=change.field, - old=change.old, - new=change.new, + old=change_old, + new=change_new, ) diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py index f86baaa..08bd7a6 100644 --- a/apiserver/apiserver/api/views.py +++ b/apiserver/apiserver/api/views.py @@ -23,6 +23,7 @@ from .permissions import ( IsObjOwnerOrAdmin, IsSessionInstructorOrAdmin, ReadOnly, + IsAdmin, IsAdminOrReadOnly, IsInstructorOrReadOnly ) @@ -413,6 +414,12 @@ class PasteView(views.APIView): raise exceptions.ValidationError(dict(paste='This field is required.')) +class HistoryViewSet(Base, List, Retrieve): + permission_classes = [AllowMetadata | IsAdmin] + serializer_class = serializers.HistorySerializer + queryset = models.HistoryIndex.objects.order_by('-history_date') + + class RegistrationView(RegisterView): serializer_class = serializers.MyRegisterSerializer diff --git a/apiserver/apiserver/urls.py b/apiserver/apiserver/urls.py index 3581d63..4407a03 100644 --- a/apiserver/apiserver/urls.py +++ b/apiserver/apiserver/urls.py @@ -15,6 +15,7 @@ router.register(r'stats', views.StatsViewSet, basename='stats') router.register(r'search', views.SearchViewSet, basename='search') router.register(r'members', views.MemberViewSet, basename='members') router.register(r'courses', views.CourseViewSet, basename='course') +router.register(r'history', views.HistoryViewSet, basename='history') router.register(r'sessions', views.SessionViewSet, basename='session') router.register(r'training', views.TrainingViewSet, basename='training') router.register(r'transactions', views.TransactionViewSet, basename='transaction')