diff --git a/CHANGES/2388.bugfix b/CHANGES/2388.bugfix
new file mode 100644
index 0000000000..03885a7886
--- /dev/null
+++ b/CHANGES/2388.bugfix
@@ -0,0 +1 @@
+Vendor django-automated-logging to provide Django 4.x compatibility
\ No newline at end of file
diff --git a/MANIFEST.in b/MANIFEST.in
index d4f1096923..80073bf970 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -15,3 +15,5 @@ exclude netlify.toml
exclude docs
exclude docs_requirements.txt
exclude .readthedocs.yaml
+include django-automated-logging-LICENSE.txt
+include galaxy_ng/automated_logging/templates/dal/admin/view.html
diff --git a/django-automated-logging-LICENSE.txt b/django-automated-logging-LICENSE.txt
new file mode 100644
index 0000000000..dd2fc2fa9e
--- /dev/null
+++ b/django-automated-logging-LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Bilal Mahmoud
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/flake8.cfg b/flake8.cfg
index 13b8e83717..548abd7e8b 100644
--- a/flake8.cfg
+++ b/flake8.cfg
@@ -9,6 +9,7 @@ exclude =
.ci/scripts/*,
.github/workflows/scripts/*,
.venv/*,
+ ./galaxy_ng/_vendor/automated_logging/*,
ignore = BLK,W503,Q000,D,D100,D101,D102,D103,D104,D105,D106,D107,D200,D401,D402,E203
max-line-length = 100
diff --git a/galaxy_ng/__init__.py b/galaxy_ng/__init__.py
index bac2b8df2f..3501295896 100644
--- a/galaxy_ng/__init__.py
+++ b/galaxy_ng/__init__.py
@@ -1,3 +1,8 @@
+import sys
+from ._vendor import automated_logging
+
+sys.modules.setdefault("automated_logging", automated_logging)
+
__version__ = "4.8.0dev"
default_app_config = "galaxy_ng.app.PulpGalaxyPluginAppConfig"
diff --git a/galaxy_ng/_vendor/__init__.py b/galaxy_ng/_vendor/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/galaxy_ng/_vendor/automated_logging/__init__.py b/galaxy_ng/_vendor/automated_logging/__init__.py
new file mode 100755
index 0000000000..bd0ba9fa96
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/__init__.py
@@ -0,0 +1,7 @@
+"""
+Django Automated Logging makes logging easy.
+
+Django Automated Logging (DAL) is a package with the purpose of
+making logging in django automated and easy.
+"""
+default_app_config = 'automated_logging.apps.AutomatedloggingConfig'
diff --git a/galaxy_ng/_vendor/automated_logging/admin/__init__.py b/galaxy_ng/_vendor/automated_logging/admin/__init__.py
new file mode 100644
index 0000000000..01564b2e1e
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/admin/__init__.py
@@ -0,0 +1,9 @@
+""" This is just a file that imports all admin interfaces defined in this package """
+
+from automated_logging.admin.model_entry import *
+from automated_logging.admin.model_event import *
+from automated_logging.admin.model_mirror import *
+
+from automated_logging.admin.request_event import *
+
+from automated_logging.admin.unspecified_event import *
diff --git a/galaxy_ng/_vendor/automated_logging/admin/base.py b/galaxy_ng/_vendor/automated_logging/admin/base.py
new file mode 100644
index 0000000000..869abd4bd2
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/admin/base.py
@@ -0,0 +1,70 @@
+from django.contrib.admin.options import BaseModelAdmin, ModelAdmin, TabularInline
+from django.contrib.admin.templatetags.admin_urls import admin_urlname
+from django.shortcuts import resolve_url
+from django.utils.html import format_html
+from django.utils.safestring import SafeText
+
+from automated_logging.models import BaseModel
+
+
+class MixinBase(BaseModelAdmin):
+ """
+ TabularInline and ModelAdmin readonly mixin have both the same methods and
+ return the same, because of that fact we have a mixin base
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.readonly_fields = [f.name for f in self.model._meta.get_fields()]
+
+ def get_actions(self, request):
+ """ get_actions from ModelAdmin, but remove all write operations."""
+ actions = super().get_actions(request)
+ actions.pop('delete_selected', None)
+
+ return actions
+
+ def has_add_permission(self, request, instance=None):
+ """ no-one should have the ability to add something => r/o"""
+ return False
+
+ def has_delete_permission(self, request, instance=None):
+ """ no-one should have the ability to delete something => r/o """
+ return False
+
+ def has_change_permission(self, request, instance=None):
+ """ no-one should have the ability to edit something => r/o """
+ return False
+
+ def save_model(self, request, instance, form, change):
+ """ disable saving by doing nothing """
+ pass
+
+ def delete_model(self, request, instance):
+ """ disable deleting by doing nothing """
+ pass
+
+ def save_related(self, request, form, formsets, change):
+ """ we don't need to save related, because save_model does nothing """
+ pass
+
+ # helpers
+ def model_admin_url(self, instance: BaseModel, name: str = None) -> str:
+ """ Helper to return a URL to another object """
+ url = resolve_url(
+ admin_urlname(instance._meta, SafeText("change")), instance.pk
+ )
+ return format_html('{}', url, name or str(instance))
+
+
+class ReadOnlyAdminMixin(MixinBase, ModelAdmin):
+ """ Disables all editing capabilities for the model admin """
+
+ change_form_template = "dal/admin/view.html"
+
+
+class ReadOnlyTabularInlineMixin(MixinBase, TabularInline):
+ """ Disables all editing capabilities for inline """
+
+ model = None
diff --git a/galaxy_ng/_vendor/automated_logging/admin/model_entry.py b/galaxy_ng/_vendor/automated_logging/admin/model_entry.py
new file mode 100644
index 0000000000..af1630f661
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/admin/model_entry.py
@@ -0,0 +1,79 @@
+"""
+Everything related to the admin interface of ModelEntry is located in here
+"""
+
+from django.contrib.admin import register
+from django.shortcuts import redirect
+
+from automated_logging.admin.model_event import ModelEventAdmin
+from automated_logging.admin.base import ReadOnlyAdminMixin, ReadOnlyTabularInlineMixin
+from automated_logging.models import ModelEntry, ModelEvent
+
+
+class ModelEventInline(ReadOnlyTabularInlineMixin):
+ """ inline for all attached events """
+
+ model = ModelEvent
+
+ def __init__(self, *args, **kwargs):
+ super(ModelEventInline, self).__init__(*args, **kwargs)
+
+ self.readonly_fields = [*self.readonly_fields, 'get_uuid', 'get_modifications']
+
+ def get_uuid(self, instance):
+ """ make the uuid small """
+ return self.model_admin_url(instance, str(instance.id).split('-')[0])
+
+ get_uuid.short_description = 'UUID'
+
+ def get_modifications(self, instance):
+ """ ModelEventAdmin already implements this functions, we just refer to it"""
+ return ModelEventAdmin.get_modifications(self, instance)
+
+ get_modifications.short_description = 'Modifications'
+
+ fields = ('get_uuid', 'updated_at', 'user', 'get_modifications')
+
+ ordering = ('-updated_at',)
+
+ verbose_name = 'Event'
+ verbose_name_plural = 'Events'
+
+
+@register(ModelEntry)
+class ModelEntryAdmin(ReadOnlyAdminMixin):
+ """ admin page specification for ModelEntry """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.readonly_fields = [*self.readonly_fields, 'get_model', 'get_application']
+
+ def changelist_view(self, request, **kwargs):
+ """ instead of showing the changelist view redirect to the parent app_list"""
+ return redirect('admin:app_list', self.model._meta.app_label)
+
+ def has_module_permission(self, request):
+ """ remove model entries from the index.html list """
+ return False
+
+ def get_model(self, instance):
+ """ get the model mirror """
+ return self.model_admin_url(instance.mirror)
+
+ get_model.short_description = 'Model'
+
+ def get_application(self, instance):
+ """ get the application """
+ return instance.mirror.application
+
+ get_application.short_description = 'Application'
+
+ fieldsets = (
+ (
+ 'Information',
+ {'fields': ('id', 'get_model', 'get_application', 'primary_key', 'value')},
+ ),
+ )
+
+ inlines = [ModelEventInline]
diff --git a/galaxy_ng/_vendor/automated_logging/admin/model_event.py b/galaxy_ng/_vendor/automated_logging/admin/model_event.py
new file mode 100644
index 0000000000..07069024c9
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/admin/model_event.py
@@ -0,0 +1,195 @@
+"""
+Everything related to the admin interface of ModelEvent is located in here
+"""
+
+from django.contrib.admin import register, RelatedOnlyFieldListFilter
+from django.utils.html import format_html
+
+from automated_logging.admin.base import ReadOnlyTabularInlineMixin, ReadOnlyAdminMixin
+from automated_logging.helpers import Operation
+from automated_logging.models import (
+ ModelValueModification,
+ ModelRelationshipModification,
+ ModelEvent,
+)
+
+
+class ModelValueModificationInline(ReadOnlyTabularInlineMixin):
+ """ inline for all modifications """
+
+ model = ModelValueModification
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.readonly_fields = [*self.readonly_fields, 'get_uuid', 'get_field']
+
+ def get_uuid(self, instance):
+ """ make the uuid small """
+ return str(instance.id).split('-')[0]
+
+ get_uuid.short_description = 'UUID'
+
+ def get_field(self, instance):
+ """ show the field name """
+ return instance.field.name
+
+ get_field.short_description = 'Field'
+
+ fields = ('get_uuid', 'operation', 'get_field', 'previous', 'current')
+ can_delete = False
+
+ verbose_name = 'Modification'
+ verbose_name_plural = 'Modifications'
+
+
+class ModelRelationshipModificationInline(ReadOnlyTabularInlineMixin):
+ """ inline for all relationship modifications """
+
+ model = ModelRelationshipModification
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.readonly_fields = [*self.readonly_fields, 'get_uuid', 'get_field']
+
+ def get_uuid(self, instance):
+ """ make the uuid small """
+ return str(instance.id).split('-')[0]
+
+ get_uuid.short_description = 'UUID'
+
+ def get_field(self, instance):
+ """ show the field name """
+ return instance.field.name
+
+ get_field.short_description = 'Field'
+
+ fields = ('get_uuid', 'operation', 'get_field', 'entry')
+ can_delete = False
+
+ verbose_name = 'Relationship'
+ verbose_name_plural = 'Relationships'
+
+
+@register(ModelEvent)
+class ModelEventAdmin(ReadOnlyAdminMixin):
+ """ admin page specification for ModelEvent """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.readonly_fields = [
+ *self.readonly_fields,
+ 'get_application',
+ 'get_user',
+ 'get_model_link',
+ ]
+
+ def get_modifications(self, instance):
+ """
+ Modifications in short form, are colored for better readability.
+
+ Colors taken from
+ https://github.com/django/django/tree/master/django/contrib/admin/static/admin/img
+ """
+ colors = {
+ Operation.CREATE: '#70bf2b',
+ Operation.MODIFY: '#efb80b',
+ Operation.DELETE: '#dd4646',
+ }
+ return format_html(
+ ', '.join(
+ [
+ *[
+ f''
+ f'{m.short()}'
+ f''
+ for m in instance.modifications.all()
+ ],
+ *[
+ f''
+ f'{r.medium()[0]}'
+ f'[{r.medium()[1]}]'
+ for r in instance.relationships.all()
+ ],
+ ],
+ )
+ )
+
+ get_modifications.short_description = 'Modifications'
+
+ def get_model(self, instance):
+ """
+ get the model
+ TODO: consider splitting this up to model/pk/value
+ """
+ return instance.entry.short()
+
+ get_model.short_description = 'Model'
+
+ def get_model_link(self, instance):
+ """ get the model with a link to the entry """
+ return self.model_admin_url(instance.entry)
+
+ get_model_link.short_description = 'Model'
+
+ def get_application(self, instance):
+ """
+ helper to get the application from the child ModelMirror
+ :param instance:
+ :return:
+ """
+ return instance.entry.mirror.application
+
+ get_application.short_description = 'Application'
+
+ def get_id(self, instance):
+ """ shorten the id to the first 8 digits """
+ return str(instance.id).split('-')[0]
+
+ get_id.short_description = 'UUID'
+
+ def get_user(self, instance):
+ """ return the user with a link """
+ return self.model_admin_url(instance.user) if instance.user else None
+
+ get_user.short_description = 'User'
+
+ list_display = (
+ 'get_id',
+ 'updated_at',
+ 'user',
+ 'get_application',
+ 'get_model',
+ 'get_modifications',
+ )
+
+ list_filter = (
+ 'updated_at',
+ ('user', RelatedOnlyFieldListFilter),
+ ('entry__mirror__application', RelatedOnlyFieldListFilter),
+ ('entry__mirror', RelatedOnlyFieldListFilter),
+ )
+
+ date_hierarchy = 'updated_at'
+ ordering = ('-updated_at',)
+
+ fieldsets = (
+ (
+ 'Information',
+ {
+ 'fields': (
+ 'id',
+ 'get_user',
+ 'updated_at',
+ 'get_application',
+ 'get_model_link',
+ )
+ },
+ ),
+ ('Introspection', {'fields': ('performance', 'snapshot')}),
+ )
+ inlines = [ModelValueModificationInline, ModelRelationshipModificationInline]
+
+ show_change_link = True
diff --git a/galaxy_ng/_vendor/automated_logging/admin/model_mirror.py b/galaxy_ng/_vendor/automated_logging/admin/model_mirror.py
new file mode 100644
index 0000000000..fea1482bf9
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/admin/model_mirror.py
@@ -0,0 +1,39 @@
+"""
+Everything related to the admin interface of ModelMirror is located in here
+"""
+from django.contrib.admin import register
+from django.shortcuts import redirect
+
+from automated_logging.admin.base import ReadOnlyAdminMixin, ReadOnlyTabularInlineMixin
+from automated_logging.models import ModelMirror, ModelField
+
+
+class ModelFieldInline(ReadOnlyTabularInlineMixin):
+ """ list all recorded fields """
+
+ model = ModelField
+
+ fields = ['name', 'type']
+
+ verbose_name = 'Recorded Field'
+ verbose_name_plural = 'Recorded Fields'
+
+
+@register(ModelMirror)
+class ModelMirrorAdmin(ReadOnlyAdminMixin):
+ """ admin page specification for ModelMirror """
+
+ def has_module_permission(self, request):
+ """ prevents this from showing up index.html """
+ return False
+
+ def changelist_view(self, request, **kwargs):
+ """ instead of showing the changelist view redirect to the parent app_list"""
+ return redirect('admin:app_list', self.model._meta.app_label)
+
+ date_hierarchy = 'updated_at'
+ ordering = ('-updated_at',)
+
+ fieldsets = (('Information', {'fields': ('id', 'application', 'name')},),)
+
+ inlines = [ModelFieldInline]
diff --git a/galaxy_ng/_vendor/automated_logging/admin/request_event.py b/galaxy_ng/_vendor/automated_logging/admin/request_event.py
new file mode 100644
index 0000000000..93d597b01c
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/admin/request_event.py
@@ -0,0 +1,48 @@
+"""
+Everything related to the admin interface of RequestEvent is located in here
+"""
+from django.contrib.admin import register, RelatedOnlyFieldListFilter
+
+from automated_logging.admin.base import ReadOnlyAdminMixin, ReadOnlyTabularInlineMixin
+from automated_logging.models import RequestEvent
+
+
+@register(RequestEvent)
+class RequestEventAdmin(ReadOnlyAdminMixin):
+ """ admin page specification for the RequestEvent """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.readonly_fields = [*self.readonly_fields, 'get_uri', 'get_user']
+
+ def get_id(self, instance):
+ """ shorten the id to the first 8 digits """
+ return str(instance.id).split('-')[0]
+
+ get_id.short_description = 'UUID'
+
+ def get_uri(self, instance):
+ """ get the uri. just a redirect to set the short description. """
+ return instance.uri
+
+ get_uri.short_description = 'URI'
+
+ def get_user(self, instance):
+ """ get the user with a URL """
+ return self.model_admin_url(instance.user)
+
+ get_user.short_description = 'User'
+
+ list_display = ('get_id', 'updated_at', 'user', 'method', 'status', 'uri')
+
+ date_hierarchy = 'updated_at'
+ ordering = ('-updated_at',)
+
+ list_filter = ('updated_at', ('user', RelatedOnlyFieldListFilter))
+
+ fieldsets = (
+ ('Information', {'fields': ('id', 'get_user', 'updated_at', 'application',)},),
+ ('HTTP', {'fields': ('method', 'status', 'get_uri')}),
+ )
+ # TODO: Context
diff --git a/galaxy_ng/_vendor/automated_logging/admin/unspecified_event.py b/galaxy_ng/_vendor/automated_logging/admin/unspecified_event.py
new file mode 100644
index 0000000000..cc784ac5c3
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/admin/unspecified_event.py
@@ -0,0 +1,36 @@
+"""
+Everything related to the admin interface of UnspecifiedEvent is located in here
+"""
+from django.contrib.admin import register, RelatedOnlyFieldListFilter
+
+from automated_logging.admin.base import ReadOnlyAdminMixin, ReadOnlyTabularInlineMixin
+from automated_logging.models import UnspecifiedEvent
+
+
+@register(UnspecifiedEvent)
+class UnspecifiedEventAdmin(ReadOnlyAdminMixin):
+ """ admin page specification for the UnspecifiedEvent """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def get_id(self, instance):
+ """ shorten the id to the first 8 digits """
+ return str(instance.id).split('-')[0]
+
+ get_id.short_description = 'UUID'
+
+ list_display = ('get_id', 'updated_at', 'level', 'message')
+
+ date_hierarchy = 'updated_at'
+ ordering = ('-updated_at',)
+
+ list_filter = ('updated_at',)
+
+ fieldsets = (
+ (
+ 'Information',
+ {'fields': ('id', 'updated_at', 'application', 'level', 'message')},
+ ),
+ ('Location', {'fields': ('file', 'line')}),
+ )
diff --git a/galaxy_ng/_vendor/automated_logging/apps.py b/galaxy_ng/_vendor/automated_logging/apps.py
new file mode 100755
index 0000000000..4f8bde218a
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/apps.py
@@ -0,0 +1,16 @@
+from django.apps import AppConfig
+from .settings import settings
+
+
+class AutomatedloggingConfig(AppConfig):
+ name = 'automated_logging'
+ verbose_name = 'Django Automated Logging (DAL)'
+
+ def ready(self):
+ if 'request' in settings.modules:
+ from .signals import request
+ if 'model' in settings.modules:
+ from .signals import save
+ from .signals import m2m
+
+ from .handlers import DatabaseHandler
diff --git a/galaxy_ng/_vendor/automated_logging/decorators.py b/galaxy_ng/_vendor/automated_logging/decorators.py
new file mode 100644
index 0000000000..820e510519
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/decorators.py
@@ -0,0 +1,212 @@
+from functools import wraps, partial
+from typing import List, NamedTuple, Set, Optional, Type, Dict, Callable, Union
+
+
+from automated_logging.helpers import (
+ Operation,
+ get_or_create_thread,
+ function2path,
+)
+from automated_logging.helpers.enums import VerbOperationMap
+
+
+def _normalize_view_args(methods: List[str]) -> Set[str]:
+ if methods is not None:
+ methods = {m.upper() for m in methods}
+
+ return methods
+
+
+# TODO: consider adding status_codes
+def exclude_view(func=None, *, methods: Optional[List[str]] = ()):
+ """
+ Decorator used for ignoring specific views, without adding them
+ to the AUTOMATED_LOGGING configuration.
+
+ This is done via the local threading object. This is done via the function
+ name and module location.
+
+ :param func: function to be decorated
+ :param methods: methods to be ignored (case-insensitive),
+ None => No method will be ignored,
+ [] => All methods will be ignored
+
+ :return: function
+ """
+ if func is None:
+ return partial(exclude_view, methods=methods)
+
+ methods = _normalize_view_args(methods)
+
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ """ simple wrapper """
+ thread, _ = get_or_create_thread()
+
+ path = function2path(func)
+ if (
+ path in thread.dal['ignore.views']
+ and thread.dal['ignore.views'][path] is not None
+ and methods is not None
+ ):
+ methods.update(thread.dal['ignore.views'][path])
+
+ thread.dal['ignore.views'][path] = methods
+
+ return func(*args, **kwargs)
+
+ return wrapper
+
+
+def include_view(func=None, *, methods: List[str] = None):
+ """
+ Decorator used for including specific views **regardless** if they
+ are included in one of the exclusion patterns, this can be selectively done
+ via methods. Non matching methods will still go through the exclusion pattern
+ matching.
+
+ :param func: function to be decorated
+ :param methods: methods to be included (case-insensitive)
+ None => All methods will be explicitly included
+ [] => No method will be explicitly included
+ :return: function
+ """
+ if func is None:
+ return partial(include_view, methods=methods)
+
+ methods = _normalize_view_args(methods)
+
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ """ simple wrapper """
+ thread, _ = get_or_create_thread()
+
+ path = function2path(func)
+ if (
+ path in thread.dal['include.views']
+ and thread.dal['include.views'][path] is not None
+ and methods is not None
+ ):
+ methods.update(thread.dal['include.views'][path])
+
+ thread.dal['include.views'][path] = methods
+ return func(*args, **kwargs)
+
+ return wrapper
+
+
+def _normalize_model_args(
+ operations: List[str], fields: List[str]
+) -> [Set[Operation], Set[str]]:
+ if operations is not None and not all(
+ isinstance(op, Operation) for op in operations
+ ):
+ operations = {
+ VerbOperationMap[o.lower()]
+ for o in operations
+ if o.lower() in VerbOperationMap.keys()
+ }
+
+ if fields is not None:
+ fields = set(fields)
+
+ return operations, fields
+
+
+_include_models = {}
+_exclude_models = {}
+
+IgnoreModel = NamedTuple(
+ "IgnoreModel", (('operations', Set[Operation]), ('fields', Set[str]))
+)
+
+
+def _register_model(
+ registry: Dict[str, Union['IgnoreModel', 'IncludeModel']],
+ container: Type[Union['IgnoreModel', 'IncludeModel']],
+ decorator: Callable,
+ model=None,
+ operations: List[str] = None,
+ fields: List[str] = None,
+):
+ if model is None:
+ return partial(decorator, operations=operations, fields=fields)
+
+ operations, fields = _normalize_model_args(operations, fields)
+ path = function2path(model)
+
+ if (
+ path in registry
+ and registry[path].operations is not None
+ and operations is not None
+ ):
+ operations.update(registry[path].operations)
+
+ if path in registry and registry[path].fields is not None and fields is not None:
+ fields.update(registry[path].fields)
+
+ registry[path] = container(operations, fields)
+
+ # this makes it so that we have a method we can call to re apply dal.
+ model.__dal_register__ = lambda: _register_model(
+ registry, container, decorator, model, operations, fields
+ )
+ return model
+
+
+def exclude_model(
+ model=None, *, operations: Optional[List[str]] = (), fields: List[str] = ()
+):
+ """
+ Decorator used for ignoring specific models, without using the
+ class or AUTOMATED_LOGGING configuration
+
+ This is done via the local threading object. __module__ and __name__ are used
+ to determine the right model.
+
+ :param model: function to be decorated
+ :param operations: operations to be ignored can be a list of:
+ modify, create, delete (case-insensitive)
+ [] => All operations will be ignored
+ None => No operation will be ignored
+ :param fields: fields to be ignored in not ignored operations
+ [] => All fields will be ignored
+ None => No field will be ignored
+ :return: function
+ """
+ global _exclude_models
+
+ return _register_model(
+ _exclude_models, IgnoreModel, exclude_model, model, operations, fields
+ )
+
+
+IncludeModel = NamedTuple(
+ "IncludeModel", (('operations', Set[Operation]), ('fields', Set[str]))
+)
+
+
+def include_model(
+ model=None, *, operations: List[str] = None, fields: List[str] = None
+):
+ """
+ Decorator used for including specific models, despite potentially being ignored
+ by the exclusion preferences set in the configuration.
+
+ :param model: function to be decorated
+ :param operations: operations to be ignored can be a list of:
+ modify, create, delete (case-insensitive)
+ [] => No operation will be explicitly included
+ None => All operations will be explicitly included
+ :param fields: fields to be explicitly included
+ [] => No fields will be explicitly included
+ None => All fields will be explicitly included.
+
+ :return: function
+ """
+
+ global _include_models
+
+ return _register_model(
+ _include_models, IncludeModel, include_model, model, operations, fields
+ )
diff --git a/galaxy_ng/_vendor/automated_logging/handlers.py b/galaxy_ng/_vendor/automated_logging/handlers.py
new file mode 100755
index 0000000000..4e264d9274
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/handlers.py
@@ -0,0 +1,289 @@
+import re
+from collections import OrderedDict
+from datetime import timedelta
+from logging import Handler, LogRecord
+from pathlib import Path
+from threading import Thread
+from typing import Dict, Any, TYPE_CHECKING, List, Optional, Union, Type, Tuple
+
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils import timezone
+from django.db.models import ForeignObject, Model
+
+
+if TYPE_CHECKING:
+ # we need to do this, to avoid circular imports
+ from automated_logging.models import (
+ RequestEvent,
+ ModelEvent,
+ ModelValueModification,
+ ModelRelationshipModification,
+ )
+
+
+class DatabaseHandler(Handler):
+ def __init__(
+ self, *args, batch: Optional[int] = 1, threading: bool = False, **kwargs
+ ):
+ self.limit = batch or 1
+ self.threading = threading
+ self.instances = OrderedDict()
+ super(DatabaseHandler, self).__init__(*args, **kwargs)
+
+ @staticmethod
+ def _clear(config):
+ from automated_logging.models import ModelEvent, RequestEvent, UnspecifiedEvent
+ from django.db import transaction
+
+ current = timezone.now()
+ with transaction.atomic():
+ if config.model.max_age:
+ ModelEvent.objects.filter(
+ created_at__lte=current - config.model.max_age
+ ).delete()
+ if config.unspecified.max_age:
+ UnspecifiedEvent.objects.filter(
+ created_at__lte=current - config.unspecified.max_age
+ ).delete()
+
+ if config.request.max_age:
+ RequestEvent.objects.filter(
+ created_at__lte=current - config.request.max_age
+ ).delete()
+
+ def save(self, instance=None, commit=True, clear=True):
+ """
+ Internal save procedure.
+ Handles deletion when an event exceeds max_age
+ and batch saving via atomic transactions.
+
+ :return: None
+ """
+ from django.db import transaction
+ from automated_logging.settings import settings
+
+ if instance:
+ self.instances[instance.pk] = instance
+ if len(self.instances) < self.limit:
+ if clear:
+ self._clear(settings)
+ return instance
+
+ if not commit:
+ return instance
+
+ def database(instances, config):
+ """ wrapper so that we can actually use threading """
+ with transaction.atomic():
+ [i.save() for k, i in instances.items()]
+
+ if clear:
+ self._clear(config)
+ instances.clear()
+
+ if self.threading:
+ thread = Thread(
+ group=None, target=database, args=(self.instances, settings)
+ )
+ thread.start()
+ else:
+ database(self.instances, settings)
+
+ return instance
+
+ def get_or_create(self, target: Type[Model], **kwargs) -> Tuple[Model, bool]:
+ """
+ proxy for "get_or_create" from django,
+ instead of creating it immediately we
+ dd it to the list of objects to be created in a single swoop
+
+ :type target: Model to be get_or_create
+ :type kwargs: properties to be used to find and create the new object
+ """
+ created = False
+ try:
+ instance = target.objects.get(**kwargs)
+ except ObjectDoesNotExist:
+ instance = target(**kwargs)
+ self.save(instance, commit=False, clear=False)
+ created = True
+
+ return instance, created
+
+ def prepare_save(self, instance: Model):
+ """
+ Due to the nature of all modifications and such there are some models
+ that are in nature get_or_create and not creations
+ (we don't want so much additional data)
+
+ This is a recursive function that looks for relationships and
+ replaces specific values with their get_or_create counterparts.
+
+ :param instance: model
+ :return: instance that is suitable for saving
+ """
+ from automated_logging.models import (
+ Application,
+ ModelMirror,
+ ModelField,
+ ModelEntry,
+ )
+
+ if isinstance(instance, Application):
+ return Application.objects.get_or_create(name=instance.name)[0]
+ elif isinstance(instance, ModelMirror):
+ return self.get_or_create(
+ ModelMirror,
+ name=instance.name,
+ application=self.prepare_save(instance.application),
+ )[0]
+ elif isinstance(instance, ModelField):
+ entry, _ = self.get_or_create(
+ ModelField,
+ name=instance.name,
+ mirror=self.prepare_save(instance.mirror),
+ )
+ if entry.type != instance.type:
+ entry.type = instance.type
+ self.save(entry, commit=False, clear=False)
+ return entry
+
+ elif isinstance(instance, ModelEntry):
+ entry, _ = self.get_or_create(
+ ModelEntry,
+ mirror=self.prepare_save(instance.mirror),
+ primary_key=instance.primary_key,
+ )
+ if entry.value != instance.value:
+ entry.value = instance.value
+ self.save(entry, commit=False, clear=False)
+ return entry
+
+ # ForeignObjectRel is untouched rn
+ for field in [
+ f
+ for f in instance._meta.get_fields()
+ if isinstance(f, ForeignObject)
+ and getattr(instance, f.name, None) is not None
+ # check the attribute module really being automated_logging
+ # to make sure that we do not follow down a rabbit hole
+ and getattr(instance, f.name).__class__.__module__.split('.', 1)[0]
+ == 'automated_logging'
+ ]:
+ setattr(
+ instance, field.name, self.prepare_save(getattr(instance, field.name))
+ )
+
+ self.save(instance, commit=False, clear=False)
+ return instance
+
+ def unspecified(self, record: LogRecord) -> None:
+ """
+ This is for messages that are not sent from django-automated-logging.
+ The option to still save these log messages is there. We create
+ the event in the handler and then save them.
+
+ :param record:
+ :return:
+ """
+ from automated_logging.models import UnspecifiedEvent, Application
+ from automated_logging.signals import unspecified_exclusion
+ from django.apps import apps
+
+ event = UnspecifiedEvent()
+ if hasattr(record, 'message'):
+ event.message = record.message
+ event.level = record.levelno
+ event.line = record.lineno
+ event.file = Path(record.pathname)
+
+ # this is semi-reliable, but I am unsure of a better way to do this.
+ applications = apps.app_configs.keys()
+ path = Path(record.pathname)
+ candidates = [p for p in path.parts if p in applications]
+ if candidates:
+ # use the last candidate (closest to file)
+ event.application = Application(name=candidates[-1])
+ elif record.module in applications:
+ # if we cannot find the application, we use the module as application
+ event.application = Application(name=record.module)
+ else:
+ # if we cannot determine the application from the application
+ # or from the module we presume that the application is unknown
+ event.application = Application(name=None)
+
+ if not unspecified_exclusion(event):
+ self.prepare_save(event)
+ self.save(event)
+
+ def model(
+ self,
+ record: LogRecord,
+ event: 'ModelEvent',
+ modifications: List['ModelValueModification'],
+ data: Dict[str, Any],
+ ) -> None:
+ """
+ This is for model specific logging events.
+ Compiles the information into an event and saves that event
+ and all modifications done.
+
+ :param event:
+ :param modifications:
+ :param record:
+ :param data:
+ :return:
+ """
+ self.prepare_save(event)
+ self.save(event)
+
+ for modification in modifications:
+ modification.event = event
+ self.prepare_save(modification)
+ self.save()
+
+ def m2m(
+ self,
+ record: LogRecord,
+ event: 'ModelEvent',
+ relationships: List['ModelRelationshipModification'],
+ data: Dict[str, Any],
+ ) -> None:
+ self.prepare_save(event)
+ self.save(event)
+
+ for relationship in relationships:
+ relationship.event = event
+ self.prepare_save(relationship)
+ self.save(relationship)
+
+ def request(self, record: LogRecord, event: 'RequestEvent') -> None:
+ """
+ The request event already has a model prepared that we just
+ need to prepare and save.
+
+ :param record: LogRecord
+ :param event: Event supplied via the LogRecord
+ :return: nothing
+ """
+
+ self.prepare_save(event)
+ self.save(event)
+
+ def emit(self, record: LogRecord) -> None:
+ """
+ Emit function that gets triggered for every log message in scope.
+
+ The record will be processed according to the action set.
+ :param record:
+ :return:
+ """
+ if not hasattr(record, 'action'):
+ return self.unspecified(record)
+
+ if record.action == 'model':
+ return self.model(record, record.event, record.modifications, record.data)
+ elif record.action == 'model[m2m]':
+ return self.m2m(record, record.event, record.relationships, record.data)
+ elif record.action == 'request':
+ return self.request(record, record.event)
diff --git a/galaxy_ng/_vendor/automated_logging/helpers/__init__.py b/galaxy_ng/_vendor/automated_logging/helpers/__init__.py
new file mode 100644
index 0000000000..1f949a684f
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/helpers/__init__.py
@@ -0,0 +1,189 @@
+"""
+Helpers that are used throughout django-automated-logging
+"""
+
+from datetime import datetime
+from typing import Any, Union, Dict, NamedTuple
+
+from automated_logging.helpers.enums import Operation
+from automated_logging.middleware import AutomatedLoggingMiddleware
+
+
+def namedtuple2dict(root: Union[NamedTuple, Dict]) -> dict:
+ """
+ transforms nested namedtuple into a dict
+
+ :param root: namedtuple to convert
+ :return: dictionary from namedtuple
+ """
+
+ output = {}
+ if (
+ isinstance(root, tuple)
+ and hasattr(root, '_serialize')
+ and callable(root._serialize)
+ ):
+ return root._serialize()
+
+ root = root if isinstance(root, dict) else root._asdict()
+
+ def eligible(x):
+ """ check if value x is eligible for recursion """
+ return isinstance(x, tuple) or isinstance(x, dict)
+
+ for k, v in root.items():
+ if isinstance(v, set) or isinstance(v, list):
+ output[k] = [namedtuple2dict(i) if eligible(i) else i for i in v]
+ else:
+ output[k] = namedtuple2dict(v) if eligible(v) else v
+
+ return output
+
+
+def get_or_create_meta(instance) -> [Any, bool]:
+ """
+ Simple helper function that creates the dal object
+ in _meta.
+
+ :param instance:
+ :return:
+ """
+ return instance, get_or_create_local(instance._meta)
+
+
+def get_or_create_thread() -> [Any, bool]:
+ """
+ Get or create the local thread, will always return False as the thread
+ won't be created, but the local dal object will.
+
+ get_or_create to conform with the other functions.
+
+ :return: thread, created dal object?
+ """
+ thread = AutomatedLoggingMiddleware.thread
+
+ return (
+ thread,
+ get_or_create_local(
+ thread,
+ {
+ 'ignore.views': dict,
+ 'ignore.models': dict,
+ 'include.views': dict,
+ 'include.models': dict,
+ },
+ ),
+ )
+
+
+def get_or_create_local(target: Any, defaults={}, key='dal') -> bool:
+ """
+ Get or create local storage DAL metadata container,
+ where dal specific data is.
+
+ :return: created?
+ """
+
+ if not hasattr(target, key):
+ setattr(target, key, MetaDataContainer(defaults))
+ return True
+
+ return False
+
+
+def get_or_create_model_event(
+ instance, operation: Operation, force=False, extra=False
+) -> [Any, bool]:
+ """
+ Get or create the ModelEvent of an instance.
+ This function will also populate the event with the current information.
+
+ :param instance: instance to derive an event from
+ :param operation: specified operation that is done
+ :param force: force creation of new event?
+ :param extra: extra information inserted?
+ :return: [event, created?]
+ """
+ from automated_logging.models import (
+ ModelEvent,
+ ModelEntry,
+ ModelMirror,
+ Application,
+ )
+ from automated_logging.settings import settings
+
+ get_or_create_meta(instance)
+
+ if hasattr(instance._meta.dal, 'event') and not force:
+ return instance._meta.dal.event, False
+
+ instance._meta.dal.event = None
+
+ event = ModelEvent()
+ event.user = AutomatedLoggingMiddleware.get_current_user()
+
+ if settings.model.snapshot and extra:
+ event.snapshot = instance
+
+ if (
+ settings.model.performance
+ and hasattr(instance._meta.dal, 'performance')
+ and extra
+ ):
+ event.performance = datetime.now() - instance._meta.dal.performance
+ instance._meta.dal.performance = None
+
+ event.operation = operation
+ event.entry = ModelEntry()
+ event.entry.mirror = ModelMirror()
+ event.entry.mirror.name = instance.__class__.__name__
+ event.entry.mirror.application = Application(name=instance._meta.app_label)
+ event.entry.value = repr(instance) or str(instance)
+ event.entry.primary_key = instance.pk
+
+ instance._meta.dal.event = event
+
+ return instance._meta.dal.event, True
+
+
+def function2path(func):
+ """ simple helper function to return the module path of a function """
+ return f'{func.__module__}.{func.__name__}'
+
+
+class MetaDataContainer(dict):
+ """
+ MetaDataContainer is used to store DAL specific metadata
+ in various places.
+
+ Values can be retrieved via attribute or key retrieval.
+
+ A dictionary with key attributes can be provided when __init__.
+ The key should be the name of the item, the value should be a function
+ that gets called when an item with that key does
+ not exist gets accessed, to auto-initialize that key.
+ """
+
+ def __init__(self, defaults={}):
+ super().__init__()
+
+ self.auto = defaults
+
+ def __getitem__(self, item):
+ try:
+ return super().__getitem__(item)
+ except KeyError:
+ if item in self.auto:
+ self[item] = self.auto[item]()
+ return self[item]
+ else:
+ raise KeyError
+
+ def __getattr__(self, item):
+ try:
+ return self[item]
+ except KeyError:
+ raise AttributeError
+
+ def __setattr__(self, key, value):
+ self[key] = value
diff --git a/galaxy_ng/_vendor/automated_logging/helpers/enums.py b/galaxy_ng/_vendor/automated_logging/helpers/enums.py
new file mode 100644
index 0000000000..2096e68118
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/helpers/enums.py
@@ -0,0 +1,49 @@
+""" Various enums used in DAL """
+
+from enum import Enum
+
+
+class Operation(int, Enum):
+ """
+ Simple Enum that will be used across the code to
+ indicate the current operation that happened.
+
+ Due to the fact that enum support for django was
+ only added in 3.0 we have DjangoOperations to convert
+ it to the old django format.
+ """
+
+ CREATE = 1
+ MODIFY = 0
+ DELETE = -1
+
+
+DjangoOperations = [(e.value, o.lower()) for o, e in Operation.__members__.items()]
+VerbOperationMap = {
+ 'create': Operation.CREATE,
+ 'modify': Operation.MODIFY,
+ 'delete': Operation.DELETE,
+ 'add': Operation.CREATE,
+ 'remove': Operation.DELETE,
+}
+VerbM2MOperationMap = {
+ 'add': Operation.CREATE,
+ 'modify': Operation.MODIFY,
+ 'remove': Operation.DELETE,
+}
+PastOperationMap = {
+ 'created': Operation.CREATE,
+ 'modified': Operation.MODIFY,
+ 'deleted': Operation.DELETE,
+}
+PastM2MOperationMap = {
+ 'added': Operation.CREATE,
+ 'modified': Operation.MODIFY,
+ 'removed': Operation.DELETE,
+}
+ShortOperationMap = {
+ '+': Operation.CREATE,
+ '~': Operation.MODIFY,
+ '-': Operation.DELETE,
+}
+TranslationOperationMap = {**VerbOperationMap, **PastOperationMap, **ShortOperationMap}
diff --git a/galaxy_ng/_vendor/automated_logging/helpers/exceptions.py b/galaxy_ng/_vendor/automated_logging/helpers/exceptions.py
new file mode 100644
index 0000000000..4de4c30fad
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/helpers/exceptions.py
@@ -0,0 +1,13 @@
+""" custom exceptions """
+
+
+class NoMatchFound(Exception):
+ """ error that indicates that no match has been found for a regex """
+
+ pass
+
+
+class CouldNotConvertError(Exception):
+ """ error that is thrown when no conversion could be done """
+
+ pass
diff --git a/galaxy_ng/_vendor/automated_logging/helpers/schemas.py b/galaxy_ng/_vendor/automated_logging/helpers/schemas.py
new file mode 100644
index 0000000000..e77ed46cdd
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/helpers/schemas.py
@@ -0,0 +1,224 @@
+import re
+import typing
+from collections import namedtuple
+from datetime import timedelta
+from typing import NamedTuple
+
+from marshmallow import Schema, EXCLUDE, post_load
+from marshmallow.fields import List, String, Nested, TimeDelta
+
+from automated_logging.helpers.exceptions import NoMatchFound, CouldNotConvertError
+
+Search = NamedTuple('Search', (('type', str), ('value', str)))
+Search._serialize = lambda self: f'{self.type}:{self.value}'
+
+
+class Set(List):
+ """
+ This is like a list, just compiles down to a set when serializing.
+ """
+
+ def _serialize(
+ self, value, attr, obj, **kwargs
+ ) -> typing.Optional[typing.Set[typing.Any]]:
+ return set(super(Set, self)._serialize(value, attr, obj, **kwargs))
+
+ def _deserialize(self, value, attr, data, **kwargs) -> typing.Set[typing.Any]:
+ return set(super(Set, self)._deserialize(value, attr, data, **kwargs))
+
+
+class LowerCaseString(String):
+ """
+ String that is always going to be serialized to a lowercase string,
+ using `str.lower()`
+ """
+
+ def _deserialize(self, value, attr, data, **kwargs) -> str:
+ output = super()._deserialize(value, attr, data, **kwargs)
+
+ return output.lower()
+
+
+class Duration(TimeDelta):
+ """ TimeDelta derivative, with more input methods """
+
+ def _convert(
+ self, target: typing.Union[int, str, timedelta, None]
+ ) -> typing.Optional[timedelta]:
+ if target is None:
+ return None
+
+ if isinstance(target, timedelta):
+ return target
+
+ if isinstance(target, int) or isinstance(target, float):
+ return timedelta(seconds=target)
+
+ if isinstance(target, str):
+ REGEX = (
+ r'^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$'
+ )
+ match = re.match(REGEX, target, re.IGNORECASE)
+ if not match:
+ raise self.make_error('invalid') from NoMatchFound
+
+ components = list(match.groups())
+ # remove leading T capture - isn't used, by removing the 5th capture group
+ components.pop(4)
+
+ adjusted = {'days': 0, 'seconds': 0}
+ conversion = [
+ ['days', 365], # year
+ ['days', 30], # month
+ ['days', 7], # week
+ ['days', 1], # day
+ ['seconds', 3600], # hour
+ ['seconds', 60], # minute
+ ['seconds', 1], # second
+ ]
+
+ for pointer in range(len(components)):
+ if not components[pointer]:
+ continue
+ rate = conversion[pointer]
+ native = int(re.findall(r'(\d+)', components[pointer])[0])
+
+ adjusted[rate[0]] += native * rate[1]
+
+ return timedelta(**adjusted)
+
+ raise self.make_error('invalid') from CouldNotConvertError
+
+ def _deserialize(self, value, attr, data, **kwargs) -> typing.Optional[timedelta]:
+ try:
+ output = self._convert(value)
+ except OverflowError as error:
+ raise self.make_error('invalid') from error
+
+ return output
+
+
+class SearchString(String):
+ """
+ Used for:
+ - ModelString
+ - FieldString
+ - ApplicationString
+ - FileString
+
+ SearchStrings are used for models, fields and applications.
+ They can be either a glob (prefixed with either glob or gl),
+ regex (prefixed with either regex or re)
+ or plain (prefixed with plain or pl).
+
+ All SearchStrings ignore the case of the raw string.
+
+ format: :
+ examples:
+ - gl:app* (glob matching)
+ - glob:app* (glob matching)
+ - pl:app (exact matching)
+ - plain:app (exact matching)
+ - re:^app.*$ (regex matching)
+ - regex:^app.*$ (regex matching)
+ - :app* (glob matching)
+ - app (glob matching)
+ """
+
+ def _deserialize(self, value, attr, data, **kwargs) -> Search:
+ if isinstance(value, dict) and 'type' in value and 'value' in value:
+ value = f'{value["type"]}:{value["value"]}'
+
+ output = super()._deserialize(value, attr, data, **kwargs)
+
+ match = re.match(r'^(\w*):(.*)$', output, re.IGNORECASE)
+ if match:
+ module = match.groups()[0].lower()
+ match = match.groups()[1]
+
+ if module.startswith('gl'):
+ return Search('glob', match.lower())
+ elif module.startswith('pl'):
+ return Search('plain', match.lower())
+ elif module.startswith('re'):
+ # regex shouldn't be lowercase
+ # we just ignore the case =
+ return Search('regex', match)
+
+ raise self.make_error('invalid') from NotImplementedError
+
+ return Search('glob', output)
+
+
+class MissingNested(Nested):
+ """
+ Modified marshmallow Nested, that is defaulting missing to loading an empty
+ schema, to populate it with data.
+ """
+
+ def __init__(self, *args, **kwargs):
+ if 'missing' not in kwargs:
+ kwargs['missing'] = lambda: args[0]().load({})
+
+ super().__init__(*args, **kwargs)
+
+
+class BaseSchema(Schema):
+ """
+ Modified marshmallow Schema, that is defaulting the unknown keyword to EXCLUDE,
+ not RAISE (marshmallow default) and when loading converts the dict into a namedtuple.
+ """
+
+ def __init__(self, *args, **kwargs):
+ if 'unknown' not in kwargs:
+ kwargs['unknown'] = EXCLUDE
+
+ super().__init__(*args, **kwargs)
+
+ @staticmethod
+ def namedtuple_or(left: NamedTuple, right: NamedTuple):
+ """
+ __or__ implementation for the namedtuple
+ """
+ values = {}
+
+ if not isinstance(left, tuple) or not isinstance(right, tuple):
+ raise NotImplementedError
+
+ for name in left._fields:
+ field = getattr(left, name)
+ values[name] = field
+
+ if not hasattr(right, name):
+ continue
+
+ if isinstance(field, tuple) or isinstance(field, set):
+ values[name] = field | getattr(right, name)
+
+ return left._replace(**values)
+
+ @staticmethod
+ def namedtuple_factory(name, keys):
+ """
+ create the namedtuple from the name and keys to attach functions that are needed.
+
+ Attaches:
+ binary **or** operation to support globals propagation
+ """
+ Object = namedtuple(name, keys)
+ Object.__or__ = BaseSchema.namedtuple_or
+ return Object
+
+ @post_load
+ def make_namedtuple(self, data: typing.Dict, **kwargs):
+ """
+ converts the loaded data dict into a namedtuple
+
+ :param data: loaded data
+ :param kwargs: marshmallow kwargs
+ :return: namedtuple
+ """
+ name = self.__class__.__name__.replace('Schema', '')
+
+ Object = BaseSchema.namedtuple_factory(name, data.keys())
+ return Object(**data)
diff --git a/galaxy_ng/_vendor/automated_logging/middleware.py b/galaxy_ng/_vendor/automated_logging/middleware.py
new file mode 100644
index 0000000000..78ac09634d
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/middleware.py
@@ -0,0 +1,130 @@
+import logging
+import threading
+from typing import NamedTuple, Optional, TYPE_CHECKING
+
+from django.http import HttpRequest, HttpResponse
+
+if TYPE_CHECKING:
+ from django.contrib.auth.models import AbstractUser
+
+RequestInformation = NamedTuple(
+ 'RequestInformation',
+ [
+ ('request', HttpRequest),
+ ('response', Optional[HttpResponse]),
+ ('exception', Optional[Exception]),
+ ],
+)
+
+
+class AutomatedLoggingMiddleware:
+ """
+ Middleware used by django-automated-logging
+ to provide request specific data to the request signals via
+ the local thread.
+ """
+
+ thread = threading.local()
+
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ AutomatedLoggingMiddleware.thread.__dal__ = None
+
+ @staticmethod
+ def save(request, response=None, exception=None):
+ """
+ Helper middleware, that sadly needs to be present.
+ the request_finished and request_started signals only
+ expose the class, not the actual request and response.
+
+ We save the request and response specific data in the thread.
+
+ :param request: Django Request
+ :param response: Optional Django Response
+ :param exception: Optional Exception
+ :return:
+ """
+
+ AutomatedLoggingMiddleware.thread.__dal__ = RequestInformation(
+ request, response, exception
+ )
+
+ def __call__(self, request):
+ """
+ TODO: fix staticfiles has no environment?!
+ it seems like middleware isn't getting called for serving the static files,
+ this seems very odd.
+
+ There are 2 different states, request object will be stored when available
+ and response will only be available post get_response.
+
+ :param request:
+ :return:
+ """
+ self.save(request)
+
+ response = self.get_response(request)
+
+ self.save(request, response)
+
+ return response
+
+ def process_exception(self, request, exception):
+ """
+ Exception proceeds the same as __call__ and therefore should
+ also save things in the local thread.
+
+ :param request: Django Request
+ :param exception: Thrown Exception
+ :return: -
+ """
+ self.save(request, exception=exception)
+
+ @staticmethod
+ def cleanup():
+ """
+ Cleanup function, that should be called last. Overwrites the
+ custom __dal__ object with None, to make sure the next request
+ does not use the same object.
+
+ :return: -
+ """
+ AutomatedLoggingMiddleware.thread.__dal__ = None
+
+ @staticmethod
+ def get_current_environ() -> Optional[RequestInformation]:
+ """
+ Helper staticmethod that looks if the __dal__ custom attribute
+ is present and returns either the attribute or None
+
+ :return: Optional[RequestInformation]
+ """
+
+ if getattr(AutomatedLoggingMiddleware.thread, '__dal__', None):
+ return RequestInformation(*AutomatedLoggingMiddleware.thread.__dal__)
+
+ return None
+
+ @staticmethod
+ def get_current_user(
+ environ: RequestInformation = None,
+ ) -> Optional['AbstractUser']:
+ """
+ Helper staticmethod that returns the current user, taken from
+ the current environment.
+
+ :return: Optional[User]
+ """
+ from django.contrib.auth.models import AnonymousUser
+
+ if not environ:
+ environ = AutomatedLoggingMiddleware.get_current_environ()
+
+ if not environ:
+ return None
+
+ if isinstance(environ.request.user, AnonymousUser):
+ return None
+
+ return environ.request.user
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0001_initial.py b/galaxy_ng/_vendor/automated_logging/migrations/0001_initial.py
new file mode 100644
index 0000000000..ed7e3a91f7
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0001_initial.py
@@ -0,0 +1,171 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.2 on 2018-02-15 14:22
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('contenttypes', '0002_remove_content_type_name'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='LDAP',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('action', models.TextField()),
+ ('succeeded', models.NullBooleanField()),
+ ('errorMessage', models.TextField(blank=True, null=True)),
+ ('basedn', models.TextField(blank=True, null=True)),
+ ('entry', models.TextField(blank=True, null=True)),
+ ('objectClass', models.TextField(blank=True, null=True)),
+ ('cn', models.TextField(blank=True, null=True)),
+ ('existing_members', models.TextField(blank=True, null=True)),
+ ('data_members', models.TextField(blank=True, null=True)),
+ ('diff_members', models.TextField(blank=True, null=True)),
+ ],
+ options={
+ 'verbose_name': 'LDAP event log entry',
+ 'verbose_name_plural': 'LDAP event log entries',
+ },
+ ),
+ migrations.CreateModel(
+ name='Model',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('message', models.TextField(null=True)),
+ ('action', models.PositiveSmallIntegerField(choices=[(0, 'n/a'), (1, 'add'), (2, 'change'), (3, 'delete')], default=0)),
+ ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='atl_model_application', to='contenttypes.ContentType')),
+ ],
+ options={
+ 'verbose_name': 'model log entry',
+ 'verbose_name_plural': 'model log entries',
+ },
+ ),
+ migrations.CreateModel(
+ name='ModelChangelog',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'verbose_name': 'Model entry change',
+ 'verbose_name_plural': 'Model entry changes',
+ },
+ ),
+ migrations.CreateModel(
+ name='ModelModification',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='ModelObject',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('value', models.CharField(max_length=255)),
+ ('model', models.CharField(max_length=255)),
+ ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='atl_modelobject_application', to='contenttypes.ContentType')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='Request',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('url', models.URLField()),
+ ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+ ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'request event entry',
+ 'verbose_name_plural': 'request event entries',
+ },
+ ),
+ migrations.CreateModel(
+ name='Unspecified',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('message', models.TextField(null=True)),
+ ('level', models.PositiveSmallIntegerField(default=20)),
+ ('file', models.CharField(max_length=255, null=True)),
+ ('line', models.PositiveIntegerField(null=True)),
+ ],
+ options={
+ 'verbose_name': ' logging entry (Errors, Warnings, Info)',
+ 'verbose_name_plural': ' logging entries (Errors, Warnings, Info)',
+ },
+ ),
+ migrations.AddField(
+ model_name='modelmodification',
+ name='currently',
+ field=models.ManyToManyField(related_name='changelog_current', to='automated_logging.ModelObject'),
+ ),
+ migrations.AddField(
+ model_name='modelmodification',
+ name='previously',
+ field=models.ManyToManyField(related_name='changelog_previous', to='automated_logging.ModelObject'),
+ ),
+ migrations.AddField(
+ model_name='modelchangelog',
+ name='information',
+ field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='automated_logging.ModelObject'),
+ ),
+ migrations.AddField(
+ model_name='modelchangelog',
+ name='inserted',
+ field=models.ManyToManyField(related_name='changelog_inserted', to='automated_logging.ModelObject'),
+ ),
+ migrations.AddField(
+ model_name='modelchangelog',
+ name='modification',
+ field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='automated_logging.ModelModification'),
+ ),
+ migrations.AddField(
+ model_name='modelchangelog',
+ name='removed',
+ field=models.ManyToManyField(related_name='changelog_removed', to='automated_logging.ModelObject'),
+ ),
+ migrations.AddField(
+ model_name='model',
+ name='information',
+ field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='atl_model_information', to='automated_logging.ModelObject'),
+ ),
+ migrations.AddField(
+ model_name='model',
+ name='modification',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='automated_logging.ModelChangelog'),
+ ),
+ migrations.AddField(
+ model_name='model',
+ name='user',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0002_auto_20180215_1540.py b/galaxy_ng/_vendor/automated_logging/migrations/0002_auto_20180215_1540.py
new file mode 100644
index 0000000000..c0df10ac90
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0002_auto_20180215_1540.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.2 on 2018-02-15 15:40
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Application',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ default=uuid.uuid4, primary_key=True, serialize=False
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('name', models.CharField(max_length=255)),
+ ],
+ options={'abstract': False,},
+ ),
+ migrations.RenameField(
+ model_name='modelobject', old_name='application', new_name='type',
+ ),
+ migrations.RemoveField(model_name='modelobject', name='model',),
+ migrations.RemoveField(model_name='model', name='application'),
+ migrations.AddField(
+ model_name='model',
+ name='application',
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='atl_model_application',
+ to='automated_logging.Application',
+ ),
+ ),
+ migrations.RemoveField(model_name='request', name='application'),
+ migrations.AddField(
+ model_name='request',
+ name='application',
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to='automated_logging.Application',
+ ),
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0003_auto_20180216_0900.py b/galaxy_ng/_vendor/automated_logging/migrations/0003_auto_20180216_0900.py
new file mode 100644
index 0000000000..11f35258d2
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0003_auto_20180216_0900.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.2 on 2018-02-16 09:00
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0002_auto_20180215_1540'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='modelobject',
+ name='value',
+ field=models.CharField(max_length=255, null=True),
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0004_auto_20180216_0935.py b/galaxy_ng/_vendor/automated_logging/migrations/0004_auto_20180216_0935.py
new file mode 100644
index 0000000000..ba93662f28
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0004_auto_20180216_0935.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.2 on 2018-02-16 09:35
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0003_auto_20180216_0900'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='modelobject',
+ name='type',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='atl_modelobject_application', to='contenttypes.ContentType'),
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0005_auto_20180216_0941.py b/galaxy_ng/_vendor/automated_logging/migrations/0005_auto_20180216_0941.py
new file mode 100644
index 0000000000..b237147f7a
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0005_auto_20180216_0941.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.2 on 2018-02-16 09:41
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0004_auto_20180216_0935'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='modelchangelog',
+ name='information',
+ field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='automated_logging.ModelObject'),
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0006_auto_20180216_1004.py b/galaxy_ng/_vendor/automated_logging/migrations/0006_auto_20180216_1004.py
new file mode 100644
index 0000000000..31d3cd3344
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0006_auto_20180216_1004.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.2 on 2018-02-16 10:04
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0005_auto_20180216_0941'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='model',
+ name='application',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='atl_model_application', to='automated_logging.Application'),
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0007_auto_20180216_1005.py b/galaxy_ng/_vendor/automated_logging/migrations/0007_auto_20180216_1005.py
new file mode 100644
index 0000000000..c67f272823
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0007_auto_20180216_1005.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.2 on 2018-02-16 10:05
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0006_auto_20180216_1004'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='request',
+ name='application',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='automated_logging.Application'),
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0008_auto_20180216_1005.py b/galaxy_ng/_vendor/automated_logging/migrations/0008_auto_20180216_1005.py
new file mode 100644
index 0000000000..92963baf3e
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0008_auto_20180216_1005.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.2 on 2018-02-16 10:05
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0007_auto_20180216_1005'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='model',
+ name='application',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='atl_model_application', to='automated_logging.Application'),
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0009_auto_20180216_1006.py b/galaxy_ng/_vendor/automated_logging/migrations/0009_auto_20180216_1006.py
new file mode 100644
index 0000000000..a248ca4595
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0009_auto_20180216_1006.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.2 on 2018-02-16 10:06
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0008_auto_20180216_1005'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='model',
+ name='application',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='atl_model_application', to='automated_logging.Application'),
+ ),
+ migrations.AlterField(
+ model_name='model',
+ name='information',
+ field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='atl_model_information', to='automated_logging.ModelObject'),
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0010_auto_20180216_1430.py b/galaxy_ng/_vendor/automated_logging/migrations/0010_auto_20180216_1430.py
new file mode 100644
index 0000000000..7dbf92ac15
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0010_auto_20180216_1430.py
@@ -0,0 +1,49 @@
+# Generated by Django 2.0.2 on 2018-02-16 14:30
+
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0009_auto_20180216_1006'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Field',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('name', models.CharField(max_length=255)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='ModelStorage',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('name', models.CharField(max_length=255)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.AddField(
+ model_name='field',
+ name='model',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='automated_logging.ModelStorage'),
+ ),
+ migrations.AddField(
+ model_name='modelobject',
+ name='field',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='automated_logging.Field'),
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0011_auto_20180216_1545.py b/galaxy_ng/_vendor/automated_logging/migrations/0011_auto_20180216_1545.py
new file mode 100644
index 0000000000..5c972ab79b
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0011_auto_20180216_1545.py
@@ -0,0 +1,29 @@
+# Generated by Django 2.0.2 on 2018-02-16 15:45
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0010_auto_20180216_1430'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='model',
+ options={'verbose_name': 'Changelog', 'verbose_name_plural': 'Changelogs'},
+ ),
+ migrations.AlterModelOptions(
+ name='modelchangelog',
+ options={},
+ ),
+ migrations.AlterModelOptions(
+ name='request',
+ options={'verbose_name': 'Request', 'verbose_name_plural': 'Requests'},
+ ),
+ migrations.AlterModelOptions(
+ name='unspecified',
+ options={'verbose_name': 'Non DJL Message', 'verbose_name_plural': 'Non DJL Messages'},
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0012_auto_20180218_1101.py b/galaxy_ng/_vendor/automated_logging/migrations/0012_auto_20180218_1101.py
new file mode 100644
index 0000000000..eb6456c2db
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0012_auto_20180218_1101.py
@@ -0,0 +1,24 @@
+# Generated by Django 2.0.2 on 2018-02-18 11:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0011_auto_20180216_1545'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='request',
+ name='method',
+ field=models.CharField(default='GET', max_length=64),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name='request',
+ name='url',
+ field=models.CharField(max_length=255),
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0013_auto_20180218_1106.py b/galaxy_ng/_vendor/automated_logging/migrations/0013_auto_20180218_1106.py
new file mode 100644
index 0000000000..45760e1892
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0013_auto_20180218_1106.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.0.2 on 2018-02-18 11:06
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0012_auto_20180218_1101'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='request',
+ name='url',
+ ),
+ migrations.AddField(
+ model_name='request',
+ name='uri',
+ field=models.URLField(default='/'),
+ preserve_default=False,
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0014_auto_20180219_0859.py b/galaxy_ng/_vendor/automated_logging/migrations/0014_auto_20180219_0859.py
new file mode 100644
index 0000000000..f8910b4a0b
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0014_auto_20180219_0859.py
@@ -0,0 +1,31 @@
+# Generated by Django 2.0.2 on 2018-02-19 08:59
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0013_auto_20180218_1106'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='request',
+ name='status',
+ field=models.PositiveSmallIntegerField(null=True),
+ ),
+ migrations.RemoveField(model_name='field', name='model'),
+ migrations.AddField(
+ model_name='field',
+ name='model',
+ field=models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='dal_field',
+ to='contenttypes.ContentType',
+ ),
+ ),
+ migrations.DeleteModel(name='ModelStorage',),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0015_auto_20181229_2323.py b/galaxy_ng/_vendor/automated_logging/migrations/0015_auto_20181229_2323.py
new file mode 100644
index 0000000000..c883081449
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0015_auto_20181229_2323.py
@@ -0,0 +1,61 @@
+# Generated by Django 2.1.1 on 2018-12-29 23:23
+
+from django.db import migrations, models
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0014_auto_20180219_0859'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='LDAP',
+ ),
+ migrations.AlterModelOptions(
+ name='unspecified',
+ options={'verbose_name': 'Non DAL Message', 'verbose_name_plural': 'Non DAL Messages'},
+ ),
+ migrations.AlterField(
+ model_name='application',
+ name='id',
+ field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False),
+ ),
+ migrations.AlterField(
+ model_name='field',
+ name='id',
+ field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False),
+ ),
+ migrations.AlterField(
+ model_name='model',
+ name='id',
+ field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False),
+ ),
+ migrations.AlterField(
+ model_name='modelchangelog',
+ name='id',
+ field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False),
+ ),
+ migrations.AlterField(
+ model_name='modelmodification',
+ name='id',
+ field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False),
+ ),
+ migrations.AlterField(
+ model_name='modelobject',
+ name='id',
+ field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False),
+ ),
+ migrations.AlterField(
+ model_name='request',
+ name='id',
+ field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False),
+ ),
+ migrations.AlterField(
+ model_name='unspecified',
+ name='id',
+ field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False),
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0016_auto_20200803_1917.py b/galaxy_ng/_vendor/automated_logging/migrations/0016_auto_20200803_1917.py
new file mode 100644
index 0000000000..d90be04d8d
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0016_auto_20200803_1917.py
@@ -0,0 +1,788 @@
+# Generated by Django 3.0.7 on 2020-08-03 19:17
+# Edited by Bilal Mahmoud for 5.x.x to 6.x.x conversion
+
+from django.db import migrations
+from django.conf import settings
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import picklefield.fields
+import uuid
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def convert(apps, schema_editor):
+ """ convert from 5.x.x to 6.x.x """
+ alias = schema_editor.connection.alias
+
+ # convert all new applications
+ logger.info('Converting Applications from 5.x.x to 6.x.x')
+ # Application does not change, except new unknown Application
+ Application = apps.get_model('automated_logging', 'Application')
+ applications = [Application(name=None)]
+ no_application = applications[0]
+
+ # convert request events
+ logger.info('Converting Request Events from 5.x.x to 6.x.x')
+ RequestOld = apps.get_model('automated_logging', 'Request')
+ RequestEvent = apps.get_model('automated_logging', 'RequestEvent')
+ requests = [
+ RequestEvent(
+ id=r.id,
+ created_at=r.created_at,
+ updated_at=r.updated_at,
+ user=r.user,
+ uri=str(r.uri),
+ method=r.method[:32],
+ status=r.status,
+ application=Application.objects.using(alias).get(id=r.application.id)
+ if r.application
+ else no_application,
+ )
+ for r in RequestOld.objects.using(alias).all()
+ ]
+
+ # convert unspecified events
+ logger.info('Converting Unspecified Events from 5.x.x to 6.x.x')
+ UnspecifiedOld = apps.get_model('automated_logging', 'Unspecified')
+ UnspecifiedEvent = apps.get_model('automated_logging', 'UnspecifiedEvent')
+ unspecified = [
+ UnspecifiedEvent(
+ id=u.id,
+ created_at=u.created_at,
+ updated_at=u.updated_at,
+ message=u.message,
+ level=u.level,
+ file=u.file,
+ line=u.line,
+ application=no_application,
+ )
+ for u in UnspecifiedOld.objects.using(alias).all()
+ ]
+
+ # convert model events
+ logger.info(
+ 'Converting Models Events from 5.x.x to 6.x.x (This might take a while)'
+ )
+ ModelOld = apps.get_model('automated_logging', 'Model')
+ ModelEvent = apps.get_model('automated_logging', 'ModelEvent')
+
+ ModelMirror = apps.get_model('automated_logging', 'ModelMirror')
+ ModelEntry = apps.get_model('automated_logging', 'ModelEntry')
+ ModelField = apps.get_model('automated_logging', 'ModelField')
+ ModelRelationshipModification = apps.get_model(
+ 'automated_logging', 'ModelRelationshipModification'
+ )
+ ModelValueModification = apps.get_model(
+ 'automated_logging', 'ModelValueModification'
+ )
+
+ mirrors = [ModelMirror(name='', application=no_application)]
+ no_mirror = mirrors[0]
+
+ entries = [ModelEntry(value='', primary_key='', mirror=no_mirror)]
+ no_entry = entries[0]
+
+ fields = [ModelField(name='', mirror=no_mirror, type='')]
+ no_field = fields[0]
+
+ events = []
+
+ value_modifications = []
+ relationship_modifications = []
+
+ def get_application(content_type):
+ """
+ simple helper function to get a new application from a content type
+ :return: ApplicationNew
+ """
+ app = next(
+ (a for a in applications if a.name == content_type.app_label),
+ None,
+ )
+
+ if not app:
+ app = Application(name=content_type.app_label)
+ applications.append(app)
+
+ return app
+
+ def get_mirror(content_type):
+ """
+ simple helper function to get the new mirror from content type
+ :return: ModelMirror
+ """
+ app = get_application(content_type)
+ mir = next((m for m in mirrors if m.name == content_type.model), None)
+ if not mir:
+ mir = ModelMirror(name=content_type.model, application=app)
+ mirrors.append(mir)
+
+ return mir
+
+ def get_entry(value, mir):
+ """
+ simple helper function to find the appropriate entry
+ :return: ModelEntry
+ """
+ ent = next((e for e in entries if e.value == value), None)
+ if not ent:
+ ent = ModelEntry(value=value, primary_key='', mirror=mir)
+ entries.append(ent)
+ return ent
+
+ def get_field(target):
+ """
+ simple inline helper function to get the correct field
+ :return: ModelField
+ """
+
+ if target is None:
+ return no_field
+
+ app = get_application(target.model)
+
+ mir = next(
+ (m for m in mirrors if m.application.name == target.model.app_label),
+ None,
+ )
+ if not mir:
+ mir = ModelMirror(name=target.model.model, application=app)
+
+ fie = next(
+ (e for e in fields if e.name == target.name and e.mirror == mir),
+ None,
+ )
+ if not fie:
+ fie = ModelField(name=target.name, mirror=mir, type='')
+ fields.append(fie)
+
+ return fie
+
+ oldies = ModelOld.objects.using(alias).all()
+
+ progress = oldies.count() // 10
+ idx = 0
+ for old in oldies:
+ event = ModelEvent(
+ id=old.id, created_at=old.created_at, updated_at=old.updated_at
+ )
+
+ # converting old.information
+ # old.information -> event.model
+ # value => value
+ # content_type => ModelMirror
+ # app_label => Application
+ # model => ModelMirror.name
+ # provide some defaults, so that if no content_type is there
+ # the program doesn't poo all over the floor.
+ entry = no_entry
+ if old.information:
+ mirror = no_mirror
+
+ # there is a chance old.information.type is null,
+ # we need to check for that case
+ if old.information.type:
+ mirror = get_mirror(old.information.type)
+
+ entry = next((e for e in entries if e.value == old.information.value), None)
+ if not entry:
+ entry = ModelEntry(
+ value=old.information.value, primary_key='', mirror=mirror
+ )
+ entries.append(entry)
+ event.entry = entry
+
+ ACTION_TRANSLATIONS = {0: None, 1: 1, 2: 0, 3: -1}
+
+ # old.modification -> event.modifications, event.relationships
+ # if information has content_type then relationship, else modification
+ # problem: field is unknown
+ # modification => modifications
+ # operation.YOUCANDECIDE => operation
+ # previously => previous
+ # currently => current
+ # inserted => relationship
+ # removed => relationship
+
+ event.operation = ACTION_TRANSLATIONS[old.action]
+
+ if old.modification:
+ for inserted in old.modification.inserted.all():
+ mirror = get_mirror(inserted.type)
+ rel = ModelRelationshipModification(
+ field=get_field(inserted.field),
+ entry=get_entry(inserted.value, mirror),
+ operation=1,
+ )
+ rel.event = event
+ relationship_modifications.append(rel)
+
+ for removed in old.modification.removed.all():
+ mirror = get_mirror(removed.type)
+ rel = ModelRelationshipModification(
+ field=get_field(removed.field),
+ entry=get_entry(removed.value, mirror),
+ operation=-1,
+ )
+ rel.event = event
+ relationship_modifications.append(rel)
+
+ if old.modification.modification:
+ previously = {
+ f.field.id: f
+ for f in old.modification.modification.previously.all()
+ }
+ for currently in old.modification.modification.currently.all():
+ # skip None string values
+ if currently.value == 'None':
+ currently.value = None
+
+ operation = 0
+ previous = None
+ if currently.field.id not in previously:
+ operation = 1
+ else:
+ previous = previously[currently.field.id].value
+ # previous can be "None" string
+ if previous == 'None':
+ operation = 1
+ previous = None
+ if previous is not None and currently.value is None:
+ operation = -1
+
+ val = ModelValueModification(
+ operation=operation,
+ field=get_field(currently.field),
+ previous=previous,
+ current=currently.value,
+ )
+ val.event = event
+ value_modifications.append(val)
+
+ for removed in set(
+ p.field.id for p in old.modification.modification.previously.all()
+ ).difference(
+ c.field.id for c in old.modification.modification.currently.all()
+ ):
+ removed = previously[removed]
+ # skip None string values
+ if removed.value == 'None':
+ continue
+
+ val = ModelValueModification(
+ operation=-1,
+ field=get_field(removed.field),
+ previous=removed.value,
+ current=None,
+ )
+ val.event = event
+ value_modifications.append(val)
+
+ event.user = old.user
+ events.append(event)
+ if idx % progress == 0:
+ logger.info(f'{(idx // progress) * 10}%...')
+ idx += 1
+
+ logger.info('Bulk Saving Converted Objects (This can take a while)')
+ Application.objects.using(alias).bulk_create(applications)
+ RequestEvent.objects.using(alias).bulk_create(requests)
+ UnspecifiedEvent.objects.using(alias).bulk_create(unspecified)
+ logger.info('Saved Application, RequestEvent and UnspecifiedEvent')
+
+ ModelMirror.objects.using(alias).bulk_create(mirrors)
+ ModelEntry.objects.using(alias).bulk_create(entries)
+ ModelField.objects.using(alias).bulk_create(fields)
+ logger.info('Saved ModelMirror, ModelEntry, ModelField')
+
+ ModelEvent.objects.using(alias).bulk_create(events)
+
+ ModelValueModification.objects.using(alias).bulk_create(value_modifications)
+ ModelRelationshipModification.objects.using(alias).bulk_create(
+ relationship_modifications
+ )
+
+ logger.info(
+ 'Saved ModelValueModification, ModelRelationshipModification and ModelEvent'
+ )
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('automated_logging', '0015_auto_20181229_2323'),
+ ]
+
+ operations = [
+ # migrations.CreateModel(
+ # # create temporary model
+ # # will be renamed
+ # name='ApplicationTemp',
+ # fields=[
+ # (
+ # 'id',
+ # models.UUIDField(
+ # db_index=True,
+ # default=uuid.uuid4,
+ # primary_key=True,
+ # serialize=False,
+ # ),
+ # ),
+ # ('created_at', models.DateTimeField(auto_now_add=True)),
+ # ('updated_at', models.DateTimeField(auto_now=True)),
+ # ('name', models.CharField(max_length=255, null=True)),
+ # ],
+ # options={
+ # 'verbose_name': 'Application',
+ # 'verbose_name_plural': 'Applications',
+ # },
+ # ),
+ migrations.CreateModel(
+ name='ModelEntry',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ db_index=True,
+ default=uuid.uuid4,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('value', models.TextField()),
+ ('primary_key', models.TextField()),
+ ],
+ options={
+ 'verbose_name': 'Model Entry',
+ 'verbose_name_plural': 'Model Entries',
+ },
+ ),
+ migrations.CreateModel(
+ name='ModelEvent',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ db_index=True,
+ default=uuid.uuid4,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ (
+ 'operation',
+ models.SmallIntegerField(
+ choices=[(1, 'create'), (0, 'modify'), (-1, 'delete')],
+ null=True,
+ validators=[
+ django.core.validators.MinValueValidator(-1),
+ django.core.validators.MaxValueValidator(1),
+ ],
+ ),
+ ),
+ (
+ 'snapshot',
+ picklefield.fields.PickledObjectField(editable=False, null=True),
+ ),
+ ('performance', models.DurationField(null=True)),
+ (
+ 'entry',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to='automated_logging.ModelEntry',
+ ),
+ ),
+ (
+ 'user',
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ options={
+ 'verbose_name': 'Model Event',
+ 'verbose_name_plural': 'Model Events',
+ },
+ ),
+ migrations.CreateModel(
+ name='ModelField',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ db_index=True,
+ default=uuid.uuid4,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('name', models.CharField(max_length=255)),
+ ('type', models.CharField(max_length=255)),
+ ],
+ options={
+ 'verbose_name': 'Model Field',
+ 'verbose_name_plural': 'Model Fields',
+ },
+ ),
+ migrations.CreateModel(
+ name='RequestContext',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ db_index=True,
+ default=uuid.uuid4,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ (
+ 'content',
+ picklefield.fields.PickledObjectField(editable=False, null=True),
+ ),
+ ('type', models.CharField(max_length=255)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='UnspecifiedEvent',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ db_index=True,
+ default=uuid.uuid4,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('message', models.TextField(null=True)),
+ ('level', models.PositiveIntegerField(default=20)),
+ ('line', models.PositiveIntegerField(null=True)),
+ ('file', models.TextField(null=True)),
+ (
+ 'application',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to='automated_logging.Application',
+ ),
+ ),
+ ],
+ options={
+ 'verbose_name': 'Unspecified Event',
+ 'verbose_name_plural': 'Unspecified Events',
+ },
+ ),
+ migrations.CreateModel(
+ name='RequestEvent',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ db_index=True,
+ default=uuid.uuid4,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('uri', models.TextField()),
+ ('status', models.PositiveSmallIntegerField()),
+ ('method', models.CharField(max_length=32)),
+ ('ip', models.GenericIPAddressField(null=True)),
+ (
+ 'application',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to='automated_logging.Application',
+ ),
+ ),
+ (
+ 'request',
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='request_context',
+ to='automated_logging.RequestContext',
+ ),
+ ),
+ (
+ 'response',
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='response_context',
+ to='automated_logging.RequestContext',
+ ),
+ ),
+ (
+ 'user',
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ options={
+ 'verbose_name': 'Request Event',
+ 'verbose_name_plural': 'Request Events',
+ },
+ ),
+ migrations.CreateModel(
+ name='ModelValueModification',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ db_index=True,
+ default=uuid.uuid4,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ (
+ 'operation',
+ models.SmallIntegerField(
+ choices=[(1, 'create'), (0, 'modify'), (-1, 'delete')],
+ null=True,
+ validators=[
+ django.core.validators.MinValueValidator(-1),
+ django.core.validators.MaxValueValidator(1),
+ ],
+ ),
+ ),
+ ('previous', models.TextField(null=True)),
+ ('current', models.TextField(null=True)),
+ (
+ 'event',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='modifications',
+ to='automated_logging.ModelEvent',
+ ),
+ ),
+ (
+ 'field',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to='automated_logging.ModelField',
+ ),
+ ),
+ ],
+ options={
+ 'verbose_name': 'Model Entry Event Value Modification',
+ 'verbose_name_plural': 'Model Entry Event Value Modifications',
+ },
+ ),
+ migrations.CreateModel(
+ name='ModelRelationshipModification',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ db_index=True,
+ default=uuid.uuid4,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ (
+ 'operation',
+ models.SmallIntegerField(
+ choices=[(1, 'create'), (0, 'modify'), (-1, 'delete')],
+ null=True,
+ validators=[
+ django.core.validators.MinValueValidator(-1),
+ django.core.validators.MaxValueValidator(1),
+ ],
+ ),
+ ),
+ (
+ 'event',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='relationships',
+ to='automated_logging.ModelEvent',
+ ),
+ ),
+ (
+ 'field',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to='automated_logging.ModelField',
+ ),
+ ),
+ (
+ 'entry',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to='automated_logging.ModelEntry',
+ ),
+ ),
+ ],
+ options={
+ 'verbose_name': 'Model Entry Event Relationship Modification',
+ 'verbose_name_plural': 'Model Entry Event Relationship Modifications',
+ },
+ ),
+ migrations.CreateModel(
+ name='ModelMirror',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ db_index=True,
+ default=uuid.uuid4,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('name', models.CharField(max_length=255)),
+ (
+ 'application',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to='automated_logging.Application',
+ ),
+ ),
+ ],
+ options={
+ 'verbose_name': 'Model Mirror',
+ 'verbose_name_plural': 'Model Mirrors',
+ },
+ ),
+ migrations.AddField(
+ model_name='modelfield',
+ name='mirror',
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to='automated_logging.ModelMirror',
+ ),
+ ),
+ migrations.AddField(
+ model_name='modelentry',
+ name='mirror',
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to='automated_logging.ModelMirror',
+ ),
+ ),
+ migrations.AlterField(
+ model_name='application',
+ name='name',
+ field=models.CharField(max_length=255, null=True),
+ ),
+ # end of adding
+ # conversion
+ migrations.RunPython(convert),
+ # migrations.RenameModel('ApplicationTemp', 'Application'),
+ # removing 5.x.x
+ migrations.RemoveField(
+ model_name='field',
+ name='model',
+ ),
+ migrations.RemoveField(
+ model_name='model',
+ name='application',
+ ),
+ migrations.RemoveField(
+ model_name='model',
+ name='information',
+ ),
+ migrations.RemoveField(
+ model_name='model',
+ name='modification',
+ ),
+ migrations.RemoveField(
+ model_name='model',
+ name='user',
+ ),
+ migrations.RemoveField(
+ model_name='modelchangelog',
+ name='information',
+ ),
+ migrations.RemoveField(
+ model_name='modelchangelog',
+ name='inserted',
+ ),
+ migrations.RemoveField(
+ model_name='modelchangelog',
+ name='modification',
+ ),
+ migrations.RemoveField(
+ model_name='modelchangelog',
+ name='removed',
+ ),
+ migrations.RemoveField(
+ model_name='modelmodification',
+ name='currently',
+ ),
+ migrations.RemoveField(
+ model_name='modelmodification',
+ name='previously',
+ ),
+ migrations.RemoveField(
+ model_name='modelobject',
+ name='field',
+ ),
+ migrations.RemoveField(
+ model_name='modelobject',
+ name='type',
+ ),
+ migrations.RemoveField(
+ model_name='request',
+ name='application',
+ ),
+ migrations.RemoveField(
+ model_name='request',
+ name='user',
+ ),
+ migrations.DeleteModel(
+ name='Unspecified',
+ ),
+ # migrations.DeleteModel(name='Application',),
+ migrations.DeleteModel(
+ name='Field',
+ ),
+ migrations.DeleteModel(
+ name='Model',
+ ),
+ migrations.DeleteModel(
+ name='ModelChangelog',
+ ),
+ migrations.DeleteModel(
+ name='ModelModification',
+ ),
+ migrations.DeleteModel(
+ name='ModelObject',
+ ),
+ migrations.DeleteModel(
+ name='Request',
+ ),
+ # end of removing
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0017_auto_20200819_1004.py b/galaxy_ng/_vendor/automated_logging/migrations/0017_auto_20200819_1004.py
new file mode 100644
index 0000000000..4372363138
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0017_auto_20200819_1004.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.0.7 on 2020-08-19 10:04
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('automated_logging', '0016_auto_20200803_1917'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='application',
+ options={'verbose_name': 'Application', 'verbose_name_plural': 'Applications'},
+ ),
+ ]
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/0018_decoratoroverrideexclusiontest_foreignkeytest_fullclassbasedexclusiontest_fulldecoratorbasedexclusio.py b/galaxy_ng/_vendor/automated_logging/migrations/0018_decoratoroverrideexclusiontest_foreignkeytest_fullclassbasedexclusiontest_fulldecoratorbasedexclusio.py
new file mode 100644
index 0000000000..454608683b
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/migrations/0018_decoratoroverrideexclusiontest_foreignkeytest_fullclassbasedexclusiontest_fulldecoratorbasedexclusio.py
@@ -0,0 +1,282 @@
+# Generated by Django 3.0.7 on 2020-09-13 18:33
+
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+ from django.conf import settings
+
+ dependencies = [
+ ('automated_logging', '0017_auto_20200819_1004'),
+ ]
+
+ if hasattr(settings, 'AUTOMATED_LOGGING_DEV') and settings.AUTOMATED_LOGGING_DEV:
+ operations = [
+ migrations.CreateModel(
+ name='DecoratorOverrideExclusionTest',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ default=uuid.uuid4, primary_key=True, serialize=False
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('random', models.CharField(max_length=255, null=True)),
+ ('random2', models.CharField(max_length=255, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='FullClassBasedExclusionTest',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ default=uuid.uuid4, primary_key=True, serialize=False
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('random', models.CharField(max_length=255, null=True)),
+ ('random2', models.CharField(max_length=255, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='FullDecoratorBasedExclusionTest',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ default=uuid.uuid4, primary_key=True, serialize=False
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('random', models.CharField(max_length=255, null=True)),
+ ('random2', models.CharField(max_length=255, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='OrdinaryTest',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ default=uuid.uuid4, primary_key=True, serialize=False
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('random', models.CharField(max_length=255, null=True)),
+ ('random2', models.CharField(max_length=255, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='PartialClassBasedExclusionTest',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ default=uuid.uuid4, primary_key=True, serialize=False
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('random', models.CharField(max_length=255, null=True)),
+ ('random2', models.CharField(max_length=255, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='PartialDecoratorBasedExclusionTest',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ default=uuid.uuid4, primary_key=True, serialize=False
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('random', models.CharField(max_length=255, null=True)),
+ ('random2', models.CharField(max_length=255, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='SpeedTest',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ default=uuid.uuid4, primary_key=True, serialize=False
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('column0', models.CharField(max_length=15, null=True)),
+ ('column1', models.CharField(max_length=15, null=True)),
+ ('column2', models.CharField(max_length=15, null=True)),
+ ('column3', models.CharField(max_length=15, null=True)),
+ ('column4', models.CharField(max_length=15, null=True)),
+ ('column5', models.CharField(max_length=15, null=True)),
+ ('column6', models.CharField(max_length=15, null=True)),
+ ('column7', models.CharField(max_length=15, null=True)),
+ ('column8', models.CharField(max_length=15, null=True)),
+ ('column9', models.CharField(max_length=15, null=True)),
+ ('column10', models.CharField(max_length=15, null=True)),
+ ('column11', models.CharField(max_length=15, null=True)),
+ ('column12', models.CharField(max_length=15, null=True)),
+ ('column13', models.CharField(max_length=15, null=True)),
+ ('column14', models.CharField(max_length=15, null=True)),
+ ('column15', models.CharField(max_length=15, null=True)),
+ ('column16', models.CharField(max_length=15, null=True)),
+ ('column17', models.CharField(max_length=15, null=True)),
+ ('column18', models.CharField(max_length=15, null=True)),
+ ('column19', models.CharField(max_length=15, null=True)),
+ ('column20', models.CharField(max_length=15, null=True)),
+ ('column21', models.CharField(max_length=15, null=True)),
+ ('column22', models.CharField(max_length=15, null=True)),
+ ('column23', models.CharField(max_length=15, null=True)),
+ ('column24', models.CharField(max_length=15, null=True)),
+ ('column25', models.CharField(max_length=15, null=True)),
+ ('column26', models.CharField(max_length=15, null=True)),
+ ('column27', models.CharField(max_length=15, null=True)),
+ ('column28', models.CharField(max_length=15, null=True)),
+ ('column29', models.CharField(max_length=15, null=True)),
+ ('column30', models.CharField(max_length=15, null=True)),
+ ('column31', models.CharField(max_length=15, null=True)),
+ ('column32', models.CharField(max_length=15, null=True)),
+ ('column33', models.CharField(max_length=15, null=True)),
+ ('column34', models.CharField(max_length=15, null=True)),
+ ('column35', models.CharField(max_length=15, null=True)),
+ ('column36', models.CharField(max_length=15, null=True)),
+ ('column37', models.CharField(max_length=15, null=True)),
+ ('column38', models.CharField(max_length=15, null=True)),
+ ('column39', models.CharField(max_length=15, null=True)),
+ ('column40', models.CharField(max_length=15, null=True)),
+ ('column41', models.CharField(max_length=15, null=True)),
+ ('column42', models.CharField(max_length=15, null=True)),
+ ('column43', models.CharField(max_length=15, null=True)),
+ ('column44', models.CharField(max_length=15, null=True)),
+ ('column45', models.CharField(max_length=15, null=True)),
+ ('column46', models.CharField(max_length=15, null=True)),
+ ('column47', models.CharField(max_length=15, null=True)),
+ ('column48', models.CharField(max_length=15, null=True)),
+ ('column49', models.CharField(max_length=15, null=True)),
+ ('column50', models.CharField(max_length=15, null=True)),
+ ('column51', models.CharField(max_length=15, null=True)),
+ ('column52', models.CharField(max_length=15, null=True)),
+ ('column53', models.CharField(max_length=15, null=True)),
+ ('column54', models.CharField(max_length=15, null=True)),
+ ('column55', models.CharField(max_length=15, null=True)),
+ ('column56', models.CharField(max_length=15, null=True)),
+ ('column57', models.CharField(max_length=15, null=True)),
+ ('column58', models.CharField(max_length=15, null=True)),
+ ('column59', models.CharField(max_length=15, null=True)),
+ ('column60', models.CharField(max_length=15, null=True)),
+ ('column61', models.CharField(max_length=15, null=True)),
+ ('column62', models.CharField(max_length=15, null=True)),
+ ('column63', models.CharField(max_length=15, null=True)),
+ ('column64', models.CharField(max_length=15, null=True)),
+ ('column65', models.CharField(max_length=15, null=True)),
+ ('column66', models.CharField(max_length=15, null=True)),
+ ('column67', models.CharField(max_length=15, null=True)),
+ ('column68', models.CharField(max_length=15, null=True)),
+ ('column69', models.CharField(max_length=15, null=True)),
+ ('column70', models.CharField(max_length=15, null=True)),
+ ('column71', models.CharField(max_length=15, null=True)),
+ ('column72', models.CharField(max_length=15, null=True)),
+ ('column73', models.CharField(max_length=15, null=True)),
+ ('column74', models.CharField(max_length=15, null=True)),
+ ('column75', models.CharField(max_length=15, null=True)),
+ ('column76', models.CharField(max_length=15, null=True)),
+ ('column77', models.CharField(max_length=15, null=True)),
+ ('column78', models.CharField(max_length=15, null=True)),
+ ('column79', models.CharField(max_length=15, null=True)),
+ ('column80', models.CharField(max_length=15, null=True)),
+ ('column81', models.CharField(max_length=15, null=True)),
+ ('column82', models.CharField(max_length=15, null=True)),
+ ('column83', models.CharField(max_length=15, null=True)),
+ ('column84', models.CharField(max_length=15, null=True)),
+ ('column85', models.CharField(max_length=15, null=True)),
+ ('column86', models.CharField(max_length=15, null=True)),
+ ('column87', models.CharField(max_length=15, null=True)),
+ ('column88', models.CharField(max_length=15, null=True)),
+ ('column89', models.CharField(max_length=15, null=True)),
+ ('column90', models.CharField(max_length=15, null=True)),
+ ('column91', models.CharField(max_length=15, null=True)),
+ ('column92', models.CharField(max_length=15, null=True)),
+ ('column93', models.CharField(max_length=15, null=True)),
+ ('column94', models.CharField(max_length=15, null=True)),
+ ('column95', models.CharField(max_length=15, null=True)),
+ ('column96', models.CharField(max_length=15, null=True)),
+ ('column97', models.CharField(max_length=15, null=True)),
+ ('column98', models.CharField(max_length=15, null=True)),
+ ('column99', models.CharField(max_length=15, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='OneToOneTest',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ default=uuid.uuid4, primary_key=True, serialize=False
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ (
+ 'relationship',
+ models.OneToOneField(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to='automated_logging.OrdinaryTest',
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name='M2MTest',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ default=uuid.uuid4, primary_key=True, serialize=False
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ (
+ 'relationship',
+ models.ManyToManyField(to='automated_logging.OrdinaryTest'),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ForeignKeyTest',
+ fields=[
+ (
+ 'id',
+ models.UUIDField(
+ default=uuid.uuid4, primary_key=True, serialize=False
+ ),
+ ),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ (
+ 'relationship',
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to='automated_logging.OrdinaryTest',
+ ),
+ ),
+ ],
+ ),
+ ]
+
+ else:
+ operations = []
diff --git a/galaxy_ng/_vendor/automated_logging/migrations/__init__.py b/galaxy_ng/_vendor/automated_logging/migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/galaxy_ng/_vendor/automated_logging/models.py b/galaxy_ng/_vendor/automated_logging/models.py
new file mode 100755
index 0000000000..6cb072eaff
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/models.py
@@ -0,0 +1,355 @@
+"""
+Model definitions for django-automated-logging.
+"""
+import uuid
+
+from django.conf import settings
+from django.core.validators import MaxValueValidator, MinValueValidator
+from django.db import models
+from django.db.models import (
+ CharField,
+ ForeignKey,
+ CASCADE,
+ TextField,
+ SmallIntegerField,
+ PositiveIntegerField,
+ DurationField,
+ GenericIPAddressField,
+ PositiveSmallIntegerField,
+)
+from picklefield.fields import PickledObjectField
+
+from automated_logging.helpers import Operation
+from automated_logging.helpers.enums import (
+ DjangoOperations,
+ PastM2MOperationMap,
+ ShortOperationMap,
+)
+from automated_logging.settings import dev
+
+
+class BaseModel(models.Model):
+ """BaseModel that is inherited from every model. Includes basic information."""
+
+ id = models.UUIDField(default=uuid.uuid4, primary_key=True, db_index=True)
+
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ abstract = True
+
+
+class Application(BaseModel):
+ """
+ Used to save from which application an event or model originates.
+ This is used to group by application.
+
+ The application name can be null,
+ if the name is None, then the application is unknown.
+ """
+
+ name = CharField(max_length=255, null=True)
+
+ class Meta:
+ verbose_name = "Application"
+ verbose_name_plural = "Applications"
+
+ class LoggingIgnore:
+ complete = True
+
+ def __str__(self):
+ return self.name or "Unknown"
+
+
+class ModelMirror(BaseModel):
+ """
+ Used to mirror properties of models - this is used to preserve logs of
+ models removed to make the logs independent of the presence of the model
+ in the application.
+ """
+
+ name = CharField(max_length=255)
+ application = ForeignKey(Application, on_delete=CASCADE)
+
+ class Meta:
+ verbose_name = "Model Mirror"
+ verbose_name_plural = "Model Mirrors"
+
+ class LoggingIgnore:
+ complete = True
+
+ def __str__(self):
+ return self.name
+
+
+class ModelField(BaseModel):
+ """
+ Used to mirror properties of model fields - this is used to preserve logs of
+ models and fields that might be removed/modified and have them independent
+ of the actual field.
+ """
+
+ name = CharField(max_length=255)
+
+ mirror = ForeignKey(ModelMirror, on_delete=CASCADE)
+ type = CharField(max_length=255) # string of type
+
+ class Meta:
+ verbose_name = "Model Field"
+ verbose_name_plural = "Model Fields"
+
+ class LoggingIgnore:
+ complete = True
+
+
+class ModelEntry(BaseModel):
+ """
+ Used to mirror the evaluated model value (via repr) and primary key and
+ to ensure the log integrity independent of presence of the entry.
+ """
+
+ mirror = ForeignKey(ModelMirror, on_delete=CASCADE)
+
+ value = TextField() # (repr)
+ primary_key = TextField()
+
+ class Meta:
+ verbose_name = "Model Entry"
+ verbose_name_plural = "Model Entries"
+
+ class LoggingIgnore:
+ complete = True
+
+ def __str__(self) -> str:
+ return f'{self.mirror.name}' f'(pk="{self.primary_key}", value="{self.value}")'
+
+ def long(self) -> str:
+ """
+ long representation
+ """
+
+ return f'{self.mirror.application.name}.{self})'
+
+ def short(self) -> str:
+ """
+ short representation
+ """
+ return f'{self.mirror.name}({self.primary_key})'
+
+
+class ModelEvent(BaseModel):
+ """
+ Used to record model entry events, like modification, removal or adding of
+ values or relationships.
+ """
+
+ operation = SmallIntegerField(
+ validators=[MinValueValidator(-1), MaxValueValidator(1)],
+ null=True,
+ choices=DjangoOperations,
+ )
+
+ user = ForeignKey(
+ settings.AUTH_USER_MODEL, on_delete=CASCADE, null=True
+ ) # maybe don't cascade?
+ entry = ForeignKey(ModelEntry, on_delete=CASCADE)
+
+ # modifications = None # One2Many -> ModelModification
+ # relationships = None # One2Many -> ModelRelationship
+
+ # v experimental, opt-in (pickled object)
+ snapshot = PickledObjectField(null=True)
+ performance = DurationField(null=True)
+
+ class Meta:
+ verbose_name = "Model Event"
+ verbose_name_plural = "Model Events"
+
+ class LoggingIgnore:
+ complete = True
+
+
+class ModelValueModification(BaseModel):
+ """
+ Used to record the model entry event modifications of simple values.
+
+ The operation attribute can have 4 valid values:
+ -1 (delete), 0 (modify), 1 (create), None (n/a)
+
+ previous and current record the value change that happened.
+ """
+
+ operation = SmallIntegerField(
+ validators=[MinValueValidator(-1), MaxValueValidator(1)],
+ null=True,
+ choices=DjangoOperations,
+ )
+
+ field = ForeignKey(ModelField, on_delete=CASCADE)
+
+ previous = TextField(null=True)
+ current = TextField(null=True)
+
+ event = ForeignKey(ModelEvent, on_delete=CASCADE, related_name='modifications')
+
+ class Meta:
+ verbose_name = "Model Entry Event Value Modification"
+ verbose_name_plural = "Model Entry Event Value Modifications"
+
+ class LoggingIgnore:
+ complete = True
+
+ def __str__(self) -> str:
+ return (
+ f'[{self.field.mirror.application.name}:'
+ f'{self.field.mirror.name}:'
+ f'{self.field.name}] '
+ f'{self.previous} -> {self.current}'
+ )
+
+ def short(self) -> str:
+ """
+ short representation analogue of __str__
+ """
+ operation = Operation(self.operation)
+ shorthand = {v: k for k, v in ShortOperationMap.items()}[operation]
+
+ return f'{shorthand}{self.field.name}'
+
+
+class ModelRelationshipModification(BaseModel):
+ """
+ Used to record the model entry even modifications of relationships. (M2M, Foreign)
+
+
+ The operation attribute can have 4 valid values:
+ -1 (delete), 0 (modify), 1 (create), None (n/a)
+
+ field is the field where the relationship changed (entry got added or removed)
+ and model is the entry that got removed/added from the relationship.
+ """
+
+ operation = SmallIntegerField(
+ validators=[MinValueValidator(-1), MaxValueValidator(1)],
+ null=True,
+ choices=DjangoOperations,
+ )
+
+ field = ForeignKey(ModelField, on_delete=CASCADE)
+ entry = ForeignKey(ModelEntry, on_delete=CASCADE)
+
+ event = ForeignKey(ModelEvent, on_delete=CASCADE, related_name='relationships')
+
+ class Meta:
+ verbose_name = "Model Entry Event Relationship Modification"
+ verbose_name_plural = "Model Entry Event Relationship Modifications"
+
+ class LoggingIgnore:
+ complete = True
+
+ def __str__(self) -> str:
+ operation = Operation(self.operation)
+ past = {v: k for k, v in PastM2MOperationMap.items()}[operation]
+
+ return (
+ f'[{self.field.mirror.application}:'
+ f'{self.field.mirror.name}:'
+ f'{self.field.name}] '
+ f'{past} {self.entry}'
+ )
+
+ def short(self) -> str:
+ """
+ short representation
+ """
+ operation = Operation(self.operation)
+ shorthand = {v: k for k, v in ShortOperationMap.items()}[operation]
+ return f'{shorthand}{self.entry.short()}'
+
+ def medium(self) -> [str, str]:
+ """
+ short representation analogue of __str__ with additional field context
+ :return:
+ """
+ operation = Operation(self.operation)
+ shorthand = {v: k for k, v in ShortOperationMap.items()}[operation]
+
+ return f'{shorthand}{self.field.name}', f'{self.entry.short()}'
+
+
+class RequestContext(BaseModel):
+ """
+ Used to record contents of request and responses and their type.
+ """
+
+ content = PickledObjectField(null=True)
+ type = CharField(max_length=255)
+
+ class LoggingIgnore:
+ complete = True
+
+
+class RequestEvent(BaseModel):
+ """
+ Used to record events of requests that happened.
+
+ uri is the accessed path and data is the data that was being transmitted
+ and is opt-in for collection.
+
+ status and method are their respective HTTP equivalents.
+ """
+
+ user = ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE, null=True)
+
+ # to mitigate "max_length"
+ uri = TextField()
+
+ request = ForeignKey(
+ RequestContext, on_delete=CASCADE, null=True, related_name='request_context'
+ )
+ response = ForeignKey(
+ RequestContext, on_delete=CASCADE, null=True, related_name='response_context'
+ )
+
+ status = PositiveSmallIntegerField()
+ method = CharField(max_length=32)
+
+ application = ForeignKey(Application, on_delete=CASCADE)
+
+ ip = GenericIPAddressField(null=True)
+
+ class Meta:
+ verbose_name = "Request Event"
+ verbose_name_plural = "Request Events"
+
+ class LoggingIgnore:
+ complete = True
+
+
+class UnspecifiedEvent(BaseModel):
+ """
+ Used to record unspecified internal events that are dispatched via
+ the python logging library. saves the message, level, line, file and application.
+ """
+
+ message = TextField(null=True)
+ level = PositiveIntegerField(default=20)
+
+ line = PositiveIntegerField(null=True)
+ file = TextField(null=True)
+
+ application = ForeignKey(Application, on_delete=CASCADE)
+
+ class Meta:
+ verbose_name = "Unspecified Event"
+ verbose_name_plural = "Unspecified Events"
+
+ class LoggingIgnore:
+ complete = True
+
+
+if dev:
+ # if in development mode (set when testing or development)
+ # import extra models
+ from automated_logging.tests.models import *
diff --git a/galaxy_ng/_vendor/automated_logging/settings.py b/galaxy_ng/_vendor/automated_logging/settings.py
new file mode 100644
index 0000000000..081236f5c1
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/settings.py
@@ -0,0 +1,259 @@
+"""
+Serialization of AUTOMATED_LOGGING_SETTINGS
+"""
+
+from collections import namedtuple
+from functools import lru_cache
+from logging import INFO, NOTSET, CRITICAL
+from pprint import pprint
+
+from marshmallow.fields import Boolean, Integer
+from marshmallow.validate import OneOf, Range
+
+from automated_logging.helpers.schemas import (
+ Set,
+ LowerCaseString,
+ SearchString,
+ MissingNested,
+ BaseSchema,
+ Search,
+ Duration,
+)
+
+
+class RequestExcludeSchema(BaseSchema):
+ """
+ Configuration schema for request exclusion, that is only used in RequestSchema,
+ is used to exclude unknown sources, applications, methods and status codes.
+ """
+
+ unknown = Boolean(missing=False)
+ applications = Set(SearchString(), missing=set())
+
+ methods = Set(LowerCaseString(), missing={'GET'})
+ status = Set(Integer(validate=Range(min=0)), missing={200})
+
+
+class RequestDataSchema(BaseSchema):
+ """
+ Configuration schema for request data that is only used in RequestSchema
+ and is used to enable data collection, ignore keys that are going to be omitted
+ mask keys (their value is going to be replaced with )
+ """
+
+ enabled = Set(
+ LowerCaseString(validate=OneOf(['request', 'response'])),
+ missing=set(),
+ )
+ query = Boolean(missing=False)
+
+ ignore = Set(LowerCaseString(), missing=set())
+ mask = Set(LowerCaseString(), missing={'password'})
+
+ # TODO: add more, change name?
+ content_types = Set(
+ LowerCaseString(validate=OneOf(['application/json'])),
+ missing={'application/json'},
+ )
+
+
+class RequestSchema(BaseSchema):
+ """
+ Configuration schema for the request module.
+ """
+
+ loglevel = Integer(missing=INFO, validate=Range(min=NOTSET, max=CRITICAL))
+ exclude = MissingNested(RequestExcludeSchema)
+
+ data = MissingNested(RequestDataSchema)
+
+ ip = Boolean(missing=True)
+ # TODO: performance setting?
+
+ log_request_was_not_recorded = Boolean(missing=True)
+ max_age = Duration(missing=None)
+
+
+class ModelExcludeSchema(BaseSchema):
+ """
+ Configuration schema, that is only used in ModelSchema and is used to
+ exclude unknown sources, fields, models and applications.
+
+ fields should be either (every field that matches this name will be excluded),
+ or ., or ..
+
+ models should be either (every model regardless of module or application).
+ (python module location) or . (python module location)
+ """
+
+ unknown = Boolean(missing=False)
+ fields = Set(SearchString(), missing=set())
+ models = Set(SearchString(), missing=set())
+ applications = Set(SearchString(), missing=set())
+
+
+class ModelSchema(BaseSchema):
+ """
+ Configuration schema for the model module. mask property indicates
+ which fields to specifically replace with ,
+ this should be used for fields that are
+ sensitive, but shouldn't be completely excluded.
+ """
+
+ loglevel = Integer(missing=INFO, validate=Range(min=NOTSET, max=CRITICAL))
+ exclude = MissingNested(ModelExcludeSchema)
+
+ # should the log message include all modifications done?
+ detailed_message = Boolean(missing=True)
+
+ # if execution_time should be measured of ModelEvent
+ performance = Boolean(missing=False)
+ snapshot = Boolean(missing=False)
+
+ max_age = Duration(missing=None)
+
+
+class UnspecifiedExcludeSchema(BaseSchema):
+ """
+ Configuration schema, that is only used in UnspecifiedSchema and defines
+ the configuration settings to allow unknown sources, exclude files and
+ specific Django applications
+ """
+
+ unknown = Boolean(missing=False)
+ files = Set(SearchString(), missing=set())
+ applications = Set(SearchString(), missing=set())
+
+
+class UnspecifiedSchema(BaseSchema):
+ """
+ Configuration schema for the unspecified module.
+ """
+
+ loglevel = Integer(missing=INFO, validate=Range(min=NOTSET, max=CRITICAL))
+ exclude = MissingNested(UnspecifiedExcludeSchema)
+
+ max_age = Duration(missing=None)
+
+
+class GlobalsExcludeSchema(BaseSchema):
+ """
+ Configuration schema, that is used for every single module.
+ There are some packages where it is sensible to have the same
+ exclusions.
+
+ Things specified in globals will get appended to the other configurations.
+ """
+
+ applications = Set(
+ SearchString(),
+ missing={
+ Search('glob', 'session*'),
+ Search('plain', 'admin'),
+ Search('plain', 'basehttp'),
+ Search('plain', 'migrations'),
+ Search('plain', 'contenttypes'),
+ },
+ )
+
+
+class GlobalsSchema(BaseSchema):
+ """
+ Configuration schema for global, module unspecific configuration details.
+ """
+
+ exclude = MissingNested(GlobalsExcludeSchema)
+
+
+class ConfigSchema(BaseSchema):
+ """
+ Skeleton configuration schema, that is used to enable/disable modules
+ and includes the nested module configurations.
+ """
+
+ modules = Set(
+ LowerCaseString(validate=OneOf(['request', 'model', 'unspecified'])),
+ missing={'request', 'model', 'unspecified'},
+ )
+
+ request = MissingNested(RequestSchema)
+ model = MissingNested(ModelSchema)
+ unspecified = MissingNested(UnspecifiedSchema)
+
+ globals = MissingNested(GlobalsSchema)
+
+
+default: namedtuple = ConfigSchema().load({})
+
+
+class Settings:
+ """
+ Settings wrapper,
+ with the wrapper we can force lru_cache to be
+ cleared on the specific instance
+ """
+
+ def __init__(self):
+ self.loaded = None
+ self.load()
+
+ @lru_cache()
+ def load(self):
+ """
+ loads settings from the schemes provided,
+ done via function to utilize LRU cache
+ """
+
+ from django.conf import settings as st
+
+ loaded: namedtuple = default
+
+ if hasattr(st, 'AUTOMATED_LOGGING'):
+ loaded = ConfigSchema().load(st.AUTOMATED_LOGGING)
+
+ # be sure `loaded` has globals as we're working with those,
+ # if that is not the case early return.
+ if not hasattr(loaded, 'globals'):
+ return loaded
+
+ # use the binary **or** operator to apply globals to Set() attributes
+ values = {}
+ for name in loaded._fields:
+ field = getattr(loaded, name)
+ values[name] = field
+
+ if not isinstance(field, tuple) or name == 'globals':
+ continue
+
+ values[name] = field | loaded.globals
+
+ self.loaded = loaded._replace(**values)
+ return self
+
+ def __getattr__(self, item):
+ # self.load() should only trigger when the cache is invalid
+ self.load()
+
+ return getattr(self.loaded, item)
+
+
+@lru_cache()
+def load_dev():
+ """
+ utilize LRU cache and local imports to always
+ have an up to date version of the settings
+
+ :return:
+ """
+ from django.conf import settings as st
+
+ return getattr(st, 'AUTOMATED_LOGGING_DEV', False)
+
+
+if __name__ == '__main__':
+ from automated_logging.helpers import namedtuple2dict
+
+ pprint(namedtuple2dict(default))
+
+settings = Settings()
+dev = load_dev()
diff --git a/galaxy_ng/_vendor/automated_logging/signals/__init__.py b/galaxy_ng/_vendor/automated_logging/signals/__init__.py
new file mode 100644
index 0000000000..35e9bee1e8
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/signals/__init__.py
@@ -0,0 +1,258 @@
+"""
+Helper functions that are specifically used in the signals only.
+"""
+
+import re
+from fnmatch import fnmatch
+from functools import lru_cache
+from pathlib import Path
+from typing import List, Optional, Callable, Any
+
+from automated_logging.helpers import (
+ get_or_create_meta,
+ get_or_create_thread,
+ function2path,
+ Operation,
+)
+from automated_logging.models import RequestEvent, UnspecifiedEvent
+import automated_logging.decorators
+from automated_logging.settings import settings
+from automated_logging.helpers.schemas import Search
+
+
+# suboptimal meta is also cached -> look into how to solve
+@lru_cache()
+def cached_model_exclusion(sender, meta, operation) -> bool:
+ """ cached so that we don't need to abuse ._meta and can invalidate the cache """
+ return model_exclusion(sender, meta, operation)
+
+
+def lazy_model_exclusion(instance, operation, sender) -> bool:
+ """
+ First look if the model has been excluded already
+ -> only then look if excluded.
+
+ Replaced by LRU-Cache.
+ """
+
+ return cached_model_exclusion(sender, instance._meta, operation)
+
+
+def candidate_in_scope(candidate: str, scope: List[Search]) -> bool:
+ """
+ Check if the candidate string is valid with the scope supplied,
+ the scope should be list of search strings - that can be either
+ glob, plain or regex
+
+ :param candidate: search string
+ :param scope: List of Search
+ :return: valid?
+ """
+
+ for search in scope:
+ match = False
+ if search.type == 'glob':
+ match = fnmatch(candidate.lower(), search.value.lower())
+ if search.type == 'plain':
+ match = candidate.lower() == search.value.lower()
+ if search.type == 'regex':
+ match = bool(re.match(search.value, candidate, re.IGNORECASE))
+
+ if match:
+ return True
+
+ return False
+
+
+def request_exclusion(event: RequestEvent, view: Optional[Callable] = None) -> bool:
+ """
+ Determine if a request should be ignored/excluded from getting
+ logged, these exclusions should be specified in the settings.
+
+ :param event: RequestEvent
+ :param view: Optional - function used by the resolver
+ :return: should be excluded?
+ """
+
+ if view:
+ thread, _ = get_or_create_thread()
+ ignore = thread.dal['ignore.views']
+ include = thread.dal['include.views']
+ path = function2path(view)
+
+ # if include None or method in include return False and don't
+ # check further, else just continue with checking
+ if path in include and (include[path] is None or event.method in include[path]):
+ return False
+
+ if (
+ path in ignore
+ # if ignored[compiled] is None, then no method will be ignored
+ and ignore[path] is not None
+ # ignored[compiled] == [] indicates all should be ignored
+ and (len(ignore[path]) == 0 or event.method in ignore[path])
+ ):
+ return True
+
+ exclusions = settings.request.exclude
+ if event.method.lower() in exclusions.methods:
+ return True
+
+ if event.application.name and candidate_in_scope(
+ event.application.name, exclusions.applications
+ ):
+ return True
+
+ if event.status in exclusions.status:
+ return True
+
+ # if the application.name = None, then the application is unknown.
+ # exclusions.unknown specifies if unknown should be excluded!
+ if not event.application.name and exclusions.unknown:
+ return True
+
+ return False
+
+
+def _function_model_exclusion(sender, scope: str, item: Any) -> Optional[bool]:
+ if not sender:
+ return None
+
+ thread, _ = get_or_create_thread()
+
+ # noinspection PyProtectedMember
+ ignore = automated_logging.decorators._exclude_models
+ # noinspection PyProtectedMember
+ include = automated_logging.decorators._include_models
+
+ path = function2path(sender)
+
+ if path in include:
+ items = getattr(include[path], scope)
+ if items is None or item in items:
+ return False
+
+ if path in ignore:
+ items = getattr(ignore[path], scope)
+ if items is not None and (len(items) == 0 or item in items):
+ return True
+
+ return None
+
+
+def model_exclusion(sender, meta, operation: Operation) -> bool:
+ """
+ Determine if the instance of a model should be excluded,
+ these exclusions should be specified in the settings.
+
+ :param meta:
+ :param sender:
+ :param operation:
+ :return: should be excluded?
+ """
+ decorators = _function_model_exclusion(sender, 'operations', operation)
+ if decorators is not None:
+ return decorators
+
+ if hasattr(sender, 'LoggingIgnore') and (
+ getattr(sender.LoggingIgnore, 'complete', False)
+ or {
+ Operation.CREATE: 'create',
+ Operation.MODIFY: 'modify',
+ Operation.DELETE: 'delete',
+ }[operation]
+ in [o.lower() for o in getattr(sender.LoggingIgnore, 'operations', [])]
+ ):
+ return True
+
+ exclusions = settings.model.exclude
+ module = sender.__module__
+ name = sender.__name__
+ application = meta.app_label
+
+ if (
+ candidate_in_scope(name, exclusions.models)
+ or candidate_in_scope(f'{module}.{name}', exclusions.models)
+ or candidate_in_scope(f'{application}.{name}', exclusions.models)
+ ):
+ return True
+
+ if candidate_in_scope(module, exclusions.models):
+ return True
+
+ if application and candidate_in_scope(application, exclusions.applications):
+ return True
+
+ # if there is no application string then we assume the model
+ # location is unknown, if the flag exclude.unknown = True, then we just exclude
+ if not application and exclusions.unknown:
+ return True
+
+ return False
+
+
+def field_exclusion(field: str, instance, sender=None) -> bool:
+ """
+ Determine if the field of an instance should be excluded.
+ """
+
+ decorators = _function_model_exclusion(sender, 'fields', field)
+ if decorators is not None:
+ return decorators
+
+ if hasattr(instance.__class__, 'LoggingIgnore') and (
+ getattr(instance.__class__.LoggingIgnore, 'complete', False)
+ or field in getattr(instance.__class__.LoggingIgnore, 'fields', [])
+ ):
+ return True
+
+ exclusions = settings.model.exclude
+ application = instance._meta.app_label
+ model = instance.__class__.__name__
+
+ if (
+ candidate_in_scope(field, exclusions.fields)
+ or candidate_in_scope(f'{model}.{field}', exclusions.fields)
+ or candidate_in_scope(f'{application}.{model}.{field}', exclusions.fields)
+ ):
+ return True
+
+ return False
+
+
+def unspecified_exclusion(event: UnspecifiedEvent) -> bool:
+ """
+ Determine if an unspecified event needs to be excluded.
+ """
+ exclusions = settings.unspecified.exclude
+
+ if event.application.name and candidate_in_scope(
+ event.application.name, exclusions.applications
+ ):
+ return True
+
+ if candidate_in_scope(str(event.file), exclusions.files):
+ return True
+
+ path = Path(event.file)
+ # match greedily by first trying the complete path, if that doesn't match try
+ # full relative and then complete relative.
+ if [
+ v
+ for v in exclusions.files
+ if v.type != 'regex'
+ and (
+ path.match(v.value)
+ or path.match(f'/*{v.value}')
+ or fnmatch(path, f'{v.value}/*')
+ or fnmatch(path, f'/*{v.value}')
+ or fnmatch(path, f'/*{v.value}/*')
+ )
+ ]:
+ return True
+
+ # application.name = None and exclusion.unknown = True
+ if not event.application.name and exclusions.unknown:
+ return True
+
+ return False
diff --git a/galaxy_ng/_vendor/automated_logging/signals/m2m.py b/galaxy_ng/_vendor/automated_logging/signals/m2m.py
new file mode 100644
index 0000000000..c44c20ed53
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/signals/m2m.py
@@ -0,0 +1,195 @@
+"""
+This module specifically handlers "many to many" changes, those are
+a bit more complicated as we need to detect the changes
+on a per field basis.
+
+This finds the changes and redirects them to the handler,
+without doing any changes to the database.
+"""
+
+
+import logging
+from typing import Optional
+
+from django.db.models import Manager
+from django.db.models.fields.related import ManyToManyField
+from django.db.models.signals import m2m_changed
+from django.dispatch import receiver
+
+from automated_logging.helpers import (
+ Operation,
+ get_or_create_model_event,
+ get_or_create_meta,
+)
+from automated_logging.models import (
+ ModelRelationshipModification,
+ ModelEntry,
+ ModelMirror,
+ Application,
+ ModelField,
+)
+from automated_logging.settings import settings
+from automated_logging.signals import lazy_model_exclusion
+
+logger = logging.getLogger("automated_logging")
+
+
+def find_m2m_rel(sender, model) -> Optional[ManyToManyField]:
+ """
+ This finds the "many to many" relationship that is used by the sender.
+ """
+ for field in model._meta.get_fields():
+ if isinstance(field, ManyToManyField) and field.remote_field.through == sender:
+ return field
+
+ return None
+
+
+def post_processor(sender, instance, model, operation, targets):
+ """
+ if the change is in reverse or not, the processing of the changes is still
+ the same, so we have this method to take care of constructing the changes
+ :param sender:
+ :param instance:
+ :param model:
+ :param operation:
+ :param targets:
+ :return:
+ """
+ relationships = []
+
+ m2m_rel = find_m2m_rel(sender, model)
+ if not m2m_rel:
+ logger.warning(f'[DAL] save[m2m] could not find ManyToManyField for {instance}')
+ return
+
+ field = ModelField()
+ field.name = m2m_rel.name
+ field.mirror = ModelMirror(
+ name=model.__name__, application=Application(name=instance._meta.app_label)
+ )
+ field.type = m2m_rel.__class__.__name__
+
+ # there is the possibility that a pre_clear occurred, if that is the case
+ # extend the targets and pop the list of affected instances from the attached
+ # field
+ get_or_create_meta(instance)
+ if (
+ hasattr(instance._meta.dal, 'm2m_pre_clear')
+ and field.name in instance._meta.dal.m2m_pre_clear
+ and operation == Operation.DELETE
+ ):
+ cleared = instance._meta.dal.m2m_pre_clear[field.name]
+ targets.extend(cleared)
+ instance._meta.dal.m2m_pre_clear.pop(field.name)
+
+ for target in targets:
+ relationship = ModelRelationshipModification()
+ relationship.operation = operation
+ relationship.field = field
+ mirror = ModelMirror()
+ mirror.name = target.__class__.__name__
+ mirror.application = Application(name=target._meta.app_label)
+ relationship.entry = ModelEntry(
+ mirror=mirror, value=repr(target), primary_key=target.pk
+ )
+ relationships.append(relationship)
+
+ if len(relationships) == 0:
+ # there was no actual change, so we're not propagating the event
+ return
+
+ event, _ = get_or_create_model_event(instance, operation)
+
+ user = None
+ logger.log(
+ settings.model.loglevel,
+ f'{user or "Anonymous"} modified field '
+ f'{field.name} | Model: '
+ f'{field.mirror.application}.{field.mirror} '
+ f'| Modifications: {", ".join([r.short() for r in relationships])}',
+ extra={
+ 'action': 'model[m2m]',
+ 'data': {'instance': instance, 'sender': sender},
+ 'relationships': relationships,
+ 'event': event,
+ },
+ )
+
+
+def pre_clear_processor(sender, instance, pks, model, reverse, operation) -> None:
+ """
+ pre_clear needs a specific processor as we attach the changes that are about
+ to happen to the instance first, and then use them in post_delete/post_clear
+
+ if reverse = False then every element gets removed from the relationship field,
+ but if reverse = True then instance should be removed from every target.
+
+ Note: it seems that pre_clear is not getting fired for reverse.
+
+ :return: None
+ """
+ if reverse:
+ return
+
+ get_or_create_meta(instance)
+
+ rel = find_m2m_rel(sender, instance.__class__)
+ if 'm2m_pre_clear' not in instance._meta.dal:
+ instance._meta.dal.m2m_pre_clear = {}
+
+ cleared = getattr(instance, rel.name, [])
+ if isinstance(cleared, Manager):
+ cleared = list(cleared.all())
+ instance._meta.dal.m2m_pre_clear = {rel.name: cleared}
+
+
+@receiver(m2m_changed, weak=False)
+def m2m_changed_signal(
+ sender, instance, action, reverse, model, pk_set, using, **kwargs
+) -> None:
+ """
+ Django sends this signal when many-to-many relationships change.
+
+ One of the more complex signals, due to the fact that change can be reversed
+ we need to either process
+ instance field changes of pk_set (reverse=False) or
+ pk_set field changes of instance. (reverse=True)
+
+ The changes will always get applied in the model where the field in defined.
+
+ # TODO: post_remove also gets triggered when there is nothing actually getting removed
+ :return: None
+ """
+ if action not in ['post_add', 'post_remove', 'pre_clear', 'post_clear']:
+ return
+
+ if action == 'pre_clear':
+ operation = Operation.DELETE
+
+ return pre_clear_processor(
+ sender,
+ instance,
+ list(pk_set) if pk_set else None,
+ model,
+ reverse,
+ operation,
+ )
+ elif action == 'post_add':
+ operation = Operation.CREATE
+ elif action == 'post_clear':
+ operation = Operation.DELETE
+ else:
+ operation = Operation.DELETE
+
+ targets = model.objects.filter(pk__in=list(pk_set)) if pk_set else []
+ if reverse:
+ for target in [
+ t for t in targets if not lazy_model_exclusion(t, operation, t.__class__)
+ ]:
+ post_processor(sender, target, target.__class__, operation, [instance])
+ else:
+ if lazy_model_exclusion(instance, operation, instance.__class__):
+ return
+
+ post_processor(sender, instance, instance.__class__, operation, targets)
diff --git a/galaxy_ng/_vendor/automated_logging/signals/request.py b/galaxy_ng/_vendor/automated_logging/signals/request.py
new file mode 100644
index 0000000000..e82ccee360
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/signals/request.py
@@ -0,0 +1,140 @@
+"""
+File handles the processing and redirection of all request related
+signals
+"""
+
+import logging
+import urllib.parse
+
+from django.core.handlers.wsgi import WSGIRequest
+from django.dispatch import receiver
+from django.core.signals import got_request_exception, request_finished
+from django.http import Http404, JsonResponse
+from django.urls import resolve
+
+from automated_logging.middleware import AutomatedLoggingMiddleware
+from automated_logging.models import RequestEvent, Application, RequestContext
+from automated_logging.settings import settings
+from automated_logging.signals import request_exclusion
+
+# TODO: should django-ipware be optional?
+try:
+ from ipware import get_client_ip
+except ImportError:
+ get_client_ip = None
+
+
+logger = logging.getLogger("automated_logging")
+
+
+@receiver(request_finished, weak=False)
+def request_finished_signal(sender, **kwargs) -> None:
+ """
+ This signal gets the environment from the local thread and
+ sends a logging message, that message will be processed by the
+ handler later on.
+
+ This is a simple redirection.
+
+ :return: -
+ """
+ level = settings.request.loglevel
+ environ = AutomatedLoggingMiddleware.get_current_environ()
+
+ if not environ:
+ if settings.request.log_request_was_not_recorded:
+ logger.info(
+ "Environment for request couldn't be determined. "
+ "Request was not recorded."
+ )
+ return
+
+ request = RequestEvent()
+
+ request.user = AutomatedLoggingMiddleware.get_current_user(environ)
+ request.uri = environ.request.get_full_path()
+
+ if not settings.request.data.query:
+ request.uri = urllib.parse.urlparse(request.uri).path
+
+ if 'request' in settings.request.data.enabled:
+ request_context = RequestContext()
+ request_context.content = environ.request.body
+ request_context.type = environ.request.content_type
+
+ request.request = request_context
+
+ if 'response' in settings.request.data.enabled:
+ response_context = RequestContext()
+ response_context.content = environ.response.content
+ response_context.type = environ.response['Content-Type']
+
+ request.response = response_context
+
+ # TODO: context parsing, masking and removal
+ if get_client_ip and settings.request.ip:
+ request.ip, _ = get_client_ip(environ.request)
+
+ request.status = environ.response.status_code if environ.response else None
+ request.method = environ.request.method.upper()
+ request.context_type = environ.request.content_type
+
+ try:
+ function = resolve(environ.request.path).func
+ except Http404:
+ function = None
+
+ request.application = Application(name=None)
+ if function:
+ application = function.__module__.split('.')[0]
+ request.application = Application(name=application)
+
+ if request_exclusion(request, function):
+ return
+
+ logger_ip = f' from {request.ip}' if get_client_ip and settings.request.ip else ''
+ logger.log(
+ level,
+ f'[{request.method}] [{request.status}] '
+ f'{getattr(request, "user", None) or "Anonymous"} '
+ f'at {request.uri}{logger_ip}',
+ extra={'action': 'request', 'event': request},
+ )
+
+
+@receiver(got_request_exception, weak=False)
+def request_exception(sender, request, **kwargs):
+ """
+ Exception logging for requests, via the django signal.
+
+ The signal can also return a WSGIRequest exception, which does not
+ have all fields that are needed.
+
+ :return: -
+ """
+
+ status = int(request.status_code) if hasattr(request, 'status_code') else None
+ method = request.method if hasattr(request, 'method') else None
+ reason = request.reason_phrase if hasattr(request, 'reason_phrase') else None
+ level = logging.CRITICAL if status and status <= 500 else logging.WARNING
+
+ is_wsgi = isinstance(request, WSGIRequest)
+
+ logger.log(
+ level,
+ f'[{method or "UNK"}] [{status or "UNK"}] '
+ f'{is_wsgi and "[WSGIResponse] "}'
+ f'Exception: {reason or "UNKNOWN"}',
+ )
+
+
+@receiver(request_finished, weak=False)
+def thread_cleanup(sender, **kwargs):
+ """
+ This signal just calls the thread cleanup function to make sure,
+ that the custom thread object is always clean for the next request.
+ This needs to be always the last function registered by the receiver!
+
+ :return: -
+ """
+ AutomatedLoggingMiddleware.cleanup()
diff --git a/galaxy_ng/_vendor/automated_logging/signals/save.py b/galaxy_ng/_vendor/automated_logging/signals/save.py
new file mode 100644
index 0000000000..0f7fea9649
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/signals/save.py
@@ -0,0 +1,282 @@
+"""
+File handles every signal related to the saving/deletion of django models.
+"""
+
+import logging
+from collections import namedtuple
+from datetime import datetime
+from pprint import pprint
+from typing import Any
+
+from django.core.exceptions import ObjectDoesNotExist
+from django.db import transaction
+from django.db.models.signals import pre_save, post_save, post_delete
+from django.dispatch import receiver
+
+from automated_logging.models import (
+ ModelValueModification,
+ ModelField,
+ ModelMirror,
+ Application,
+)
+from automated_logging.settings import settings
+from automated_logging.signals import (
+ model_exclusion,
+ lazy_model_exclusion,
+ field_exclusion,
+)
+from automated_logging.helpers import (
+ get_or_create_meta,
+ Operation,
+ get_or_create_model_event,
+)
+from automated_logging.helpers.enums import PastOperationMap
+
+ChangeSet = namedtuple('ChangeSet', ('deleted', 'added', 'changed'))
+logger = logging.getLogger("automated_logging")
+
+
+def normalize_save_value(value: Any):
+ """ normalize the values given to the function to make stuff more readable """
+ if value is None or value == '':
+ return None
+ if isinstance(value, str):
+ return value
+
+ return repr(value)
+
+
+@receiver(pre_save, weak=False)
+@transaction.atomic
+def pre_save_signal(sender, instance, **kwargs) -> None:
+ """
+ Compares the current instance and old instance (fetched via the pk)
+ and generates a dictionary of changes
+
+ :param sender:
+ :param instance:
+ :param kwargs:
+ :return: None
+ """
+ get_or_create_meta(instance)
+ # clear the event to be sure
+ instance._meta.dal.event = None
+
+ operation = Operation.MODIFY
+ try:
+ pre = sender.objects.get(pk=instance.pk)
+ except ObjectDoesNotExist:
+ # __dict__ is used on pre, therefore we need to create a function
+ # that uses __dict__ too, but returns nothing.
+
+ pre = lambda _: None
+ operation = Operation.CREATE
+
+ excluded = lazy_model_exclusion(instance, operation, instance.__class__)
+ if excluded:
+ return
+
+ old, new = pre.__dict__, instance.__dict__
+
+ previously = set(
+ k for k in old.keys() if not k.startswith('_') and old[k] is not None
+ )
+ currently = set(
+ k for k in new.keys() if not k.startswith('_') and new[k] is not None
+ )
+
+ added = currently.difference(previously)
+ deleted = previously.difference(currently)
+ changed = {
+ k
+ for k in
+ # take all keys from old and new, and only use those that are in both
+ set(old.keys()) & set(new.keys())
+ # remove values that have been added or deleted (optimization)
+ .difference(added).difference(deleted)
+ # check if the value is equal, if not they are not changed
+ if old[k] != new[k]
+ }
+
+ summary = [
+ *(
+ {
+ 'operation': Operation.CREATE,
+ 'previous': None,
+ 'current': new[k],
+ 'key': k,
+ }
+ for k in added
+ ),
+ *(
+ {
+ 'operation': Operation.DELETE,
+ 'previous': old[k],
+ 'current': None,
+ 'key': k,
+ }
+ for k in deleted
+ ),
+ *(
+ {
+ 'operation': Operation.MODIFY,
+ 'previous': old[k],
+ 'current': new[k],
+ 'key': k,
+ }
+ for k in changed
+ ),
+ ]
+
+ # exclude fields not present in _meta.get_fields
+ fields = {f.name: f for f in instance._meta.get_fields()}
+ extra = {f.attname: f for f in instance._meta.get_fields() if hasattr(f, 'attname')}
+ fields = {**extra, **fields}
+
+ summary = [s for s in summary if s['key'] in fields.keys()]
+
+ # field exclusion
+ summary = [
+ s
+ for s in summary
+ if not field_exclusion(s['key'], instance, instance.__class__)
+ ]
+
+ model = ModelMirror()
+ model.name = sender.__name__
+ model.application = Application(name=instance._meta.app_label)
+
+ modifications = []
+ for entry in summary:
+ field = ModelField()
+ field.name = entry['key']
+ field.mirror = model
+
+ field.type = fields[entry['key']].__class__.__name__
+
+ modification = ModelValueModification()
+ modification.operation = entry['operation']
+ modification.field = field
+
+ modification.previous = normalize_save_value(entry['previous'])
+ modification.current = normalize_save_value(entry['current'])
+
+ modifications.append(modification)
+
+ instance._meta.dal.modifications = modifications
+
+ if settings.model.performance:
+ instance._meta.dal.performance = datetime.now()
+
+
+def post_processor(status, sender, instance, updated=None, suffix='') -> None:
+ """
+ Due to the fact that both post_delete and post_save have
+ the same logic for propagating changes, we have this helper class
+ to do so, just simply wraps and logs the data the handler needs.
+
+ The event gets created here instead of the handler to keep
+ everything consistent and have the handler as simple as possible.
+
+ :param status: Operation
+ :param sender: model class
+ :param instance: model instance
+ :param updated: updated fields
+ :param suffix: suffix to be added to the message
+ :return: None
+ """
+ past = {v: k for k, v in PastOperationMap.items()}
+
+ get_or_create_meta(instance)
+
+ event, _ = get_or_create_model_event(instance, status, force=True, extra=True)
+ modifications = getattr(instance._meta.dal, 'modifications', [])
+
+ # clear the modifications meta list
+ instance._meta.dal.modifications = []
+
+ if len(modifications) == 0 and status == Operation.MODIFY:
+ # if the event is modify, but nothing changed, don't actually propagate
+ return
+
+ logger.log(
+ settings.model.loglevel,
+ f'{event.user or "Anonymous"} {past[status]} '
+ f'{event.entry.mirror.application}.{sender.__name__} | '
+ f'Instance: {instance!r}{suffix}',
+ extra={
+ 'action': 'model',
+ 'data': {'status': status, 'instance': instance},
+ 'event': event,
+ 'modifications': modifications,
+ },
+ )
+
+
+@receiver(post_save, weak=False)
+@transaction.atomic
+def post_save_signal(
+ sender, instance, created, update_fields: frozenset, **kwargs
+) -> None:
+ """
+ Signal is getting called after a save has been concluded. When this
+ is the case we can be sure the save was successful and then only
+ propagate the changes to the handler.
+
+ :param sender: model class
+ :param instance: model instance
+ :param created: bool, was the model created?
+ :param update_fields: which fields got explicitly updated?
+ :param kwargs: django needs kwargs to be there
+ :return: -
+ """
+ status = Operation.CREATE if created else Operation.MODIFY
+ if lazy_model_exclusion(
+ instance,
+ status,
+ instance.__class__,
+ ):
+ return
+ get_or_create_meta(instance)
+
+ suffix = f''
+ if (
+ status == Operation.MODIFY
+ and hasattr(instance._meta.dal, 'modifications')
+ and settings.model.detailed_message
+ ):
+ suffix = (
+ f' | Modifications: '
+ f'{", ".join([m.short() for m in instance._meta.dal.modifications])}'
+ )
+
+ if update_fields is not None and hasattr(instance._meta.dal, 'modifications'):
+ instance._meta.dal.modifications = [
+ m for m in instance._meta.dal.modifications if m.field.name in update_fields
+ ]
+
+ post_processor(status, sender, instance, update_fields, suffix)
+
+
+@receiver(post_delete, weak=False)
+@transaction.atomic
+def post_delete_signal(sender, instance, **kwargs) -> None:
+ """
+ Signal is getting called after instance deletion. We just redirect the
+ event to the post_processor.
+
+ TODO: consider doing a "delete snapshot"
+
+ :param sender: model class
+ :param instance: model instance
+ :param kwargs: required bt django
+ :return: -
+ """
+
+ get_or_create_meta(instance)
+ instance._meta.dal.event = None
+
+ if lazy_model_exclusion(instance, Operation.DELETE, instance.__class__):
+ return
+
+ post_processor(Operation.DELETE, sender, instance)
diff --git a/galaxy_ng/_vendor/automated_logging/templates/dal/admin/view.html b/galaxy_ng/_vendor/automated_logging/templates/dal/admin/view.html
new file mode 100644
index 0000000000..2635c73aae
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/templates/dal/admin/view.html
@@ -0,0 +1,8 @@
+{% extends "admin/change_form.html" %}
+{% load i18n %}
+
+{% block submit_buttons_bottom %}
+
+{% endblock %}
diff --git a/galaxy_ng/_vendor/automated_logging/tests/__init__.py b/galaxy_ng/_vendor/automated_logging/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/galaxy_ng/_vendor/automated_logging/tests/base.py b/galaxy_ng/_vendor/automated_logging/tests/base.py
new file mode 100644
index 0000000000..e8181a900c
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/tests/base.py
@@ -0,0 +1,178 @@
+""" Test base every unit test uses """
+import importlib
+import logging.config
+from copy import copy, deepcopy
+
+from django.conf import settings
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import AbstractUser
+from django.test import TestCase, RequestFactory
+from django.urls import path
+
+from automated_logging.helpers import namedtuple2dict
+from automated_logging.middleware import AutomatedLoggingMiddleware
+from automated_logging.models import ModelEvent, RequestEvent, UnspecifiedEvent
+from automated_logging.signals import cached_model_exclusion
+
+User: AbstractUser = get_user_model()
+USER_CREDENTIALS = {'username': 'example', 'password': 'example'}
+
+
+def clear_cache():
+ """ utility method to clear the cache """
+ if hasattr(AutomatedLoggingMiddleware.thread, 'dal'):
+ delattr(AutomatedLoggingMiddleware.thread, 'dal')
+
+ import automated_logging.decorators
+
+ # noinspection PyProtectedMember
+ automated_logging.decorators._exclude_models.clear()
+ # noinspection PyProtectedMember
+ automated_logging.decorators._include_models.clear()
+
+ cached_model_exclusion.cache_clear()
+
+
+class BaseTestCase(TestCase):
+ def __init__(self, method_name):
+ from django.conf import settings
+
+ settings.AUTOMATED_LOGGING_DEV = True
+
+ super().__init__(method_name)
+
+ def request(self, method, view, data=None):
+ """
+ request a specific view and return the response.
+
+ This is not ideal and super hacky. Backups the actual urlpatterns,
+ and then overrides the urlpatterns with a temporary one and then
+ inserts the new one again.
+ """
+
+ urlconf = importlib.import_module(settings.ROOT_URLCONF)
+
+ backup = copy(urlconf.urlpatterns)
+ urlconf.urlpatterns.clear()
+ urlconf.urlpatterns.append(path('', view))
+
+ response = self.client.generic(method, '/', data=data)
+
+ urlconf.urlpatterns.clear()
+ urlconf.urlpatterns.extend(backup)
+
+ return response
+
+ def setUp(self):
+ """ setUp the DAL specific test environment """
+ from django.conf import settings
+ from automated_logging.settings import default, settings as conf
+
+ self.user = User.objects.create_user(**USER_CREDENTIALS)
+ self.user.save()
+
+ self.original_config = deepcopy(settings.AUTOMATED_LOGGING)
+
+ base = namedtuple2dict(default)
+
+ settings.AUTOMATED_LOGGING.clear()
+ for key, value in base.items():
+ settings.AUTOMATED_LOGGING[key] = deepcopy(value)
+
+ conf.load.cache_clear()
+
+ self.setUpLogging()
+ super().setUp()
+
+ def tearDown(self) -> None:
+ """ tearDown the DAL specific environment """
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ super().tearDown()
+
+ self.tearDownLogging()
+
+ settings.AUTOMATED_LOGGING.clear()
+ for key, value in self.original_config.items():
+ settings.AUTOMATED_LOGGING[key] = deepcopy(value)
+
+ conf.load.cache_clear()
+
+ clear_cache()
+
+ @staticmethod
+ def clear():
+ """ clear all events """
+ ModelEvent.objects.all().delete()
+ RequestEvent.objects.all().delete()
+ UnspecifiedEvent.objects.all().delete()
+
+ def tearDownLogging(self):
+ """
+ replace our own logging config
+ with the original to not break any other tests
+ that might depend on it.
+ """
+ from django.conf import settings
+
+ settings.LOGGING = self.logging_backup
+ logging.config.dictConfig(settings.LOGGING)
+
+ def setUpLogging(self):
+ """ sets up logging dict, so that we can actually use our own """
+ from django.conf import settings
+
+ self.logging_backup = deepcopy(settings.LOGGING)
+ settings.LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'root': {'level': 'INFO', 'handlers': ['console', 'db'],},
+ 'formatters': {
+ 'verbose': {
+ 'format': '%(levelname)s %(asctime)s %(module)s '
+ '%(process)d %(thread)d %(message)s'
+ },
+ 'simple': {'format': '%(levelname)s %(message)s'},
+ 'syslog': {
+ 'format': '%(asctime)s %%LOCAL0-%(levelname) %(message)s'
+ # 'format': '%(levelname)s %(message)s'
+ },
+ },
+ 'handlers': {
+ 'console': {
+ 'level': 'INFO',
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'verbose',
+ },
+ 'db': {
+ 'level': 'INFO',
+ 'class': 'automated_logging.handlers.DatabaseHandler',
+ },
+ },
+ 'loggers': {
+ 'automated_logging': {
+ 'level': 'INFO',
+ 'handlers': ['console', 'db'],
+ 'propagate': False,
+ },
+ 'django': {
+ 'level': 'INFO',
+ 'handlers': ['console', 'db'],
+ 'propagate': False,
+ },
+ },
+ }
+
+ logging.config.dictConfig(settings.LOGGING)
+
+ def bypass_request_restrictions(self):
+ """ bypass all request default restrictions of DAL """
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ settings.AUTOMATED_LOGGING['request']['exclude']['status'] = []
+ settings.AUTOMATED_LOGGING['request']['exclude']['methods'] = []
+ conf.load.cache_clear()
+
+ self.clear()
diff --git a/galaxy_ng/_vendor/automated_logging/tests/helpers.py b/galaxy_ng/_vendor/automated_logging/tests/helpers.py
new file mode 100644
index 0000000000..cb2244abae
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/tests/helpers.py
@@ -0,0 +1,9 @@
+""" test specific helpers """
+
+import string
+from random import choice
+
+
+def random_string(length=10):
+ """ generate a random string with the length specified """
+ return ''.join(choice(string.ascii_letters) for _ in range(length))
diff --git a/galaxy_ng/_vendor/automated_logging/tests/models.py b/galaxy_ng/_vendor/automated_logging/tests/models.py
new file mode 100644
index 0000000000..8405fff54c
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/tests/models.py
@@ -0,0 +1,132 @@
+import uuid
+from django.db.models import (
+ Model,
+ UUIDField,
+ DateTimeField,
+ ManyToManyField,
+ CASCADE,
+ ForeignKey,
+ OneToOneField,
+ CharField,
+)
+
+from automated_logging.decorators import exclude_model, include_model
+
+
+class TestBase(Model):
+ """ Base for all the test models """
+
+ id = UUIDField(default=uuid.uuid4, primary_key=True)
+
+ created_at = DateTimeField(auto_now_add=True)
+ updated_at = DateTimeField(auto_now=True)
+
+ class Meta:
+ abstract = True
+ app_label = 'automated_logging'
+
+
+class OrdinaryBaseTest(TestBase):
+ """ Ordinary base test. Has a random char field."""
+
+ random = CharField(max_length=255, null=True)
+ random2 = CharField(max_length=255, null=True)
+
+ class Meta:
+ abstract = True
+ app_label = 'automated_logging'
+
+
+class OrdinaryTest(OrdinaryBaseTest):
+ """ Ordinary test. Has a random char field."""
+
+ class Meta:
+ app_label = 'automated_logging'
+
+
+class M2MTest(TestBase):
+ """ Used to test the Many-To-Many Relationship functionality of DAL"""
+
+ relationship = ManyToManyField(OrdinaryTest)
+
+ class Meta:
+ app_label = 'automated_logging'
+
+
+class ForeignKeyTest(TestBase):
+ """ Used to test ForeignKey functionality of DAL."""
+
+ relationship = ForeignKey(OrdinaryTest, on_delete=CASCADE, null=True)
+
+ class Meta:
+ app_label = 'automated_logging'
+
+
+class OneToOneTest(TestBase):
+ """ Used to test the One-To-One Relationship functionality of DAL."""
+
+ relationship = OneToOneField(OrdinaryTest, on_delete=CASCADE, null=True)
+
+ class Meta:
+ app_label = 'automated_logging'
+
+
+class SpeedTest(TestBase):
+ """ Used to test the speed of DAL """
+
+ for idx in range(100):
+ exec(f"column{idx} = CharField(max_length=15, null=True)")
+
+ class Meta:
+ app_label = 'automated_logging'
+
+
+class FullClassBasedExclusionTest(OrdinaryBaseTest):
+ """ Used to test the full model exclusion via meta class"""
+
+ class Meta:
+ app_label = 'automated_logging'
+
+ class LoggingIgnore:
+ complete = True
+
+
+class PartialClassBasedExclusionTest(OrdinaryBaseTest):
+ """ Used to test partial ignore via fields """
+
+ class Meta:
+ app_label = 'automated_logging'
+
+ class LoggingIgnore:
+ fields = ['random']
+ operations = ['delete']
+
+
+@exclude_model
+class FullDecoratorBasedExclusionTest(OrdinaryBaseTest):
+ """ Used to test full decorator exclusion """
+
+ class Meta:
+ app_label = 'automated_logging'
+
+
+@exclude_model(operations=['delete'], fields=['random'])
+class PartialDecoratorBasedExclusionTest(OrdinaryBaseTest):
+ """ Used to test partial decorator exclusion """
+
+ class Meta:
+ app_label = 'automated_logging'
+
+
+@include_model
+class DecoratorOverrideExclusionTest(OrdinaryBaseTest):
+ """
+ Used to check if include_model
+ has precedence over class based configuration
+ """
+
+ class Meta:
+ app_label = 'automated_logging'
+
+ class LoggingIgnore:
+ complete = True
diff --git a/galaxy_ng/_vendor/automated_logging/tests/test_exclusion.py b/galaxy_ng/_vendor/automated_logging/tests/test_exclusion.py
new file mode 100644
index 0000000000..8349e81690
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/tests/test_exclusion.py
@@ -0,0 +1,622 @@
+import logging
+import threading
+from pathlib import Path
+
+from django.contrib import admin
+from django.http import JsonResponse
+
+from automated_logging.decorators import (
+ include_model,
+ exclude_view,
+ include_view,
+ exclude_model,
+)
+from automated_logging.helpers import Operation
+from automated_logging.middleware import AutomatedLoggingMiddleware
+from automated_logging.models import (
+ ModelEvent,
+ RequestEvent,
+ UnspecifiedEvent,
+ Application,
+)
+from automated_logging.signals import (
+ cached_model_exclusion,
+ model_exclusion,
+ request_exclusion,
+)
+from automated_logging.tests.models import (
+ OrdinaryTest,
+ OneToOneTest,
+ FullClassBasedExclusionTest,
+ PartialClassBasedExclusionTest,
+ FullDecoratorBasedExclusionTest,
+ PartialDecoratorBasedExclusionTest,
+ DecoratorOverrideExclusionTest,
+ M2MTest,
+)
+from automated_logging.tests.base import BaseTestCase, USER_CREDENTIALS, clear_cache
+from automated_logging.tests.helpers import random_string
+
+
+class ConfigurationBasedExclusionsTestCase(BaseTestCase):
+ def setUp(self):
+ super().setUp()
+
+ self.client.login(**USER_CREDENTIALS)
+
+ @staticmethod
+ def view(request):
+ return JsonResponse({})
+
+ def test_globals(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ self.clear()
+
+ settings.AUTOMATED_LOGGING['unspecified']['exclude']['applications'] = []
+ settings.AUTOMATED_LOGGING['model']['exclude']['applications'] = []
+ settings.AUTOMATED_LOGGING['request']['exclude']['applications'] = []
+ settings.AUTOMATED_LOGGING['globals']['exclude']['applications'] = [
+ 'automated*'
+ ]
+
+ conf.load.cache_clear()
+
+ OrdinaryTest(random=random_string()).save()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ self.request('GET', self.view)
+ self.assertEqual(RequestEvent.objects.count(), 0)
+
+ logger = logging.getLogger(__name__)
+ logger.info('[TEST]')
+ self.assertEqual(UnspecifiedEvent.objects.count(), 0)
+
+ def test_applications(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ self.clear()
+
+ settings.AUTOMATED_LOGGING['globals']['exclude']['applications'] = []
+ conf.load.cache_clear()
+
+ logger = logging.getLogger(__name__)
+ logger.info('[TEST]')
+ self.assertEqual(UnspecifiedEvent.objects.count(), 1)
+ self.clear()
+
+ settings.AUTOMATED_LOGGING['unspecified']['exclude']['applications'] = [
+ 'automated*'
+ ]
+ conf.load.cache_clear()
+
+ logger = logging.getLogger(__name__)
+ logger.info('[TEST]')
+ self.assertEqual(UnspecifiedEvent.objects.count(), 0)
+
+ settings.AUTOMATED_LOGGING['model']['exclude']['applications'] = ['automated*']
+ conf.load.cache_clear()
+
+ OrdinaryTest(random=random_string()).save()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ settings.AUTOMATED_LOGGING['request']['exclude']['applications'] = [
+ 'automated*'
+ ]
+ conf.load.cache_clear()
+
+ self.request('GET', self.view)
+ self.assertEqual(RequestEvent.objects.count(), 0)
+
+ def test_fields(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ subject = OrdinaryTest()
+ subject.save()
+
+ self.clear()
+ settings.AUTOMATED_LOGGING['model']['exclude']['fields'] = [
+ 'automated_logging.OrdinaryTest.random'
+ ]
+ conf.load.cache_clear()
+
+ subject.random = random_string()
+ subject.save()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ settings.AUTOMATED_LOGGING['model']['exclude']['fields'] = [
+ 'OrdinaryTest.random'
+ ]
+ conf.load.cache_clear()
+
+ subject.random = random_string()
+ subject.save()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ settings.AUTOMATED_LOGGING['model']['exclude']['fields'] = ['random']
+ conf.load.cache_clear()
+ subject.random = random_string()
+ subject.save()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ subject.random = random_string()
+ subject.random2 = random_string()
+ subject.save()
+ self.assertEqual(ModelEvent.objects.count(), 1)
+ self.assertEqual(ModelEvent.objects.all()[0].modifications.count(), 1)
+
+ def test_models(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ self.clear()
+
+ settings.AUTOMATED_LOGGING['model']['exclude']['models'] = [
+ 'automated_logging.tests.models.OrdinaryTest'
+ ]
+ conf.load.cache_clear()
+
+ OrdinaryTest(random=random_string()).save()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ settings.AUTOMATED_LOGGING['model']['exclude']['models'] = [
+ 'automated_logging.OrdinaryTest'
+ ]
+ conf.load.cache_clear()
+
+ OrdinaryTest(random=random_string()).save()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ settings.AUTOMATED_LOGGING['model']['exclude']['models'] = ['OrdinaryTest']
+ conf.load.cache_clear()
+
+ OrdinaryTest(random=random_string()).save()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ OneToOneTest().save()
+ self.assertEqual(ModelEvent.objects.count(), 1)
+ self.assertEqual(ModelEvent.objects.all()[0].modifications.count(), 1)
+
+ @staticmethod
+ def redirect_view(request):
+ return JsonResponse({}, status=301)
+
+ def test_status(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ settings.AUTOMATED_LOGGING['request']['exclude']['methods'] = []
+ settings.AUTOMATED_LOGGING['request']['exclude']['status'] = [200]
+ conf.load.cache_clear()
+
+ self.clear()
+
+ self.request('GET', self.view)
+ self.assertEqual(RequestEvent.objects.count(), 0)
+
+ self.request('GET', self.redirect_view)
+ self.assertEqual(RequestEvent.objects.count(), 1)
+
+ def test_method(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ settings.AUTOMATED_LOGGING['request']['exclude']['methods'] = ['GET']
+ settings.AUTOMATED_LOGGING['request']['exclude']['status'] = []
+ conf.load.cache_clear()
+
+ self.clear()
+
+ self.request('GET', self.view)
+ self.assertEqual(RequestEvent.objects.count(), 0)
+
+ self.request('POST', self.view)
+ self.assertEqual(RequestEvent.objects.count(), 1)
+
+ def test_files(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ path = Path(__file__).absolute()
+ project = Path(__file__).parent.parent.parent
+ relative = path.relative_to(project)
+
+ logger = logging.getLogger(__name__)
+ self.clear()
+
+ # absolute path
+ settings.AUTOMATED_LOGGING['unspecified']['exclude']['files'] = [
+ path.as_posix()
+ ]
+ conf.load.cache_clear()
+
+ logger.info(random_string())
+ self.assertEqual(UnspecifiedEvent.objects.count(), 0)
+
+ # relative path
+ settings.AUTOMATED_LOGGING['unspecified']['exclude']['files'] = [
+ relative.as_posix()
+ ]
+ conf.load.cache_clear()
+
+ logger.info(random_string())
+ self.assertEqual(UnspecifiedEvent.objects.count(), 0)
+
+ # file name
+ settings.AUTOMATED_LOGGING['unspecified']['exclude']['files'] = [relative.name]
+ conf.load.cache_clear()
+
+ logger.info(random_string())
+ self.assertEqual(UnspecifiedEvent.objects.count(), 0)
+
+ # single directory name
+ settings.AUTOMATED_LOGGING['unspecified']['exclude']['files'] = [
+ 'automated_logging'
+ ]
+ conf.load.cache_clear()
+
+ logger.info(random_string())
+ self.assertEqual(UnspecifiedEvent.objects.count(), 0)
+
+ # absolute directory
+ settings.AUTOMATED_LOGGING['unspecified']['exclude']['files'] = [
+ path.parent.as_posix()
+ ]
+ conf.load.cache_clear()
+
+ logger.info(random_string())
+ self.assertEqual(UnspecifiedEvent.objects.count(), 0)
+
+ # file not excluded
+ settings.AUTOMATED_LOGGING['unspecified']['exclude']['files'] = ['dal']
+ conf.load.cache_clear()
+
+ logger.info(random_string())
+ self.assertEqual(UnspecifiedEvent.objects.count(), 1)
+
+ def test_unknown(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ logger = logging.getLogger(__name__)
+
+ default_factory = logging.getLogRecordFactory()
+
+ def factory(*args, **kwargs):
+ """
+ force setting the pathname and module
+ wrong so that we can pretend to exclude unknowns
+ """
+
+ record = default_factory(*args, **kwargs)
+
+ record.pathname = '/example.py'
+ record.module = 'default'
+ return record
+
+ self.clear()
+ logging.setLogRecordFactory(factory=factory)
+
+ settings.AUTOMATED_LOGGING['unspecified']['exclude']['unknown'] = True
+ conf.load.cache_clear()
+
+ logger.info(random_string())
+ self.assertEqual(UnspecifiedEvent.objects.count(), 0)
+
+ settings.AUTOMATED_LOGGING['unspecified']['exclude']['unknown'] = False
+ conf.load.cache_clear()
+
+ logger.info(random_string())
+ self.assertEqual(UnspecifiedEvent.objects.count(), 1)
+
+ logging.setLogRecordFactory(default_factory)
+
+ def test_search_types(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ settings.AUTOMATED_LOGGING['globals']['exclude']['applications'] = []
+ settings.AUTOMATED_LOGGING['model']['exclude']['applications'] = [
+ 'pl:automated_logging'
+ ]
+ conf.load.cache_clear()
+ cached_model_exclusion.cache_clear()
+
+ self.clear()
+ OrdinaryTest().save()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ settings.AUTOMATED_LOGGING['model']['exclude']['applications'] = [
+ 'gl:automated_*'
+ ]
+ conf.load.cache_clear()
+ cached_model_exclusion.cache_clear()
+
+ OrdinaryTest().save()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ settings.AUTOMATED_LOGGING['request']['exclude']['applications'] = [
+ 're:automated.*'
+ ]
+ self.bypass_request_restrictions()
+ conf.load.cache_clear()
+ cached_model_exclusion.cache_clear()
+
+ self.request('GET', self.view)
+ self.assertEqual(RequestEvent.objects.count(), 0)
+
+ def test_module(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ settings.AUTOMATED_LOGGING['globals']['exclude']['applications'] = []
+ settings.AUTOMATED_LOGGING['model']['exclude']['models'] = [
+ 'automated_logging.tests.models'
+ ]
+ conf.load.cache_clear()
+ cached_model_exclusion.cache_clear()
+ self.clear()
+
+ OrdinaryTest().save()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ def test_mock_sender(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ class MockModel:
+ __module__ = '[TEST]'
+ __name__ = 'MockModel'
+
+ class MockMeta:
+ app_label = None
+
+ settings.AUTOMATED_LOGGING['model']['exclude']['unknown'] = True
+ conf.load.cache_clear()
+
+ self.assertTrue(model_exclusion(MockModel, MockMeta, Operation.CREATE))
+
+ settings.AUTOMATED_LOGGING['request']['exclude']['unknown'] = True
+ conf.load.cache_clear()
+
+ self.assertTrue(
+ request_exclusion(RequestEvent(application=Application(name=None)))
+ )
+
+ def test_m2m_exclusion(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ settings.AUTOMATED_LOGGING['model']['exclude']['applications'] = [
+ 'gl:automated*'
+ ]
+ conf.load.cache_clear()
+ cached_model_exclusion.cache_clear()
+
+ instance = M2MTest()
+ instance.save()
+
+ children = [OrdinaryTest()]
+ [c.save() for c in children]
+
+ self.clear()
+
+ instance.relationship.add(*children)
+ instance.save()
+
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+
+class ClassBasedExclusionsTestCase(BaseTestCase):
+ def test_complete(self):
+ self.clear()
+
+ FullClassBasedExclusionTest().save()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ def test_partial(self):
+ self.clear()
+
+ subject = PartialClassBasedExclusionTest(random=random_string())
+ subject.save()
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+ self.assertEqual(event.modifications.count(), 1)
+ self.assertEqual(event.modifications.all()[0].field.name, 'id')
+
+ self.clear()
+
+ subject.delete()
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+
+class DecoratorBasedExclusionsTestCase(BaseTestCase):
+ def test_exclude_model(self):
+ self.clear()
+
+ # manual here as the cache clear, clears the first appliance
+ FullDecoratorBasedExclusionTest.__dal_register__()
+ subject = FullDecoratorBasedExclusionTest()
+ subject.save()
+
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ subject = PartialDecoratorBasedExclusionTest.__dal_register__()(
+ random=random_string()
+ )
+ subject.save()
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+ self.assertEqual(event.modifications.count(), 1)
+
+ self.clear()
+ subject.delete()
+
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ def test_include_model(self):
+ self.clear()
+
+ subject = DecoratorOverrideExclusionTest.__dal_register__()(
+ random=random_string()
+ )
+ subject.save()
+
+ self.assertEqual(ModelEvent.objects.count(), 1)
+
+ self.clear()
+
+ # test if overriding works
+ include_model(FullClassBasedExclusionTest, operations=['delete'])()
+
+ subject = FullClassBasedExclusionTest(random=random_string())
+ subject.save()
+
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ subject.delete()
+
+ self.assertEqual(ModelEvent.objects.count(), 1)
+
+ # just to make sure we clean up
+ clear_cache()
+
+ @staticmethod
+ @exclude_view
+ def complete_exclusion(request):
+ return JsonResponse({})
+
+ @staticmethod
+ @exclude_view(methods=['GET'])
+ def partial_exclusion(request):
+ return JsonResponse({})
+
+ def test_exclude_view(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ settings.AUTOMATED_LOGGING['request']['exclude']['methods'] = []
+ settings.AUTOMATED_LOGGING['request']['exclude']['status'] = []
+ conf.load.cache_clear()
+ self.clear()
+
+ self.request('GET', self.complete_exclusion)
+ self.assertEqual(RequestEvent.objects.count(), 0)
+
+ self.request('GET', self.partial_exclusion)
+ self.assertEqual(RequestEvent.objects.count(), 0)
+ self.request('POST', self.partial_exclusion)
+ self.assertEqual(RequestEvent.objects.count(), 1)
+
+ @staticmethod
+ @include_view
+ def complete_inclusion(request):
+ return JsonResponse({})
+
+ @staticmethod
+ @include_view(methods=['POST'])
+ def partial_inclusion(request):
+ return JsonResponse({})
+
+ def test_include_view(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ self.clear()
+ # settings default to ignoring 200/GET, include_model should still record
+
+ self.request('GET', self.complete_inclusion)
+ self.assertEqual(RequestEvent.objects.count(), 1)
+ self.clear()
+
+ settings.AUTOMATED_LOGGING['request']['exclude']['status'] = []
+ settings.AUTOMATED_LOGGING['request']['exclude']['methods'] = ['GET', 'POST']
+ conf.load.cache_clear()
+
+ self.request('GET', self.partial_inclusion)
+ self.assertEqual(RequestEvent.objects.count(), 0)
+ self.request('POST', self.partial_inclusion)
+ self.assertEqual(RequestEvent.objects.count(), 1)
+ self.clear()
+
+ settings.AUTOMATED_LOGGING['request']['exclude']['methods'] = []
+ conf.load.cache_clear()
+
+ # test if include_view has higher priority than exclude_view
+ view = include_view(self.complete_exclusion, methods=['GET'])
+ self.request('GET', view)
+ self.assertEqual(RequestEvent.objects.count(), 1)
+ self.request('POST', view)
+ self.assertEqual(RequestEvent.objects.count(), 1)
+
+ clear_cache()
+
+ def test_partial_decorator(self):
+ self.clear()
+ Model = include_model(operations=['create'])(OrdinaryTest)
+ Model().save()
+ self.assertEqual(ModelEvent.objects.count(), 1)
+
+ def test_layering(self):
+ Model = include_model(operations=['modify'], fields=['random2'])(
+ include_model(operations=['create'], fields=['random2'])(OrdinaryTest)
+ )
+
+ self.clear()
+ subject = Model()
+ subject.save()
+
+ subject.random = random_string()
+ subject.save()
+
+ self.assertEqual(ModelEvent.objects.count(), 2)
+
+ clear_cache()
+
+ Model = exclude_model(operations=['modify'], fields=['random2'])(
+ exclude_model(operations=['create'], fields=['random2'])(OrdinaryTest)
+ )
+
+ self.clear()
+
+ subject = Model()
+ subject.save()
+
+ subject.random = random_string()
+ subject.save()
+
+ self.assertEqual(ModelEvent.objects.count(), 0)
+
+ def test_model_fields(self):
+ Model = include_model(operations=[], fields=['random'])(
+ exclude_model(operations=None, fields=['random2'])(OrdinaryTest)
+ )
+
+ subject = Model()
+ subject.save()
+
+ self.clear()
+
+ subject.random = random_string()
+ subject.save()
+
+ self.assertEqual(ModelEvent.objects.count(), 1)
+
+ subject.random2 = random_string()
+ subject.save()
+
+ self.assertEqual(ModelEvent.objects.count(), 1)
+
+ def test_model_admin(self):
+ FullDecoratorBasedExclusionTest.__dal_register__()
+
+ admin.site.register(FullDecoratorBasedExclusionTest)
diff --git a/galaxy_ng/_vendor/automated_logging/tests/test_handler.py b/galaxy_ng/_vendor/automated_logging/tests/test_handler.py
new file mode 100644
index 0000000000..b934a5a6c1
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/tests/test_handler.py
@@ -0,0 +1,107 @@
+# test max_age (rework?)
+# test save
+# test module removal
+import logging
+import logging.config
+from datetime import timedelta
+import time
+
+from django.http import JsonResponse
+from marshmallow import ValidationError
+
+from automated_logging.helpers.exceptions import CouldNotConvertError
+from automated_logging.models import ModelEvent, RequestEvent, UnspecifiedEvent
+from automated_logging.tests.models import OrdinaryTest
+from automated_logging.tests.base import BaseTestCase
+
+
+class TestDatabaseHandlerTestCase(BaseTestCase):
+ @staticmethod
+ def view(request):
+ return JsonResponse({})
+
+ def test_max_age(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ duration = timedelta(seconds=1)
+ logger = logging.getLogger(__name__)
+
+ settings.AUTOMATED_LOGGING['model']['max_age'] = duration
+ settings.AUTOMATED_LOGGING['request']['max_age'] = duration
+ settings.AUTOMATED_LOGGING['unspecified']['max_age'] = duration
+
+ conf.load.cache_clear()
+
+ self.clear()
+ self.bypass_request_restrictions()
+
+ OrdinaryTest().save()
+ self.request('GET', self.view)
+ logger.info('I have the high ground Anakin!')
+
+ self.assertEqual(ModelEvent.objects.count(), 1)
+ self.assertEqual(RequestEvent.objects.count(), 1)
+ self.assertEqual(UnspecifiedEvent.objects.count(), 1)
+
+ time.sleep(2)
+
+ logger.info('A surprise, to be sure, but a welcome one.')
+
+ self.assertEqual(ModelEvent.objects.count(), 0)
+ self.assertEqual(RequestEvent.objects.count(), 0)
+ self.assertEqual(UnspecifiedEvent.objects.count(), 1)
+
+ def test_max_age_input_methods(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ logger = logging.getLogger(__name__)
+
+ settings.AUTOMATED_LOGGING['unspecified']['max_age'] = timedelta(seconds=1)
+ conf.load.cache_clear()
+ self.clear()
+
+ logger.info('I will do what I must.')
+ time.sleep(1)
+ logger.info('Hello There.')
+ self.assertEqual(UnspecifiedEvent.objects.count(), 1)
+
+ settings.AUTOMATED_LOGGING['unspecified']['max_age'] = 1
+ conf.load.cache_clear()
+ self.clear()
+
+ logger.info('A yes, the negotiator.')
+ time.sleep(1)
+ logger.info('Your tactics confuse and frighten me, sir.')
+ self.assertEqual(UnspecifiedEvent.objects.count(), 1)
+
+ settings.AUTOMATED_LOGGING['unspecified']['max_age'] = 'PT1S'
+ conf.load.cache_clear()
+ self.clear()
+
+ logger.info('Don\'t make me kill you.')
+ time.sleep(1)
+ logger.info('An old friend from the dead.')
+ self.assertEqual(UnspecifiedEvent.objects.count(), 1)
+
+ def test_batching(self):
+ from django.conf import settings
+
+ logger = logging.getLogger(__name__)
+
+ config = settings.LOGGING
+
+ config['handlers']['db']['batch'] = 10
+ logging.config.dictConfig(config)
+
+ self.clear()
+ for _ in range(9):
+ logger.info('It\'s a trick. Send no reply')
+
+ self.assertEqual(UnspecifiedEvent.objects.count(), 0)
+ logger.info('I can\'t see a thing. My cockpit\'s fogging')
+ self.assertEqual(UnspecifiedEvent.objects.count(), 10)
+
+ config['handlers']['db']['batch'] = 1
+ logging.config.dictConfig(config)
diff --git a/galaxy_ng/_vendor/automated_logging/tests/test_m2m.py b/galaxy_ng/_vendor/automated_logging/tests/test_m2m.py
new file mode 100644
index 0000000000..0a70ee5a53
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/tests/test_m2m.py
@@ -0,0 +1,231 @@
+""" Test all Many-To-Many related things """
+
+
+import random
+
+from automated_logging.helpers import Operation
+from automated_logging.models import ModelEvent
+from automated_logging.tests.models import (
+ M2MTest,
+ OrdinaryTest,
+ OneToOneTest,
+ ForeignKeyTest,
+)
+from automated_logging.signals.m2m import find_m2m_rel
+from automated_logging.tests.base import BaseTestCase
+from automated_logging.tests.helpers import random_string
+
+
+class LoggedOutM2MRelationshipsTestCase(BaseTestCase):
+ def setUp(self):
+ super().setUp()
+
+ # delete all previous model events
+ ModelEvent.objects.all().delete()
+
+ @staticmethod
+ def generate_children(samples=10):
+ """ generate X children that are going to be used in various tests """
+ children = [OrdinaryTest(random=random_string()) for _ in range(samples)]
+ [c.save() for c in children]
+
+ return children
+
+ def test_add(self):
+ """ check if adding X members works correctly """
+
+ samples = 10
+ children = self.generate_children(samples)
+
+ m2m = M2MTest()
+ m2m.save()
+
+ ModelEvent.objects.all().delete()
+ m2m.relationship.add(*children)
+ m2m.save()
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+ self.assertEqual(event.modifications.count(), 0)
+ self.assertEqual(event.relationships.count(), samples)
+
+ children = {str(c.id): c for c in children}
+
+ for relationship in event.relationships.all():
+ self.assertEqual(relationship.operation, int(Operation.CREATE))
+ self.assertEqual(relationship.field.name, 'relationship')
+ self.assertIn(relationship.entry.primary_key, children)
+
+ def test_delete(self):
+ """ check if deleting X elements works correctly """
+
+ samples = 10
+ removed = 5
+ children = self.generate_children(samples)
+
+ m2m = M2MTest()
+ m2m.save()
+ m2m.relationship.add(*children)
+ m2m.save()
+ ModelEvent.objects.all().delete()
+
+ selected = random.sample(children, k=removed)
+ m2m.relationship.remove(*selected)
+ m2m.save()
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+ self.assertEqual(event.modifications.count(), 0)
+ self.assertEqual(event.relationships.count(), removed)
+
+ children = {str(c.id): c for c in children}
+ for relationship in event.relationships.all():
+ self.assertEqual(relationship.operation, int(Operation.DELETE))
+ self.assertEqual(relationship.field.name, 'relationship')
+ self.assertIn(relationship.entry.primary_key, children)
+
+ def test_clear(self):
+ """ test if clearing all elements works correctly """
+
+ samples = 10
+ children = self.generate_children(samples)
+
+ m2m = M2MTest()
+ m2m.save()
+ m2m.relationship.add(*children)
+ m2m.save()
+ ModelEvent.objects.all().delete()
+
+ m2m.relationship.clear()
+ m2m.save()
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+ self.assertEqual(event.modifications.count(), 0)
+ self.assertEqual(event.relationships.count(), samples)
+
+ children = {str(c.id): c for c in children}
+ for relationship in event.relationships.all():
+ self.assertEqual(relationship.operation, int(Operation.DELETE))
+ self.assertEqual(relationship.field.name, 'relationship')
+ self.assertIn(relationship.entry.primary_key, children)
+
+ def test_one2one(self):
+ """
+ test if OneToOne are correctly recognized,
+ should be handled by save.py
+ """
+
+ o2o = OneToOneTest()
+ o2o.save()
+
+ subject = OrdinaryTest(random=random_string())
+ subject.save()
+ ModelEvent.objects.all().delete()
+
+ o2o.relationship = subject
+ o2o.save()
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+
+ self.assertEqual(event.modifications.count(), 1)
+ self.assertEqual(event.relationships.count(), 0)
+
+ modification = event.modifications.all()[0]
+ self.assertEqual(modification.field.name, 'relationship_id')
+ self.assertEqual(modification.current, repr(subject.pk))
+
+ def test_foreign(self):
+ """
+ test if ForeignKey are correctly recognized.
+
+ should be handled by save.py
+ """
+
+ fk = ForeignKeyTest()
+ fk.save()
+
+ subject = OrdinaryTest(random=random_string())
+ subject.save()
+
+ ModelEvent.objects.all().delete()
+
+ fk.relationship = subject
+ fk.save()
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+
+ self.assertEqual(event.modifications.count(), 1)
+ self.assertEqual(event.relationships.count(), 0)
+
+ modification = event.modifications.all()[0]
+ self.assertEqual(modification.field.name, 'relationship_id')
+ self.assertEqual(modification.current, repr(subject.pk))
+
+ # def test_no_change(self):
+ # samples = 10
+ # children = self.generate_children(samples)
+ #
+ # subject = OrdinaryTest(random=random_string())
+ # subject.save()
+ #
+ # m2m = M2MTest()
+ # m2m.save()
+ # m2m.relationship.add(*children)
+ # m2m.save()
+ # ModelEvent.objects.all().delete()
+ #
+ # m2m.relationship.remove(subject)
+ # m2m.save()
+ #
+ # events = ModelEvent.objects.all()
+ # # TODO: fails
+ # # self.assertEqual(events.count(), 0)
+
+ def test_find(self):
+ m2m = M2MTest()
+ m2m.save()
+
+ self.assertIsNotNone(find_m2m_rel(m2m.relationship.through, M2MTest))
+ self.assertIsNone(find_m2m_rel(m2m.relationship.through, OrdinaryTest))
+
+ def test_reverse(self):
+ m2m = M2MTest()
+ m2m.save()
+
+ subject = OrdinaryTest(random=random_string())
+ subject.save()
+
+ ModelEvent.objects.all().delete()
+
+ subject.m2mtest_set.add(m2m)
+ subject.save()
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+
+ self.assertEqual(event.modifications.count(), 0)
+ self.assertEqual(event.relationships.count(), 1)
+ self.assertEqual(event.entry.mirror.name, 'M2MTest')
+
+ relationship = event.relationships.all()[0]
+ self.assertEqual(relationship.operation, int(Operation.CREATE))
+ self.assertEqual(relationship.field.name, 'relationship')
+ self.assertEqual(relationship.entry.primary_key, str(subject.id))
+
+
+# TODO: test lazy_model_exclusion
diff --git a/galaxy_ng/_vendor/automated_logging/tests/test_misc.py b/galaxy_ng/_vendor/automated_logging/tests/test_misc.py
new file mode 100644
index 0000000000..8b20b596cc
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/tests/test_misc.py
@@ -0,0 +1,61 @@
+from datetime import timedelta
+
+from marshmallow import ValidationError
+
+from automated_logging.signals import _function_model_exclusion
+from automated_logging.tests.base import BaseTestCase
+
+
+class MiscellaneousTestCase(BaseTestCase):
+ def test_no_sender(self):
+ self.assertIsNone(_function_model_exclusion(None, '', ''))
+
+ def test_wrong_duration(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ settings.AUTOMATED_LOGGING['unspecified']['max_age'] = complex(1, 1)
+ conf.load.cache_clear()
+ self.clear()
+
+ self.assertRaises(ValidationError, conf.load)
+
+ settings.AUTOMATED_LOGGING['unspecified']['max_age'] = (
+ timedelta.max.total_seconds() + 1
+ )
+ conf.load.cache_clear()
+ self.clear()
+
+ self.assertRaises(ValidationError, conf.load)
+
+ settings.AUTOMATED_LOGGING['unspecified']['max_age'] = 'Haha, error go brrr'
+ conf.load.cache_clear()
+ self.clear()
+
+ self.assertRaises(ValidationError, conf.load)
+
+ def test_unsupported_search_string(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ settings.AUTOMATED_LOGGING['unspecified']['exclude']['applications'] = [
+ 'te:abc'
+ ]
+ conf.load.cache_clear()
+ self.clear()
+
+ self.assertRaises(ValidationError, conf.load)
+
+ # settings.AUTOMATED_LOGGING['unspecified']['exclude']['applications'] = []
+ conf.load.cache_clear()
+ self.clear()
+
+ def test_duration_none(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ settings.AUTOMATED_LOGGING['unspecified']['max_age'] = None
+ conf.load.cache_clear()
+ self.clear()
+
+ self.assertIsNone(conf.unspecified.max_age)
diff --git a/galaxy_ng/_vendor/automated_logging/tests/test_request.py b/galaxy_ng/_vendor/automated_logging/tests/test_request.py
new file mode 100644
index 0000000000..2d8a2f6191
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/tests/test_request.py
@@ -0,0 +1,153 @@
+""" Test everything related to requests """
+import json
+from copy import deepcopy
+
+from django.http import JsonResponse
+
+from automated_logging.models import RequestEvent
+from automated_logging.tests.base import BaseTestCase, USER_CREDENTIALS
+from automated_logging.tests.helpers import random_string
+
+
+class LoggedOutRequestsTestCase(BaseTestCase):
+ def setUp(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ super().setUp()
+
+ settings.AUTOMATED_LOGGING['request']['exclude']['applications'] = []
+ conf.load.cache_clear()
+
+ RequestEvent.objects.all().delete()
+
+ @staticmethod
+ def view(request):
+ return JsonResponse({})
+
+ def test_simple(self):
+ self.bypass_request_restrictions()
+
+ self.request('GET', self.view)
+
+ events = RequestEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+
+ self.assertEqual(event.user, None)
+
+
+class LoggedInRequestsTestCase(BaseTestCase):
+ def setUp(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ super().setUp()
+
+ settings.AUTOMATED_LOGGING['request']['exclude']['applications'] = []
+ conf.load.cache_clear()
+
+ self.client.login(**USER_CREDENTIALS)
+
+ RequestEvent.objects.all().delete()
+
+ @staticmethod
+ def view(request):
+ return JsonResponse({})
+
+ def test_simple(self):
+ self.bypass_request_restrictions()
+
+ self.request('GET', self.view)
+
+ events = RequestEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+
+ self.assertEqual(event.ip, '127.0.0.1')
+ self.assertEqual(event.user, self.user)
+ self.assertEqual(event.status, 200)
+ self.assertEqual(event.method, 'GET')
+ self.assertEqual(event.uri, '/')
+
+ def test_404(self):
+ self.bypass_request_restrictions()
+
+ self.client.get(f'/{random_string()}')
+
+ events = RequestEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+ self.assertEqual(event.status, 404)
+
+ @staticmethod
+ def exception(request):
+ raise Exception
+
+ def test_500(self):
+ self.bypass_request_restrictions()
+
+ try:
+ self.request('GET', self.exception)
+ except:
+ pass
+
+ events = RequestEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+ self.assertGreaterEqual(event.status, 500)
+
+
+class DataRecordingRequestsTestCase(BaseTestCase):
+ def setUp(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ super().setUp()
+
+ settings.AUTOMATED_LOGGING['request']['exclude']['applications'] = []
+ settings.AUTOMATED_LOGGING['request']['data']['enabled'] = [
+ 'response',
+ 'request',
+ ]
+ conf.load.cache_clear()
+
+ self.client.login(**USER_CREDENTIALS)
+
+ RequestEvent.objects.all().delete()
+
+ @staticmethod
+ def view(request):
+ return JsonResponse({'test': 'example'})
+
+ def test_payload(self):
+ # TODO: preliminary until request/response parsing is implemented
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ self.bypass_request_restrictions()
+
+ settings.AUTOMATED_LOGGING['request']['data']['enabled'] = [
+ 'request',
+ 'response',
+ ]
+ conf.load.cache_clear()
+
+ self.request('GET', self.view, data=json.dumps({'X': 'Y'}))
+
+ events = RequestEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ response = json.dumps({'test': 'example'})
+ request = json.dumps({'X': 'Y'})
+ event = events[0]
+ self.assertEqual(event.response.content.decode(), response)
+ self.assertEqual(event.request.content.decode(), request)
+
+ def test_exclusion_by_application(self):
+ self.request('GET', self.view)
+ self.assertEqual(RequestEvent.objects.count(), 0)
diff --git a/galaxy_ng/_vendor/automated_logging/tests/test_save.py b/galaxy_ng/_vendor/automated_logging/tests/test_save.py
new file mode 100644
index 0000000000..c3fa6de879
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/tests/test_save.py
@@ -0,0 +1,259 @@
+""" Test the save functionality """
+import datetime
+
+from django.http import JsonResponse
+
+from automated_logging.helpers import Operation
+from automated_logging.models import ModelEvent
+from automated_logging.tests.base import BaseTestCase, USER_CREDENTIALS
+from automated_logging.tests.helpers import random_string
+from automated_logging.tests.models import OrdinaryTest
+
+
+class LoggedOutSaveModificationsTestCase(BaseTestCase):
+ def setUp(self):
+ super().setUp()
+
+ # delete all previous model events
+ ModelEvent.objects.all().delete()
+
+ def test_create_simple_value(self):
+ """
+ test if creation results in the correct fields
+ :return:
+ """
+ self.bypass_request_restrictions()
+ value = random_string()
+
+ instance = OrdinaryTest()
+ instance.random = value
+ instance.save()
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+
+ self.assertEqual(event.operation, int(Operation.CREATE))
+ self.assertEqual(event.user, None)
+
+ self.assertEqual(event.entry.primary_key, str(instance.pk))
+ self.assertEqual(event.entry.value, repr(instance))
+
+ self.assertEqual(event.entry.mirror.name, 'OrdinaryTest')
+ self.assertEqual(event.entry.mirror.application.name, 'automated_logging')
+
+ modifications = event.modifications.all()
+ # pk and random added and modified
+ self.assertEqual(modifications.count(), 2)
+ self.assertEqual({m.field.name for m in modifications}, {'random', 'id'})
+
+ modification = [m for m in modifications if m.field.name == 'random'][0]
+ self.assertEqual(modification.operation, int(Operation.CREATE))
+ self.assertEqual(modification.previous, None)
+ self.assertEqual(modification.current, value)
+ self.assertEqual(modification.event, event)
+
+ self.assertEqual(modification.field.name, 'random')
+ self.assertEqual(modification.field.type, 'CharField')
+
+ relationships = event.relationships.all()
+ self.assertEqual(relationships.count(), 0)
+
+ def test_modify(self):
+ """
+ test if modification results
+ in proper delete and create field operations
+ :return:
+ """
+ previous, current = random_string(10), random_string(10)
+
+ # create instance
+ instance = OrdinaryTest()
+ instance.random = previous
+ instance.save()
+
+ # delete all stuff related to the instance events to have a clean slate
+ ModelEvent.objects.all().delete()
+
+ instance.random = current
+ instance.save()
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+
+ self.assertEqual(event.user, None)
+ self.assertEqual(event.operation, int(Operation.MODIFY))
+
+ modifications = event.modifications.all()
+ self.assertEqual(modifications.count(), 1)
+
+ modification = modifications[0]
+ self.assertEqual(modification.operation, int(Operation.MODIFY))
+ self.assertEqual(modification.previous, previous)
+ self.assertEqual(modification.current, current)
+
+ relationships = event.relationships.all()
+ self.assertEqual(relationships.count(), 0)
+
+ def test_honor_save(self):
+ """
+ test if saving honors the only attribute
+
+ :return:
+ """
+ previous1, random1, random2 = (
+ random_string(10),
+ random_string(10),
+ random_string(10),
+ )
+
+ # create instance
+ instance = OrdinaryTest()
+ instance.random = previous1
+ instance.save()
+
+ ModelEvent.objects.all().delete()
+
+ instance.random = random1
+ instance.random2 = random2
+ instance.save(update_fields=['random2'])
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+ modifications = event.modifications.all()
+ self.assertEqual(modifications.count(), 1)
+
+ modification = modifications[0]
+ self.assertEqual(modification.operation, int(Operation.CREATE))
+ self.assertEqual(modification.field.name, 'random2')
+
+ def test_delete(self):
+ """
+ test if deletion is working correctly and records all the changes
+ :return:
+ """
+ value = random_string(10)
+
+ # create instance
+ instance = OrdinaryTest()
+ instance.random = value
+ instance.save()
+
+ pk = instance.pk
+ re = repr(instance)
+
+ ModelEvent.objects.all().delete()
+
+ instance.delete()
+
+ # DUP of save
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+ self.assertEqual(event.operation, int(Operation.DELETE))
+ self.assertEqual(event.user, None)
+
+ self.assertEqual(event.entry.primary_key, str(pk))
+ self.assertEqual(event.entry.value, re)
+
+ # DELETE does currently not record deleted values
+ modifications = event.modifications.all()
+ self.assertEqual(modifications.count(), 0)
+
+ def test_reproducibility(self):
+ """
+ test if all the changes where done
+ correctly so that you can properly derive the
+ current state from all the accumulated changes
+
+ TODO
+ """
+ pass
+
+ def test_performance(self):
+ """
+ test if setting the performance parameter works correctly
+
+ :return:
+ """
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ self.bypass_request_restrictions()
+
+ settings.AUTOMATED_LOGGING['model']['performance'] = True
+ conf.load.cache_clear()
+
+ ModelEvent.objects.all().delete()
+ instance = OrdinaryTest()
+ instance.random = random_string(10)
+ checkpoint = datetime.datetime.now()
+ instance.save()
+ checkpoint = datetime.datetime.now() - checkpoint
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+ self.assertIsNotNone(event.performance)
+ self.assertLess(event.performance.total_seconds(), checkpoint.total_seconds())
+
+ def test_snapshot(self):
+ from django.conf import settings
+ from automated_logging.settings import settings as conf
+
+ self.bypass_request_restrictions()
+
+ settings.AUTOMATED_LOGGING['model']['snapshot'] = True
+ conf.load.cache_clear()
+
+ instance = OrdinaryTest(random=random_string())
+ instance.save()
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+ self.assertIsNotNone(event.snapshot)
+ self.assertEqual(instance, event.snapshot)
+
+
+class LoggedInSaveModificationsTestCase(BaseTestCase):
+ def setUp(self):
+ super().setUp()
+
+ self.client.login(**USER_CREDENTIALS)
+
+ self.clear()
+
+ @staticmethod
+ def view(request):
+ value = random_string()
+
+ instance = OrdinaryTest()
+ instance.random = value
+ instance.save()
+
+ return JsonResponse({})
+
+ def test_user(self):
+ """ Test if DAL recognizes the user through the middleware """
+ self.bypass_request_restrictions()
+
+ response = self.request('GET', self.view)
+ self.assertEqual(response.content, b'{}')
+
+ events = ModelEvent.objects.all()
+ self.assertEqual(events.count(), 1)
+
+ event = events[0]
+ self.assertEqual(event.user, self.user)
+
+
+# TODO: test snapshot
diff --git a/galaxy_ng/_vendor/automated_logging/urls.py b/galaxy_ng/_vendor/automated_logging/urls.py
new file mode 100644
index 0000000000..637600f58a
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/urls.py
@@ -0,0 +1 @@
+urlpatterns = []
diff --git a/galaxy_ng/_vendor/automated_logging/views.py b/galaxy_ng/_vendor/automated_logging/views.py
new file mode 100644
index 0000000000..357400eee3
--- /dev/null
+++ b/galaxy_ng/_vendor/automated_logging/views.py
@@ -0,0 +1,4 @@
+"""
+Automated Logging specific views,
+currently unused except for testing redirection
+"""
diff --git a/galaxy_ng/app/dynaconf_hooks.py b/galaxy_ng/app/dynaconf_hooks.py
index 78b12b33d3..d031ef550b 100755
--- a/galaxy_ng/app/dynaconf_hooks.py
+++ b/galaxy_ng/app/dynaconf_hooks.py
@@ -250,11 +250,9 @@ def configure_logging(settings: Dynaconf) -> Dict[str, Any]:
)
}
if data["GALAXY_ENABLE_API_ACCESS_LOG"]:
- data["INSTALLED_APPS"] = ["dynaconf_merge"]
+ data["INSTALLED_APPS"] = ["galaxy_ng._vendor.automated_logging", "dynaconf_merge"]
data["MIDDLEWARE"] = [
- # We want to re-add this separately with a vendored copy
- # since the upstream is no longer maintained.
- # "automated_logging.middleware.AutomatedLoggingMiddleware",
+ "automated_logging.middleware.AutomatedLoggingMiddleware",
"dynaconf_merge",
]
data["LOGGING"] = {
diff --git a/pyproject.toml b/pyproject.toml
index d52a5843b0..0d200c541e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -75,4 +75,5 @@ ignore = [
"pr_check.sh",
"lint_requirements.txt",
"profiles/**",
+ "galaxy_ng/_vendor/**",
]
diff --git a/requirements/requirements.common.txt b/requirements/requirements.common.txt
index e09cdc0b71..da0e9e4607 100644
--- a/requirements/requirements.common.txt
+++ b/requirements/requirements.common.txt
@@ -1,5 +1,5 @@
#
-# This file is autogenerated by pip-compile with Python 3.10
+# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --output-file=requirements/requirements.common.txt setup.py
@@ -99,6 +99,7 @@ django==4.2.3
# django-guid
# django-import-export
# django-lifecycle
+ # django-picklefield
# djangorestframework
# drf-nested-routers
# drf-spectacular
@@ -113,8 +114,12 @@ django-guid==3.3.0
# via pulpcore
django-import-export==3.1.0
# via pulpcore
+django-ipware==3.0.7
+ # via galaxy-ng (setup.py)
django-lifecycle==1.0.0
# via pulpcore
+django-picklefield==3.1
+ # via galaxy-ng (setup.py)
django-prometheus==2.3.1
# via galaxy-ng (setup.py)
djangorestframework==3.14.0
@@ -201,6 +206,8 @@ markuppy==1.14
# via tablib
markupsafe==2.1.2
# via jinja2
+marshmallow==3.19.0
+ # via galaxy-ng (setup.py)
mccabe==0.7.0
# via flake8
mdurl==0.1.2
@@ -282,6 +289,7 @@ packaging==23.1
# black
# bleach
# django-lifecycle
+ # marshmallow
# pulp-glue
parsley==1.3
# via bindep
@@ -419,8 +427,6 @@ subprocess-tee==0.4.1
# via ansible-lint
tablib[html,ods,xls,xlsx,yaml]==3.4.0
# via django-import-export
-tomli==2.0.1
- # via black
types-cryptography==3.3.23.2
# via pyjwt
types-setuptools==67.7.0.2
diff --git a/requirements/requirements.insights.txt b/requirements/requirements.insights.txt
index 4fd0ee42f8..7d79533c4a 100644
--- a/requirements/requirements.insights.txt
+++ b/requirements/requirements.insights.txt
@@ -1,5 +1,5 @@
#
-# This file is autogenerated by pip-compile with Python 3.10
+# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --output-file=requirements/requirements.insights.txt requirements/requirements.insights.in setup.py
@@ -105,6 +105,7 @@ django==4.2.3
# django-guid
# django-import-export
# django-lifecycle
+ # django-picklefield
# django-storages
# djangorestframework
# drf-nested-routers
@@ -120,8 +121,12 @@ django-guid==3.3.0
# via pulpcore
django-import-export==3.1.0
# via pulpcore
+django-ipware==3.0.7
+ # via galaxy-ng (setup.py)
django-lifecycle==1.0.0
# via pulpcore
+django-picklefield==3.1
+ # via galaxy-ng (setup.py)
django-prometheus==2.3.1
# via galaxy-ng (setup.py)
django-storages[boto3]==1.13.2
@@ -212,6 +217,8 @@ markuppy==1.14
# via tablib
markupsafe==2.1.2
# via jinja2
+marshmallow==3.19.0
+ # via galaxy-ng (setup.py)
mccabe==0.7.0
# via flake8
mdurl==0.1.2
@@ -293,6 +300,7 @@ packaging==23.1
# black
# bleach
# django-lifecycle
+ # marshmallow
# pulp-glue
parsley==1.3
# via bindep
@@ -430,8 +438,6 @@ subprocess-tee==0.4.1
# via ansible-lint
tablib[html,ods,xls,xlsx,yaml]==3.4.0
# via django-import-export
-tomli==2.0.1
- # via black
types-cryptography==3.3.23.2
# via pyjwt
types-setuptools==67.7.0.2
diff --git a/requirements/requirements.standalone.txt b/requirements/requirements.standalone.txt
index ae559ef417..9c6a689487 100644
--- a/requirements/requirements.standalone.txt
+++ b/requirements/requirements.standalone.txt
@@ -1,5 +1,5 @@
#
-# This file is autogenerated by pip-compile with Python 3.10
+# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --output-file=requirements/requirements.standalone.txt requirements/requirements.standalone.in setup.py
@@ -99,6 +99,7 @@ django==4.2.3
# django-guid
# django-import-export
# django-lifecycle
+ # django-picklefield
# djangorestframework
# drf-nested-routers
# drf-spectacular
@@ -113,8 +114,12 @@ django-guid==3.3.0
# via pulpcore
django-import-export==3.1.0
# via pulpcore
+django-ipware==3.0.7
+ # via galaxy-ng (setup.py)
django-lifecycle==1.0.0
# via pulpcore
+django-picklefield==3.1
+ # via galaxy-ng (setup.py)
django-prometheus==2.3.1
# via galaxy-ng (setup.py)
djangorestframework==3.14.0
@@ -201,6 +206,8 @@ markuppy==1.14
# via tablib
markupsafe==2.1.2
# via jinja2
+marshmallow==3.19.0
+ # via galaxy-ng (setup.py)
mccabe==0.7.0
# via flake8
mdurl==0.1.2
@@ -282,6 +289,7 @@ packaging==23.1
# black
# bleach
# django-lifecycle
+ # marshmallow
# pulp-glue
parsley==1.3
# via bindep
@@ -419,8 +427,6 @@ subprocess-tee==0.4.1
# via ansible-lint
tablib[html,ods,xls,xlsx,yaml]==3.4.0
# via django-import-export
-tomli==2.0.1
- # via black
types-cryptography==3.3.23.2
# via pyjwt
types-setuptools==67.7.0.2
diff --git a/setup.py b/setup.py
index 6a04165c8d..c0856de6ef 100644
--- a/setup.py
+++ b/setup.py
@@ -117,9 +117,6 @@ def _format_pulp_requirement(plugin, specifier=None, ref=None, gh_namespace="pul
"django-prometheus>=2.0.0",
"drf-spectacular",
"pulp-container>=2.15.0,<2.17.0",
- # We want to re-add this later with a vendored copy since
- # upstream is no longer maintained.
- # "django-automated-logging@git+https://github.com/jctanner/django-automated-logging@DJANGO_4x",
"social-auth-core>=4.4.2",
"social-auth-app-django>=5.2.0",
"dynaconf>=3.1.12",
@@ -127,6 +124,10 @@ def _format_pulp_requirement(plugin, specifier=None, ref=None, gh_namespace="pul
"insights_analytics_collector>=0.3.0",
"boto3",
"distro",
+ # From vendored automated_logging
+ "marshmallow<4.0.0,>=3.6.1",
+ "django-picklefield<4.0.0,>=3.0.1",
+ "django-ipware<4.0.0,>=3.0.0",
]