mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 01:48:38 -06:00
Compare commits
1 Commits
2f64a66c27
...
4748fc4422
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4748fc4422 |
@ -158,7 +158,6 @@ LOGGING = {
|
|||||||
* `netbox.<app>.<model>` - Generic form for model-specific log messages
|
* `netbox.<app>.<model>` - Generic form for model-specific log messages
|
||||||
* `netbox.auth.*` - Authentication events
|
* `netbox.auth.*` - Authentication events
|
||||||
* `netbox.api.views.*` - Views which handle business logic for the REST API
|
* `netbox.api.views.*` - Views which handle business logic for the REST API
|
||||||
* `netbox.jobs.*` - Background jobs
|
|
||||||
* `netbox.reports.*` - Report execution (`module.name`)
|
* `netbox.reports.*` - Report execution (`module.name`)
|
||||||
* `netbox.scripts.*` - Custom script execution (`module.name`)
|
* `netbox.scripts.*` - Custom script execution (`module.name`)
|
||||||
* `netbox.views.*` - Views which handle business logic for the web UI
|
* `netbox.views.*` - Views which handle business logic for the web UI
|
||||||
|
@ -38,27 +38,6 @@ You can schedule the background job from within your code (e.g. from a model's `
|
|||||||
|
|
||||||
This is the human-friendly names of your background job. If omitted, the class name will be used.
|
This is the human-friendly names of your background job. If omitted, the class name will be used.
|
||||||
|
|
||||||
### Logging
|
|
||||||
|
|
||||||
!!! info "This feature was introduced in NetBox v4.4."
|
|
||||||
|
|
||||||
A Python logger is instantiated by the runner for each job. It can be utilized within a job's `run()` method as needed:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def run(self, *args, **kwargs):
|
|
||||||
obj = MyModel.objects.get(pk=kwargs.get('pk'))
|
|
||||||
self.logger.info("Retrieved object {obj}")
|
|
||||||
```
|
|
||||||
|
|
||||||
Four of the standard Python logging levels are supported:
|
|
||||||
|
|
||||||
* `debug()`
|
|
||||||
* `info()`
|
|
||||||
* `warning()`
|
|
||||||
* `error()`
|
|
||||||
|
|
||||||
Log entries recorded using the runner's logger will be saved in the job's log in the database in addition to being processed by other [system logging handlers](../../configuration/system.md#logging).
|
|
||||||
|
|
||||||
### Scheduled Jobs
|
### Scheduled Jobs
|
||||||
|
|
||||||
As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`.
|
As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`.
|
||||||
|
@ -4,31 +4,23 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from rq.job import JobStatus
|
from rq.job import JobStatus
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'JOB_LOG_ENTRY_LEVELS',
|
|
||||||
'RQ_TASK_STATUSES',
|
'RQ_TASK_STATUSES',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Badge:
|
class Status:
|
||||||
label: str
|
label: str
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
|
|
||||||
RQ_TASK_STATUSES = {
|
RQ_TASK_STATUSES = {
|
||||||
JobStatus.QUEUED: Badge(_('Queued'), 'cyan'),
|
JobStatus.QUEUED: Status(_('Queued'), 'cyan'),
|
||||||
JobStatus.FINISHED: Badge(_('Finished'), 'green'),
|
JobStatus.FINISHED: Status(_('Finished'), 'green'),
|
||||||
JobStatus.FAILED: Badge(_('Failed'), 'red'),
|
JobStatus.FAILED: Status(_('Failed'), 'red'),
|
||||||
JobStatus.STARTED: Badge(_('Started'), 'blue'),
|
JobStatus.STARTED: Status(_('Started'), 'blue'),
|
||||||
JobStatus.DEFERRED: Badge(_('Deferred'), 'gray'),
|
JobStatus.DEFERRED: Status(_('Deferred'), 'gray'),
|
||||||
JobStatus.SCHEDULED: Badge(_('Scheduled'), 'purple'),
|
JobStatus.SCHEDULED: Status(_('Scheduled'), 'purple'),
|
||||||
JobStatus.STOPPED: Badge(_('Stopped'), 'orange'),
|
JobStatus.STOPPED: Status(_('Stopped'), 'orange'),
|
||||||
JobStatus.CANCELED: Badge(_('Cancelled'), 'yellow'),
|
JobStatus.CANCELED: Status(_('Cancelled'), 'yellow'),
|
||||||
}
|
|
||||||
|
|
||||||
JOB_LOG_ENTRY_LEVELS = {
|
|
||||||
'debug': Badge(_('Debug'), 'gray'),
|
|
||||||
'info': Badge(_('Info'), 'blue'),
|
|
||||||
'warning': Badge(_('Warning'), 'orange'),
|
|
||||||
'error': Badge(_('Error'), 'red'),
|
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@ import logging
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'JobLogEntry',
|
'JobLogEntry',
|
||||||
)
|
)
|
||||||
@ -14,7 +12,7 @@ __all__ = (
|
|||||||
class JobLogEntry:
|
class JobLogEntry:
|
||||||
level: str
|
level: str
|
||||||
message: str
|
message: str
|
||||||
timestamp: datetime = field(default_factory=timezone.now)
|
timestamp: datetime = field(default_factory=datetime.now)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_logrecord(cls, record: logging.LogRecord):
|
def from_logrecord(cls, record: logging.LogRecord):
|
||||||
|
@ -7,6 +7,7 @@ from netbox.jobs import JobRunner, system_job
|
|||||||
from netbox.search.backends import search_backend
|
from netbox.search.backends import search_backend
|
||||||
from utilities.proxy import resolve_proxies
|
from utilities.proxy import resolve_proxies
|
||||||
from .choices import DataSourceStatusChoices, JobIntervalChoices
|
from .choices import DataSourceStatusChoices, JobIntervalChoices
|
||||||
|
from .exceptions import SyncError
|
||||||
from .models import DataSource
|
from .models import DataSource
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -22,23 +23,19 @@ class SyncDataSourceJob(JobRunner):
|
|||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
datasource = DataSource.objects.get(pk=self.job.object_id)
|
datasource = DataSource.objects.get(pk=self.job.object_id)
|
||||||
self.logger.debug(f"Found DataSource ID {datasource.pk}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.logger.info(f"Syncing data source {datasource}")
|
|
||||||
datasource.sync()
|
datasource.sync()
|
||||||
|
|
||||||
# Update the search cache for DataFiles belonging to this source
|
# Update the search cache for DataFiles belonging to this source
|
||||||
self.logger.debug("Updating search cache for data files")
|
|
||||||
search_backend.cache(datasource.datafiles.iterator())
|
search_backend.cache(datasource.datafiles.iterator())
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error syncing data source: {e}")
|
|
||||||
DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
|
DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
|
||||||
|
if type(e) is SyncError:
|
||||||
|
logging.error(e)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
self.logger.info("Syncing completed successfully")
|
|
||||||
|
|
||||||
|
|
||||||
@system_job(interval=JobIntervalChoices.INTERVAL_DAILY)
|
@system_job(interval=JobIntervalChoices.INTERVAL_DAILY)
|
||||||
class SystemHousekeepingJob(JobRunner):
|
class SystemHousekeepingJob(JobRunner):
|
||||||
|
@ -2,8 +2,6 @@ import django.contrib.postgres.fields
|
|||||||
import django.core.serializers.json
|
import django.core.serializers.json
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
import utilities.json
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
@ -17,7 +15,6 @@ class Migration(migrations.Migration):
|
|||||||
name='log_entries',
|
name='log_entries',
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
base_field=models.JSONField(
|
base_field=models.JSONField(
|
||||||
decoder=utilities.json.JobLogDecoder,
|
|
||||||
encoder=django.core.serializers.json.DjangoJSONEncoder
|
encoder=django.core.serializers.json.DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType, ContentTypeManager
|
from django.contrib.contenttypes.models import ContentType, ContentTypeManager
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from netbox.plugins import PluginConfig
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from utilities.string import title
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ObjectType',
|
'ObjectType',
|
||||||
@ -50,29 +48,3 @@ class ObjectType(ContentType):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
proxy = True
|
proxy = True
|
||||||
|
|
||||||
@property
|
|
||||||
def app_labeled_name(self):
|
|
||||||
# Override ContentType's "app | model" representation style.
|
|
||||||
return f"{self.app_verbose_name} > {title(self.model_verbose_name)}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def app_verbose_name(self):
|
|
||||||
if model := self.model_class():
|
|
||||||
return model._meta.app_config.verbose_name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def model_verbose_name(self):
|
|
||||||
if model := self.model_class():
|
|
||||||
return model._meta.verbose_name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def model_verbose_name_plural(self):
|
|
||||||
if model := self.model_class():
|
|
||||||
return model._meta.verbose_name_plural
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_plugin_model(self):
|
|
||||||
if not (model := self.model_class()):
|
|
||||||
return # Return null if model class is invalid
|
|
||||||
return isinstance(model._meta.app_config, PluginConfig)
|
|
||||||
|
@ -20,7 +20,6 @@ from core.choices import JobStatusChoices
|
|||||||
from core.dataclasses import JobLogEntry
|
from core.dataclasses import JobLogEntry
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from core.signals import job_end, job_start
|
from core.signals import job_end, job_start
|
||||||
from utilities.json import JobLogDecoder
|
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.rqworker import get_queue_for_model
|
from utilities.rqworker import get_queue_for_model
|
||||||
|
|
||||||
@ -110,10 +109,9 @@ class Job(models.Model):
|
|||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
log_entries = ArrayField(
|
log_entries = ArrayField(
|
||||||
verbose_name=_('log entries'),
|
|
||||||
base_field=models.JSONField(
|
base_field=models.JSONField(
|
||||||
encoder=DjangoJSONEncoder,
|
encoder=DjangoJSONEncoder,
|
||||||
decoder=JobLogDecoder,
|
# TODO: Specify a decoder to handle ISO 8601 timestamps
|
||||||
),
|
),
|
||||||
blank=True,
|
blank=True,
|
||||||
default=list,
|
default=list,
|
||||||
@ -219,13 +217,6 @@ class Job(models.Model):
|
|||||||
# Send signal
|
# Send signal
|
||||||
job_end.send(self)
|
job_end.send(self)
|
||||||
|
|
||||||
def log(self, record: logging.LogRecord):
|
|
||||||
"""
|
|
||||||
Record a LogRecord from Python's native logging in the job's log.
|
|
||||||
"""
|
|
||||||
entry = JobLogEntry.from_logrecord(record)
|
|
||||||
self.log_entries.append(asdict(entry))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def enqueue(
|
def enqueue(
|
||||||
cls,
|
cls,
|
||||||
@ -292,3 +283,10 @@ class Job(models.Model):
|
|||||||
transaction.on_commit(callback)
|
transaction.on_commit(callback)
|
||||||
|
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
def log(self, record: logging.LogRecord):
|
||||||
|
"""
|
||||||
|
Record a Python LogRecord in the job's log.
|
||||||
|
"""
|
||||||
|
entry = JobLogEntry.from_logrecord(record)
|
||||||
|
self.log_entries.append(asdict(entry))
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from core.constants import RQ_TASK_STATUSES
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BackendTypeColumn',
|
'BackendTypeColumn',
|
||||||
'BadgeColumn',
|
'RQJobStatusColumn',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -22,21 +23,14 @@ class BackendTypeColumn(tables.Column):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class BadgeColumn(tables.Column):
|
class RQJobStatusColumn(tables.Column):
|
||||||
"""
|
"""
|
||||||
Render a colored badge for a value.
|
Render a colored label for the status of an RQ job.
|
||||||
|
|
||||||
Args:
|
|
||||||
badges: A dictionary mapping of values to core.constants.Badge instances.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, badges, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.badges = badges
|
|
||||||
|
|
||||||
def render(self, value):
|
def render(self, value):
|
||||||
badge = self.badges.get(value)
|
status = RQ_TASK_STATUSES.get(value)
|
||||||
return mark_safe(f'<span class="badge text-bg-{badge.color}">{badge.label}</span>')
|
return mark_safe(f'<span class="badge text-bg-{status.color}">{status.label}</span>')
|
||||||
|
|
||||||
def value(self, value):
|
def value(self, value):
|
||||||
badge = self.badges.get(value)
|
status = RQ_TASK_STATUSES.get(value)
|
||||||
return badge.label
|
return status.label
|
||||||
|
@ -2,9 +2,7 @@ import django_tables2 as tables
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.tables import BaseTable, NetBoxTable, columns
|
from netbox.tables import BaseTable, NetBoxTable, columns
|
||||||
from core.constants import JOB_LOG_ENTRY_LEVELS
|
from ..models import Job
|
||||||
from core.models import Job
|
|
||||||
from core.tables.columns import BadgeColumn
|
|
||||||
|
|
||||||
|
|
||||||
class JobTable(NetBoxTable):
|
class JobTable(NetBoxTable):
|
||||||
@ -64,12 +62,10 @@ class JobTable(NetBoxTable):
|
|||||||
|
|
||||||
|
|
||||||
class JobLogEntryTable(BaseTable):
|
class JobLogEntryTable(BaseTable):
|
||||||
timestamp = columns.DateTimeColumn(
|
timestamp = tables.Column(
|
||||||
timespec='milliseconds',
|
|
||||||
verbose_name=_('Time'),
|
verbose_name=_('Time'),
|
||||||
)
|
)
|
||||||
level = BadgeColumn(
|
level = tables.Column(
|
||||||
badges=JOB_LOG_ENTRY_LEVELS,
|
|
||||||
verbose_name=_('Level'),
|
verbose_name=_('Level'),
|
||||||
)
|
)
|
||||||
message = tables.Column(
|
message = tables.Column(
|
||||||
|
@ -2,8 +2,7 @@ import django_tables2 as tables
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_tables2.utils import A
|
from django_tables2.utils import A
|
||||||
|
|
||||||
from core.constants import RQ_TASK_STATUSES
|
from core.tables.columns import RQJobStatusColumn
|
||||||
from core.tables.columns import BadgeColumn
|
|
||||||
from netbox.tables import BaseTable, columns
|
from netbox.tables import BaseTable, columns
|
||||||
|
|
||||||
|
|
||||||
@ -85,8 +84,7 @@ class BackgroundTaskTable(BaseTable):
|
|||||||
ended_at = columns.DateTimeColumn(
|
ended_at = columns.DateTimeColumn(
|
||||||
verbose_name=_("Ended")
|
verbose_name=_("Ended")
|
||||||
)
|
)
|
||||||
status = BadgeColumn(
|
status = RQJobStatusColumn(
|
||||||
badges=RQ_TASK_STATUSES,
|
|
||||||
verbose_name=_("Status"),
|
verbose_name=_("Status"),
|
||||||
accessor='get_status'
|
accessor='get_status'
|
||||||
)
|
)
|
||||||
|
@ -188,7 +188,7 @@ class JobView(generic.ObjectView):
|
|||||||
class JobLogView(generic.ObjectView):
|
class JobLogView(generic.ObjectView):
|
||||||
queryset = Job.objects.all()
|
queryset = Job.objects.all()
|
||||||
actions = (DeleteObject,)
|
actions = (DeleteObject,)
|
||||||
template_name = 'core/job/log.html'
|
template_name = 'core/job_log.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Log'),
|
label=_('Log'),
|
||||||
badge=lambda obj: len(obj.log_entries),
|
badge=lambda obj: len(obj.log_entries),
|
||||||
|
@ -1507,7 +1507,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
tx_power = forms.IntegerField(
|
tx_power = forms.IntegerField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Transmit power (dBm)'),
|
label=_('Transmit power (dBm)'),
|
||||||
min_value=-40,
|
min_value=0,
|
||||||
max_value=127
|
max_value=127
|
||||||
)
|
)
|
||||||
vrf_id = DynamicModelMultipleChoiceField(
|
vrf_id = DynamicModelMultipleChoiceField(
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('dcim', '0208_platform_manufacturer_uniqueness'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='interface',
|
|
||||||
name='tx_power',
|
|
||||||
field=models.SmallIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(-40),
|
|
||||||
django.core.validators.MaxValueValidator(127)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -719,13 +719,10 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
verbose_name=('channel width (MHz)'),
|
verbose_name=('channel width (MHz)'),
|
||||||
help_text=_("Populated by selected channel (if set)")
|
help_text=_("Populated by selected channel (if set)")
|
||||||
)
|
)
|
||||||
tx_power = models.SmallIntegerField(
|
tx_power = models.PositiveSmallIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=(
|
validators=(MaxValueValidator(127),),
|
||||||
MinValueValidator(-40),
|
|
||||||
MaxValueValidator(127),
|
|
||||||
),
|
|
||||||
verbose_name=_('transmit power (dBm)')
|
verbose_name=_('transmit power (dBm)')
|
||||||
)
|
)
|
||||||
poe_mode = models.CharField(
|
poe_mode = models.CharField(
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
import inspect
|
|
||||||
|
|
||||||
from django.urls import NoReverseMatch, reverse
|
|
||||||
from drf_spectacular.types import OpenApiTypes
|
|
||||||
from drf_spectacular.utils import extend_schema_field
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from netbox.api.serializers import BaseModelSerializer
|
from netbox.api.serializers import BaseModelSerializer
|
||||||
from utilities.views import get_viewname
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ObjectTypeSerializer',
|
'ObjectTypeSerializer',
|
||||||
@ -16,32 +10,7 @@ __all__ = (
|
|||||||
|
|
||||||
class ObjectTypeSerializer(BaseModelSerializer):
|
class ObjectTypeSerializer(BaseModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objecttype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objecttype-detail')
|
||||||
app_name = serializers.CharField(source='app_verbose_name', read_only=True)
|
|
||||||
model_name = serializers.CharField(source='model_verbose_name', read_only=True)
|
|
||||||
model_name_plural = serializers.CharField(source='model_verbose_name_plural', read_only=True)
|
|
||||||
is_plugin_model = serializers.BooleanField(read_only=True)
|
|
||||||
rest_api_endpoint = serializers.SerializerMethodField()
|
|
||||||
description = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ObjectType
|
model = ObjectType
|
||||||
fields = [
|
fields = ['id', 'url', 'display', 'app_label', 'model']
|
||||||
'id', 'url', 'display', 'app_label', 'app_name', 'model', 'model_name', 'model_name_plural',
|
|
||||||
'is_plugin_model', 'rest_api_endpoint', 'description',
|
|
||||||
]
|
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.STR)
|
|
||||||
def get_rest_api_endpoint(self, obj):
|
|
||||||
if not (model := obj.model_class()):
|
|
||||||
return
|
|
||||||
if viewname := get_viewname(model, action='list', rest_api=True):
|
|
||||||
try:
|
|
||||||
return reverse(viewname)
|
|
||||||
except NoReverseMatch:
|
|
||||||
return
|
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.STR)
|
|
||||||
def get_description(self, obj):
|
|
||||||
if not (model := obj.model_class()):
|
|
||||||
return
|
|
||||||
return inspect.getdoc(model)
|
|
||||||
|
@ -35,9 +35,7 @@ def system_job(interval):
|
|||||||
|
|
||||||
|
|
||||||
class JobLogHandler(logging.Handler):
|
class JobLogHandler(logging.Handler):
|
||||||
"""
|
|
||||||
A logging handler which records entries on a Job.
|
|
||||||
"""
|
|
||||||
def __init__(self, job, *args, **kwargs):
|
def __init__(self, job, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.job = job
|
self.job = job
|
||||||
|
@ -11,10 +11,7 @@ from core.choices import JobStatusChoices
|
|||||||
|
|
||||||
class TestJobRunner(JobRunner):
|
class TestJobRunner(JobRunner):
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
self.logger.debug("Debug message")
|
pass
|
||||||
self.logger.info("Info message")
|
|
||||||
self.logger.warning("Warning message")
|
|
||||||
self.logger.error("Error message")
|
|
||||||
|
|
||||||
|
|
||||||
class JobRunnerTestCase(TestCase):
|
class JobRunnerTestCase(TestCase):
|
||||||
@ -50,16 +47,8 @@ class JobRunnerTest(JobRunnerTestCase):
|
|||||||
def test_handle(self):
|
def test_handle(self):
|
||||||
job = TestJobRunner.enqueue(immediate=True)
|
job = TestJobRunner.enqueue(immediate=True)
|
||||||
|
|
||||||
# Check job status
|
|
||||||
self.assertEqual(job.status, JobStatusChoices.STATUS_COMPLETED)
|
self.assertEqual(job.status, JobStatusChoices.STATUS_COMPLETED)
|
||||||
|
|
||||||
# Check logging
|
|
||||||
self.assertEqual(len(job.log_entries), 4)
|
|
||||||
self.assertEqual(job.log_entries[0]['message'], "Debug message")
|
|
||||||
self.assertEqual(job.log_entries[1]['message'], "Info message")
|
|
||||||
self.assertEqual(job.log_entries[2]['message'], "Warning message")
|
|
||||||
self.assertEqual(job.log_entries[3]['message'], "Error message")
|
|
||||||
|
|
||||||
def test_handle_errored(self):
|
def test_handle_errored(self):
|
||||||
class ErroredJobRunner(TestJobRunner):
|
class ErroredJobRunner(TestJobRunner):
|
||||||
EXP = Exception('Test error')
|
EXP = Exception('Test error')
|
||||||
|
@ -1,6 +1,27 @@
|
|||||||
{% extends 'core/job/base.html' %}
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load perms %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if object.object %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object|meta:"verbose_name_plural"|bettertitle }}</a>
|
||||||
|
</li>
|
||||||
|
{% with parent_jobs_viewname=object.object|viewname:"jobs" %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url parent_jobs_viewname pk=object.object.pk %}">{{ object.object }}</a>
|
||||||
|
</li>
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'core:job_list' %}?name={{ object.name|urlencode }}">{{ object.name }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock breadcrumbs %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-12 col-md-6">
|
<div class="col col-12 col-md-6">
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
{% extends 'core/job/base.html' %}
|
|
||||||
{% load render_table from django_tables2 %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col">
|
|
||||||
<div class="card">
|
|
||||||
{% render_table table %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -3,6 +3,7 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load perms %}
|
{% load perms %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
@ -21,3 +22,13 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock breadcrumbs %}
|
{% endblock breadcrumbs %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
{% render_table table %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -1,14 +1,10 @@
|
|||||||
import decimal
|
import decimal
|
||||||
import json
|
|
||||||
|
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
from utilities.datetime import datetime_from_timestamp
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigJSONEncoder',
|
'ConfigJSONEncoder',
|
||||||
'CustomFieldJSONEncoder',
|
'CustomFieldJSONEncoder',
|
||||||
'JobLogDecoder',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -33,21 +29,3 @@ class ConfigJSONEncoder(DjangoJSONEncoder):
|
|||||||
return type(o).__name__
|
return type(o).__name__
|
||||||
|
|
||||||
return super().default(o)
|
return super().default(o)
|
||||||
|
|
||||||
|
|
||||||
class JobLogDecoder(json.JSONDecoder):
|
|
||||||
"""
|
|
||||||
Deserialize JobLogEntry timestamps.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
kwargs['object_hook'] = self._deserialize_entry
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def _deserialize_entry(self, obj: dict) -> dict:
|
|
||||||
if obj.get('timestamp'):
|
|
||||||
# Deserialize a timestamp string to a native datetime object
|
|
||||||
try:
|
|
||||||
obj['timestamp'] = datetime_from_timestamp(obj['timestamp'])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return obj
|
|
||||||
|
Loading…
Reference in New Issue
Block a user