Compare commits

..

1 Commits

Author SHA1 Message Date
Jeremy Stretch
d3c73c2862 Closes #21929: Eliminate redundant object view templates 2026-04-15 16:55:26 -04:00
53 changed files with 49 additions and 144 deletions

View File

@@ -115,20 +115,6 @@ commit_default = False
By default, a script can be scheduled for execution at a later time. Setting `scheduling_enabled` to False disables this ability: Only immediate execution will be possible. (This also disables the ability to set a recurring execution interval.)
### `notifications_default`
By default, a notification is generated for the requesting user each time a script finishes running. This attribute sets the initial value for the notifications field when running a script. Valid values are `always` (default), `on_failure`, and `never`.
```python
notifications_default = 'on_failure'
```
| Value | Behavior |
|-------|----------|
| `always` | Notify on every completion (default) |
| `on_failure` | Notify only when the job fails or errors |
| `never` | Never send a notification |
### `job_timeout`
Set the maximum allowed runtime for the script. If not set, `RQ_DEFAULT_TIMEOUT` will be used.

View File

@@ -26,14 +26,13 @@ class JobSerializer(BaseModelSerializer):
object = serializers.SerializerMethodField(
read_only=True
)
notifications = ChoiceField(choices=JobNotificationChoices, read_only=True)
class Meta:
model = Job
fields = [
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'object', 'name', 'status', 'created',
'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'queue_name',
'notifications', 'log_entries',
'log_entries',
]
brief_fields = ('url', 'created', 'completed', 'user', 'status')

View File

@@ -72,18 +72,6 @@ class JobStatusChoices(ChoiceSet):
)
class JobNotificationChoices(ChoiceSet):
NOTIFICATION_ALWAYS = 'always'
NOTIFICATION_ON_FAILURE = 'on_failure'
NOTIFICATION_NEVER = 'never'
CHOICES = (
(NOTIFICATION_ALWAYS, _('Always')),
(NOTIFICATION_ON_FAILURE, _('On failure')),
(NOTIFICATION_NEVER, _('Never')),
)
class JobIntervalChoices(ChoiceSet):
INTERVAL_MINUTELY = 1
INTERVAL_HOURLY = 60

View File

@@ -1,16 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0023_datasource_sync_permission'),
]
operations = [
migrations.AddField(
model_name='job',
name='notifications',
field=models.CharField(default='always', max_length=30),
),
]

View File

@@ -16,7 +16,7 @@ from django.utils import timezone
from django.utils.translation import gettext as _
from rq.exceptions import InvalidJobOperation
from core.choices import JobNotificationChoices, JobStatusChoices
from core.choices import JobStatusChoices
from core.dataclasses import JobLogEntry
from core.events import JOB_COMPLETED, JOB_ERRORED, JOB_FAILED
from core.models import ObjectType
@@ -118,12 +118,6 @@ class Job(models.Model):
blank=True,
help_text=_('Name of the queue in which this job was enqueued')
)
notifications = models.CharField(
verbose_name=_('notifications'),
max_length=30,
choices=JobNotificationChoices,
default=JobNotificationChoices.NOTIFICATION_ALWAYS
)
log_entries = ArrayField(
verbose_name=_('log entries'),
base_field=models.JSONField(
@@ -244,16 +238,12 @@ class Job(models.Model):
self.save()
# Notify the user (if any) of completion
if self.user and self.notifications != JobNotificationChoices.NOTIFICATION_NEVER:
if (
self.notifications == JobNotificationChoices.NOTIFICATION_ALWAYS or
status != JobStatusChoices.STATUS_COMPLETED
):
Notification(
user=self.user,
object=self,
event_type=self.get_event_type(),
).save()
if self.user:
Notification(
user=self.user,
object=self,
event_type=self.get_event_type(),
).save()
# Send signal
job_end.send(self)
@@ -277,7 +267,6 @@ class Job(models.Model):
interval=None,
immediate=False,
queue_name=None,
notifications=JobNotificationChoices.NOTIFICATION_ALWAYS,
**kwargs
):
"""
@@ -292,7 +281,6 @@ class Job(models.Model):
interval: Recurrence interval (in minutes)
immediate: Run the job immediately without scheduling it in the background. Should be used for interactive
management commands only.
notifications: Notification behavior on job completion (always, on_failure, or never)
"""
if schedule_at and immediate:
raise ValueError(_("enqueue() cannot be called with values for both schedule_at and immediate."))
@@ -314,8 +302,7 @@ class Job(models.Model):
interval=interval,
user=user,
job_id=uuid.uuid4(),
queue_name=rq_queue_name,
notifications=notifications
queue_name=rq_queue_name
)
job.full_clean()
job.save()

View File

@@ -4060,6 +4060,7 @@ class InventoryItemRoleListView(generic.ObjectListView):
@register_model_view(InventoryItemRole)
class InventoryItemRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = InventoryItemRole.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.InventoryItemRolePanel(),
@@ -4308,6 +4309,7 @@ class CableListView(generic.ObjectListView):
@register_model_view(Cable)
class CableView(generic.ObjectView):
queryset = Cable.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.CablePanel(),
@@ -4458,6 +4460,7 @@ class VirtualChassisListView(generic.ObjectListView):
@register_model_view(VirtualChassis)
class VirtualChassisView(generic.ObjectView):
queryset = VirtualChassis.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.VirtualChassisPanel(),
@@ -4883,6 +4886,7 @@ class VirtualDeviceContextListView(generic.ObjectListView):
@register_model_view(VirtualDeviceContext)
class VirtualDeviceContextView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VirtualDeviceContext.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.VirtualDeviceContextPanel(),
@@ -4968,6 +4972,7 @@ class MACAddressListView(generic.ObjectListView):
@register_model_view(MACAddress)
class MACAddressView(generic.ObjectView):
queryset = MACAddress.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.MACAddressPanel(),

View File

@@ -7,7 +7,7 @@ from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from core.api.serializers_.jobs import JobSerializer
from core.choices import JobNotificationChoices, ManagedFileRootPathChoices
from core.choices import ManagedFileRootPathChoices
from extras.models import Script, ScriptModule
from netbox.api.serializers import ValidatedModelSerializer
from utilities.datetime import local_now
@@ -114,20 +114,6 @@ class ScriptInputSerializer(serializers.Serializer):
commit = serializers.BooleanField()
schedule_at = serializers.DateTimeField(required=False, allow_null=True)
interval = serializers.IntegerField(required=False, allow_null=True)
notifications = serializers.CharField(required=False, default=JobNotificationChoices.NOTIFICATION_ALWAYS)
def validate_notifications(self, value):
"""
Validates the provided notifications value.
"""
valid_choices = dict(JobNotificationChoices.CHOICES).keys()
if value not in valid_choices:
raise serializers.ValidationError(
_('Invalid choice. Valid choices are: {choices}').format(
choices=', '.join(valid_choices)
)
)
return value
def validate_schedule_at(self, value):
"""

View File

@@ -13,7 +13,6 @@ from rest_framework.routers import APIRootView
from rest_framework.viewsets import ModelViewSet
from rq import Worker
from core.choices import JobNotificationChoices
from extras import filtersets
from extras.jobs import ScriptJob
from extras.models import *
@@ -339,10 +338,7 @@ class ScriptViewSet(ModelViewSet):
commit=input_serializer.data['commit'],
job_timeout=script.python_class.job_timeout,
schedule_at=input_serializer.validated_data.get('schedule_at'),
interval=input_serializer.validated_data.get('interval'),
notifications=input_serializer.validated_data.get(
'notifications', JobNotificationChoices.NOTIFICATION_ALWAYS
),
interval=input_serializer.validated_data.get('interval')
)
serializer = serializers.ScriptDetailSerializer(script, context={'request': request})

View File

@@ -2,7 +2,7 @@ from django import forms
from django.core.files.storage import storages
from django.utils.translation import gettext_lazy as _
from core.choices import JobIntervalChoices, JobNotificationChoices
from core.choices import JobIntervalChoices
from core.forms import ManagedFileForm
from utilities.datetime import local_now
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
@@ -35,13 +35,6 @@ class ScriptForm(forms.Form):
),
help_text=_("Interval at which this script is re-run (in minutes)")
)
_notifications = forms.ChoiceField(
required=False,
choices=JobNotificationChoices,
initial=JobNotificationChoices.NOTIFICATION_ALWAYS,
label=_("Notifications"),
help_text=_("When to notify the user of job completion")
)
def __init__(self, *args, scheduling_enabled=True, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -389,10 +389,6 @@ class BaseScript:
def scheduling_enabled(self):
return getattr(self.Meta, 'scheduling_enabled', True)
@classproperty
def notifications_default(self):
return getattr(self.Meta, 'notifications_default', 'always')
@property
def filename(self):
return inspect.getfile(self.__class__)
@@ -495,10 +491,7 @@ class BaseScript:
fieldsets.append((_('Script Data'), fields))
# Append the default fieldset if defined in the Meta class
if self.scheduling_enabled:
exec_parameters = ('_schedule_at', '_interval', '_commit', '_notifications')
else:
exec_parameters = ('_commit', '_notifications')
exec_parameters = ('_schedule_at', '_interval', '_commit') if self.scheduling_enabled else ('_commit',)
fieldsets.append((_('Script Execution Parameters'), exec_parameters))
return fieldsets
@@ -518,9 +511,6 @@ class BaseScript:
# Set initial "commit" checkbox state based on the script's Meta parameter
form.fields['_commit'].initial = self.commit_default
# Set initial "notifications" selection based on the script's Meta parameter
form.fields['_notifications'].initial = self.notifications_default
# Hide fields if scheduling has been disabled
if not self.scheduling_enabled:
form.fields['_schedule_at'].widget = forms.HiddenInput()

View File

@@ -13,7 +13,7 @@ from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
from django.views.generic import View
from core.choices import JobNotificationChoices, ManagedFileRootPathChoices
from core.choices import ManagedFileRootPathChoices
from core.models import Job
from core.object_actions import BulkSync
from dcim.models import Device, DeviceRole, Platform
@@ -65,6 +65,7 @@ class CustomFieldListView(generic.ObjectListView):
@register_model_view(CustomField)
class CustomFieldView(generic.ObjectView):
queryset = CustomField.objects.select_related('choice_set')
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.CustomFieldPanel(),
@@ -148,6 +149,7 @@ class CustomFieldChoiceSetListView(generic.ObjectListView):
@register_model_view(CustomFieldChoiceSet)
class CustomFieldChoiceSetView(generic.ObjectView):
queryset = CustomFieldChoiceSet.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.CustomFieldChoiceSetPanel(),
@@ -231,6 +233,7 @@ class CustomLinkListView(generic.ObjectListView):
@register_model_view(CustomLink)
class CustomLinkView(generic.ObjectView):
queryset = CustomLink.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.CustomLinkPanel(),
@@ -298,6 +301,7 @@ class ExportTemplateListView(generic.ObjectListView):
@register_model_view(ExportTemplate)
class ExportTemplateView(generic.ObjectView):
queryset = ExportTemplate.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.ExportTemplatePanel(),
@@ -372,6 +376,7 @@ class SavedFilterListView(SharedObjectViewMixin, generic.ObjectListView):
@register_model_view(SavedFilter)
class SavedFilterView(SharedObjectViewMixin, generic.ObjectView):
queryset = SavedFilter.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.SavedFilterPanel(),
@@ -443,6 +448,7 @@ class TableConfigListView(SharedObjectViewMixin, generic.ObjectListView):
@register_model_view(TableConfig)
class TableConfigView(SharedObjectViewMixin, generic.ObjectView):
queryset = TableConfig.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.TableConfigPanel(),
@@ -545,6 +551,7 @@ class NotificationGroupListView(generic.ObjectListView):
@register_model_view(NotificationGroup)
class NotificationGroupView(generic.ObjectView):
queryset = NotificationGroup.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.NotificationGroupPanel(),
@@ -738,6 +745,7 @@ class WebhookListView(generic.ObjectListView):
@register_model_view(Webhook)
class WebhookView(generic.ObjectView):
queryset = Webhook.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.WebhookPanel(),
@@ -807,6 +815,7 @@ class EventRuleListView(generic.ObjectListView):
@register_model_view(EventRule)
class EventRuleView(generic.ObjectView):
queryset = EventRule.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.EventRulePanel(),
@@ -878,6 +887,7 @@ class TagListView(generic.ObjectListView):
@register_model_view(Tag)
class TagView(generic.ObjectView):
queryset = Tag.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.TagPanel(),
@@ -969,6 +979,7 @@ class ConfigContextProfileListView(generic.ObjectListView):
@register_model_view(ConfigContextProfile)
class ConfigContextProfileView(generic.ObjectView):
queryset = ConfigContextProfile.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.ConfigContextProfilePanel(),
@@ -1043,6 +1054,7 @@ class ConfigContextListView(generic.ObjectListView):
@register_model_view(ConfigContext)
class ConfigContextView(generic.ObjectView):
queryset = ConfigContext.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.ConfigContextPanel(),
@@ -1172,6 +1184,7 @@ class ConfigTemplateListView(generic.ObjectListView):
@register_model_view(ConfigTemplate)
class ConfigTemplateView(generic.ObjectView):
queryset = ConfigTemplate.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.ConfigTemplatePanel(),
@@ -1302,6 +1315,7 @@ class ImageAttachmentListView(generic.ObjectListView):
@register_model_view(ImageAttachment)
class ImageAttachmentView(generic.ObjectView):
queryset = ImageAttachment.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.ImageAttachmentPanel(),
@@ -1707,7 +1721,6 @@ class ScriptView(BaseScriptView):
user=request.user,
schedule_at=form.cleaned_data.pop('_schedule_at'),
interval=form.cleaned_data.pop('_interval'),
notifications=form.cleaned_data.pop('_notifications', '') or JobNotificationChoices.NOTIFICATION_ALWAYS,
data=form.cleaned_data,
request=copy_safe_request(request),
job_timeout=script.python_class.job_timeout,

View File

@@ -51,6 +51,7 @@ class VRFListView(generic.ObjectListView):
@register_model_view(VRF)
class VRFView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VRF.objects.all()
template_name = 'generic/object.html'
layout = layout.Layout(
layout.Row(
layout.Column(
@@ -172,6 +173,7 @@ class RouteTargetListView(generic.ObjectListView):
@register_model_view(RouteTarget)
class RouteTargetView(generic.ObjectView):
queryset = RouteTarget.objects.all()
template_name = 'generic/object.html'
layout = layout.Layout(
layout.Row(
layout.Column(
@@ -1332,6 +1334,7 @@ class VLANTranslationPolicyListView(generic.ObjectListView):
@register_model_view(VLANTranslationPolicy)
class VLANTranslationPolicyView(generic.ObjectView):
queryset = VLANTranslationPolicy.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.VLANTranslationPolicyPanel(),
@@ -1414,6 +1417,7 @@ class VLANTranslationRuleListView(generic.ObjectListView):
@register_model_view(VLANTranslationRule)
class VLANTranslationRuleView(generic.ObjectView):
queryset = VLANTranslationRule.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.VLANTranslationRulePanel(),
@@ -1753,6 +1757,7 @@ class ServiceTemplateListView(generic.ObjectListView):
@register_model_view(ServiceTemplate)
class ServiceTemplateView(generic.ObjectView):
queryset = ServiceTemplate.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.ServiceTemplatePanel(),

View File

@@ -142,7 +142,6 @@ class JobRunner(ABC):
user=job.user,
schedule_at=new_scheduled_time,
interval=job.interval,
notifications=job.notifications,
**kwargs,
)

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -1 +0,0 @@
{% extends 'generic/object.html' %}

View File

@@ -328,6 +328,7 @@ class ContactRoleListView(generic.ObjectListView):
@register_model_view(ContactRole)
class ContactRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ContactRole.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
OrganizationalObjectPanel(),

View File

@@ -208,6 +208,7 @@ class TunnelTerminationListView(generic.ObjectListView):
@register_model_view(TunnelTermination)
class TunnelTerminationView(generic.ObjectView):
queryset = TunnelTermination.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.TunnelTerminationPanel(),
@@ -278,6 +279,7 @@ class IKEProposalListView(generic.ObjectListView):
@register_model_view(IKEProposal)
class IKEProposalView(generic.ObjectView):
queryset = IKEProposal.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.IKEProposalPanel(),
@@ -351,6 +353,7 @@ class IKEPolicyListView(generic.ObjectListView):
@register_model_view(IKEPolicy)
class IKEPolicyView(GetRelatedModelsMixin, generic.ObjectView):
queryset = IKEPolicy.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.IKEPolicyPanel(),
@@ -430,6 +433,7 @@ class IPSecProposalListView(generic.ObjectListView):
@register_model_view(IPSecProposal)
class IPSecProposalView(generic.ObjectView):
queryset = IPSecProposal.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.IPSecProposalPanel(),
@@ -503,6 +507,7 @@ class IPSecPolicyListView(generic.ObjectListView):
@register_model_view(IPSecPolicy)
class IPSecPolicyView(GetRelatedModelsMixin, generic.ObjectView):
queryset = IPSecPolicy.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.IPSecPolicyPanel(),
@@ -582,6 +587,7 @@ class IPSecProfileListView(generic.ObjectListView):
@register_model_view(IPSecProfile)
class IPSecProfileView(generic.ObjectView):
queryset = IPSecProfile.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.IPSecProfilePanel(),
@@ -650,6 +656,7 @@ class L2VPNListView(generic.ObjectListView):
@register_model_view(L2VPN)
class L2VPNView(generic.ObjectView):
queryset = L2VPN.objects.all()
template_name = 'generic/object.html'
layout = layout.Layout(
layout.Row(
layout.Column(
@@ -764,6 +771,7 @@ class L2VPNTerminationListView(generic.ObjectListView):
@register_model_view(L2VPNTermination)
class L2VPNTerminationView(generic.ObjectView):
queryset = L2VPNTermination.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.L2VPNTerminationPanel(),

View File

@@ -143,6 +143,7 @@ class WirelessLANListView(generic.ObjectListView):
@register_model_view(WirelessLAN)
class WirelessLANView(generic.ObjectView):
queryset = WirelessLAN.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.WirelessLANPanel(),
@@ -218,6 +219,7 @@ class WirelessLinkListView(generic.ObjectListView):
@register_model_view(WirelessLink)
class WirelessLinkView(generic.ObjectView):
queryset = WirelessLink.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
left_panels=[
panels.WirelessLinkInterfacePanel('interface_a', title=_('Interface A')),