From eb7b189970b11407f420e317186f3305885f9890 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Aug 2017 04:12:52 -0400 Subject: [PATCH] Add generic error log model. Signed-off-by: Roberto Rosario --- mayan/apps/common/admin.py | 9 +- mayan/apps/common/apps.py | 20 +++ mayan/apps/common/classes.py | 135 ++++++++++-------- mayan/apps/common/links.py | 30 ++++ mayan/apps/common/managers.py | 14 ++ .../common/migrations/0008_errorlogentry.py | 33 +++++ mayan/apps/common/models.py | 34 ++++- mayan/apps/common/permissions_runtime.py | 12 ++ mayan/apps/common/urls.py | 10 +- mayan/apps/common/views.py | 44 +++++- 10 files changed, 274 insertions(+), 67 deletions(-) create mode 100644 mayan/apps/common/managers.py create mode 100644 mayan/apps/common/migrations/0008_errorlogentry.py create mode 100644 mayan/apps/common/permissions_runtime.py diff --git a/mayan/apps/common/admin.py b/mayan/apps/common/admin.py index 2c501ac9ae..ec309e5802 100644 --- a/mayan/apps/common/admin.py +++ b/mayan/apps/common/admin.py @@ -2,7 +2,14 @@ from __future__ import unicode_literals from django.contrib import admin -from .models import SharedUploadedFile, UserLocaleProfile +from .models import ErrorLogEntry, SharedUploadedFile, UserLocaleProfile + + +@admin.register(ErrorLogEntry) +class ErrorLogEntryAdmin(admin.ModelAdmin): + date_hierarchy = 'datetime' + list_display = ('namespace', 'content_object', 'datetime', 'result') + readonly_fields = list_display @admin.register(SharedUploadedFile) diff --git a/mayan/apps/common/apps.py b/mayan/apps/common/apps.py index f4c738bb98..b3380e3d8a 100644 --- a/mayan/apps/common/apps.py +++ b/mayan/apps/common/apps.py @@ -14,6 +14,7 @@ from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from mayan.celery import app +from navigation import SourceColumn from navigation.classes import Separator, Text from rest_api.classes import APIEndPoint @@ -85,8 +86,27 @@ class CommonApp(MayanAppConfig): def ready(self): super(CommonApp, self).ready() + ErrorLogEntry = self.get_model(model_name='ErrorLogEntry') + APIEndPoint(app=self, version_string='1') + SourceColumn( + source=ErrorLogEntry, label=_('Namespace'), + attribute='namespace' + ) + SourceColumn( + source=ErrorLogEntry, label=_('Object'), + attribute='content_object' + ) + SourceColumn( + source=ErrorLogEntry, label=_('Date and time'), + attribute='datetime' + ) + SourceColumn( + source=ErrorLogEntry, label=_('Result'), + attribute='result' + ) + app.conf.CELERYBEAT_SCHEDULE.update( { 'task_delete_stale_uploads': { diff --git a/mayan/apps/common/classes.py b/mayan/apps/common/classes.py index 790122bb8e..cf67c62256 100644 --- a/mayan/apps/common/classes.py +++ b/mayan/apps/common/classes.py @@ -100,6 +100,82 @@ class DashboardWidget(object): self.__class__._registry.append(self) +@python_2_unicode_compatible +class ErrorLogNamespace(object): + def __init__(self, name, label=None): + self.name = name + self.label = label or name + + def __str__(self): + return force_text(self.label) + + def all(self): + ErrorLogEntry = apps.get_model( + app_label='common', model_name='ErrorLogEntry' + ) + + return ErrorLogEntry.objects.filter(namespace=self.name) + + +@python_2_unicode_compatible +class Filter(object): + _registry = {} + + @classmethod + def get(cls, slug): + return cls._registry[slug] + + @classmethod + def all(cls): + return cls._registry + + def __init__(self, label, slug, filter_kwargs, model, object_permission=None, hide_links=False): + self.label = label + self.slug = slug + self.filter_kwargs = filter_kwargs + self.model = model + self.object_permission = object_permission + self.hide_links = hide_links + + self.__class__._registry[self.slug] = self + + def __str__(self): + return force_text(self.label) + + def get_queryset(self, user): + AccessControlList = apps.get_model( + app_label='acls', model_name='AccessControlList' + ) + + queryset = self.model.objects.all() + for kwargs in self.filter_kwargs: + queryset = queryset.filter(**kwargs) + + queryset = queryset.distinct() + + if self.object_permission: + return AccessControlList.objects.filter_by_access( + self.object_permission, user, queryset=queryset + ) + else: + return queryset + + +class MissingItem(object): + _registry = [] + + @classmethod + def get_all(cls): + return cls._registry + + def __init__(self, label, condition, description, view): + self.label = label + self.condition = condition + self.description = description + self.view = view + self.__class__._registry.append(self) + + @python_2_unicode_compatible class ModelAttribute(object): __registry = {} @@ -179,65 +255,6 @@ class ModelAttribute(object): self.__registry[model][type_name].append(self) -class MissingItem(object): - _registry = [] - - @classmethod - def get_all(cls): - return cls._registry - - def __init__(self, label, condition, description, view): - self.label = label - self.condition = condition - self.description = description - self.view = view - self.__class__._registry.append(self) - - -@python_2_unicode_compatible -class Filter(object): - _registry = {} - - @classmethod - def get(cls, slug): - return cls._registry[slug] - - @classmethod - def all(cls): - return cls._registry - - def __init__(self, label, slug, filter_kwargs, model, object_permission=None, hide_links=False): - self.label = label - self.slug = slug - self.filter_kwargs = filter_kwargs - self.model = model - self.object_permission = object_permission - self.hide_links = hide_links - - self.__class__._registry[self.slug] = self - - def __str__(self): - return force_text(self.label) - - def get_queryset(self, user): - AccessControlList = apps.get_model( - app_label='acls', model_name='AccessControlList' - ) - - queryset = self.model.objects.all() - for kwargs in self.filter_kwargs: - queryset = queryset.filter(**kwargs) - - queryset = queryset.distinct() - - if self.object_permission: - return AccessControlList.objects.filter_by_access( - self.object_permission, user, queryset=queryset - ) - else: - return queryset - - class Package(object): _registry = [] diff --git a/mayan/apps/common/links.py b/mayan/apps/common/links.py index e4407c6d4b..73a7debc14 100644 --- a/mayan/apps/common/links.py +++ b/mayan/apps/common/links.py @@ -1,9 +1,30 @@ from __future__ import unicode_literals +from django.apps import apps from django.utils.translation import ugettext_lazy as _ from navigation import Link +from .permissions_runtime import permission_error_log_view + + +def get_kwargs_factory(variable_name): + def get_kwargs(context): + ContentType = apps.get_model( + app_label='contenttypes', model_name='ContentType' + ) + + content_type = ContentType.objects.get_for_model( + context[variable_name] + ) + return { + 'app_label': '"{}"'.format(content_type.app_label), + 'model': '"{}"'.format(content_type.model), + 'object_id': '{}.pk'.format(variable_name) + } + + return get_kwargs + link_about = Link( icon='fa fa-info', text=_('About this'), view='common:about_view' @@ -35,6 +56,15 @@ link_documentation = Link( icon='fa fa-book', tags='new_window', text=_('Documentation'), url='https://mayan.readthedocs.io/en/stable/' ) +link_error_list = Link( + permissions=(permission_error_log_view,), text=_('Errors'), + view='common:error_list', kwargs=get_kwargs_factory('resolved_object') +) +link_error_list_with_icon = Link( + icon='fa fa-lock', permissions=(permission_error_log_view,), + text=_('Erros'), view='common:error_list', + kwargs=get_kwargs_factory('resolved_object') +) link_filters = Link( icon='fa fa-filter', text=_('Data filters'), view='common:filter_selection' diff --git a/mayan/apps/common/managers.py b/mayan/apps/common/managers.py new file mode 100644 index 0000000000..dbc93c3c63 --- /dev/null +++ b/mayan/apps/common/managers.py @@ -0,0 +1,14 @@ +from __future__ import unicode_literals + +from django.apps import apps +from django.contrib.contenttypes.fields import GenericRelation +from django.conf import settings +from django.db import models + + +class ErrorLogEntryManager(models.Manager): + def register(self, model): + ErrorLogEntry = apps.get_model( + app_label='common', model_name='ErrorLogEntry' + ) + model.add_to_class('error_logs', GenericRelation(ErrorLogEntry)) diff --git a/mayan/apps/common/migrations/0008_errorlogentry.py b/mayan/apps/common/migrations/0008_errorlogentry.py new file mode 100644 index 0000000000..f08b282123 --- /dev/null +++ b/mayan/apps/common/migrations/0008_errorlogentry.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-08-25 06:52 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('common', '0007_auto_20170118_1758'), + ] + + operations = [ + migrations.CreateModel( + name='ErrorLogEntry', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('namespace', models.CharField(max_length=128, verbose_name='Namespace')), + ('object_id', models.PositiveIntegerField(blank=True, null=True)), + ('datetime', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Date time')), + ('result', models.TextField(blank=True, null=True, verbose_name='Result')), + ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='error_log_content_type', to='contenttypes.ContentType')), + ], + options={ + 'ordering': ('datetime',), + 'verbose_name': 'Error log entry', + 'verbose_name_plural': 'Error log entries', + }, + ), + ] diff --git a/mayan/apps/common/models.py b/mayan/apps/common/models.py index 1dd28f3dd0..a892bce1d3 100644 --- a/mayan/apps/common/models.py +++ b/mayan/apps/common/models.py @@ -5,10 +5,13 @@ import uuid from pytz import common_timezones from django.conf import settings +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ +from .managers import ErrorLogEntryManager from .runtime import shared_storage_backend @@ -16,6 +19,31 @@ def upload_to(instance, filename): return 'shared-file-{}'.format(uuid.uuid4().hex) +class ErrorLogEntry(models.Model): + namespace = models.CharField( + max_length=128, verbose_name=_('Namespace') + ) + content_type = models.ForeignKey( + ContentType, blank=True, on_delete=models.CASCADE, null=True, + related_name='error_log_content_type' + ) + object_id = models.PositiveIntegerField(blank=True, null=True) + content_object = GenericForeignKey( + ct_field='content_type', fk_field='object_id', + ) + datetime = models.DateTimeField( + auto_now_add=True, db_index=True, verbose_name=_('Date time') + ) + result = models.TextField(blank=True, null=True, verbose_name=_('Result')) + + objects = ErrorLogEntryManager() + + class Meta: + ordering = ('datetime',) + verbose_name = _('Error log entry') + verbose_name_plural = _('Error log entries') + + @python_2_unicode_compatible class SharedUploadedFile(models.Model): file = models.FileField( @@ -60,9 +88,9 @@ class UserLocaleProfile(models.Model): choices=settings.LANGUAGES, max_length=8, verbose_name=_('Language') ) - def __str__(self): - return force_text(self.user) - class Meta: verbose_name = _('User locale profile') verbose_name_plural = _('User locale profiles') + + def __str__(self): + return force_text(self.user) diff --git a/mayan/apps/common/permissions_runtime.py b/mayan/apps/common/permissions_runtime.py new file mode 100644 index 0000000000..0b62fcb58f --- /dev/null +++ b/mayan/apps/common/permissions_runtime.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from permissions import PermissionNamespace + +namespace = PermissionNamespace('common', _('Common')) + +permission_error_log_view = namespace.add_permission( + name='error_log_view', label=_('View errorr log') +) + diff --git a/mayan/apps/common/urls.py b/mayan/apps/common/urls.py index 403aadd7df..f8a3bfb87e 100644 --- a/mayan/apps/common/urls.py +++ b/mayan/apps/common/urls.py @@ -7,9 +7,9 @@ from .api_views import APIContentTypeList from .views import ( AboutView, CheckVersionView, CurrentUserDetailsView, CurrentUserEditView, CurrentUserLocaleProfileDetailsView, CurrentUserLocaleProfileEditView, - FaviconRedirectView, FilterResultListView, FilterSelectView, HomeView, - LicenseView, PackagesLicensesView, SetupListView, ToolsListView, - multi_object_action_view + ErrorLogEntryListView, FaviconRedirectView, FilterResultListView, + FilterSelectView, HomeView, LicenseView, PackagesLicensesView, + SetupListView, ToolsListView, multi_object_action_view ) urlpatterns = [ @@ -54,6 +54,10 @@ urlpatterns = [ r'^filter/(?P[\w-]+)/results/$', FilterResultListView.as_view(), name='filter_results' ), + url( + r'^(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/errors/$', + ErrorLogEntryListView.as_view(), name='error_list' + ), ] urlpatterns += [ diff --git a/mayan/apps/common/views.py b/mayan/apps/common/views.py index 9335c085b7..d97f5b951a 100644 --- a/mayan/apps/common/views.py +++ b/mayan/apps/common/views.py @@ -4,8 +4,9 @@ from json import dumps from django.conf import settings from django.contrib import messages +from django.contrib.contenttypes.models import ContentType from django.http import Http404, HttpResponseRedirect -from django.shortcuts import resolve_url +from django.shortcuts import get_object_or_404, resolve_url from django.template import RequestContext from django.urls import reverse, reverse_lazy from django.utils import timezone, translation @@ -13,6 +14,8 @@ from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _, ugettext from django.views.generic import RedirectView, TemplateView +from acls.models import AccessControlList + from .classes import Filter from .exceptions import NotLatestVersion from .forms import ( @@ -28,6 +31,8 @@ from .generics import ( # NOQA SingleObjectEditView, SingleObjectListView, SimpleView ) from .menus import menu_tools, menu_setup +from .models import ErrorLogEntry +from .permissions_runtime import permission_error_log_view from .utils import check_version @@ -130,6 +135,43 @@ class CurrentUserLocaleProfileEditView(SingleObjectEditView): return self.request.user.locale_profile +class ErrorLogEntryListView(SingleObjectListView): + def dispatch(self, request, *args, **kwargs): + self.object_content_type = get_object_or_404( + ContentType, app_label=self.kwargs['app_label'], + model=self.kwargs['model'] + ) + + try: + self.content_object = self.object_content_type.get_object_for_this_type( + pk=self.kwargs['object_id'] + ) + except self.object_content_type.model_class().DoesNotExist: + raise Http404 + + AccessControlList.objects.check_access( + obj=self.content_object, permissions=permission_error_log_view, + user=request.user + ) + + return super(ErrorLogEntryListView, self).dispatch( + request, *args, **kwargs + ) + + def get_extra_context(self): + return { + 'hide_object': True, + 'object': self.content_object, + 'title': _('Error log entries for: %s' % self.content_object), + } + + def get_object_list(self): + return ErrorLogEntry.objects.filter( + content_type=self.object_content_type, + object_id=self.content_object.pk + ) + + class FaviconRedirectView(RedirectView): permanent = True