mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop-2.9' into docs-refresh
This commit is contained in:
8
.github/stale.yml
vendored
8
.github/stale.yml
vendored
@ -4,19 +4,19 @@
|
||||
only: issues
|
||||
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 14
|
||||
daysUntilStale: 45
|
||||
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
daysUntilClose: 15
|
||||
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- "status: accepted"
|
||||
- "status: gathering feedback"
|
||||
- "status: blocked"
|
||||
- "status: needs milestone"
|
||||
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
staleLabel: "pending closure"
|
||||
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
|
@ -99,6 +99,10 @@ help prevent wasting time on something that might we might not be able to
|
||||
implement. When suggesting a new feature, also make sure it won't conflict with
|
||||
any work that's already in progress.
|
||||
|
||||
* Once you've opened or identified an issue you'd like to work on, ask that it
|
||||
be assigned to you so that others are aware it's being worked on. A maintainer
|
||||
will then mark the issue as "accepted."
|
||||
|
||||
* Any pull request which does _not_ relate to an accepted issue will be closed.
|
||||
|
||||
* All major new functionality must include relevant tests where applicable.
|
||||
@ -132,18 +136,17 @@ accumulating a large backlog of work.
|
||||
The core maintainers group has chosen to make use of GitHub's [Stale bot](https://github.com/apps/stale)
|
||||
to aid in issue management.
|
||||
|
||||
* Issues will be marked as stale after 14 days of no activity.
|
||||
* Then after 7 more days of inactivity, the issue will be closed.
|
||||
* Issues will be marked as stale after 45 days of no activity.
|
||||
* Then after 15 more days of inactivity, the issue will be closed.
|
||||
* Any issue bearing one of the following labels will be exempt from all Stale
|
||||
bot actions:
|
||||
* `status: accepted`
|
||||
* `status: gathering feedback`
|
||||
* `status: blocked`
|
||||
* `status: needs milestone`
|
||||
|
||||
It is natural that some new issues get more attention than others. Often this
|
||||
is a metric of an issues's overall value to the project. In other cases in
|
||||
which issues merely get lost in the shuffle, notifications from Stale bot can
|
||||
bring renewed attention to potentially meaningful issues.
|
||||
It is natural that some new issues get more attention than others. Stale bot
|
||||
helps bring renewed attention to potentially valuable issues that may have been
|
||||
overlooked.
|
||||
|
||||
## Maintainer Guidance
|
||||
|
||||
|
@ -39,9 +39,11 @@ The `run()` method should accept two arguments:
|
||||
* `commit` - A boolean indicating whether database changes will be committed.
|
||||
|
||||
!!! note
|
||||
The `commit` argument was introduced in NetBox v2.7.8. Backward compatibility is maintained for scripts which accept only the `data` argument, however moving forward scripts should accept both arguments. This backward compatibility will be removed in v2.10.
|
||||
The `commit` argument was introduced in NetBox v2.7.8. Backward compatibility is maintained for scripts which accept only the `data` argument, however beginning with v2.10 NetBox will require the `run()` method of every script to accept both arguments. (Either argument may still be ignored within the method.)
|
||||
|
||||
Returning output from your script is optional. Any raw output generated by the script will be displayed under the "output" tab in the UI.
|
||||
Defining script variables is optional: You may create a script with only a `run()` method if no user input is needed.
|
||||
|
||||
Any output generated by the script during its execution will be displayed under the "output" tab in the UI.
|
||||
|
||||
## Module Attributes
|
||||
|
||||
|
@ -30,6 +30,12 @@ Copy the 'configuration.py' you created when first installing to the new version
|
||||
# cp netbox-X.Y.Z/netbox/netbox/configuration.py netbox/netbox/netbox/configuration.py
|
||||
```
|
||||
|
||||
Copy your local requirements file if used:
|
||||
|
||||
```no-highlight
|
||||
# cp netbox-X.Y.Z/local_requirements.txt netbox/local_requirements.txt
|
||||
```
|
||||
|
||||
Also copy the LDAP configuration if using LDAP:
|
||||
|
||||
```no-highlight
|
||||
|
@ -110,6 +110,8 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
||||
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
|
||||
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
|
||||
|
||||
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
|
||||
|
||||
### Install the Plugin for Development
|
||||
|
||||
To ease development, it is recommended to go ahead and install the plugin at this point using setuptools' `develop` mode. This will create symbolic links within your Python environment to the plugin development directory. Call `setup.py` from the plugin's root directory with the `develop` argument (instead of `install`):
|
||||
|
@ -1,13 +1,25 @@
|
||||
# NetBox v2.8
|
||||
|
||||
## v2.8.9 (FUTURE)
|
||||
## v2.8.9 (2020-08-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#4898](https://github.com/netbox-community/netbox/issues/4898) - Add MAC address search field to interfaces list
|
||||
* [#4899](https://github.com/netbox-community/netbox/issues/4899) - Add MAC address column to interfaces table
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#4455](https://github.com/netbox-community/netbox/issues/4455) - Fix ordering of prefixes beneath aggregate when available space is hidden
|
||||
* [#4875](https://github.com/netbox-community/netbox/issues/4875) - Fix documentation for image attachments
|
||||
* [#4876](https://github.com/netbox-community/netbox/issues/4876) - Fix labels for sites in staging or decommissioning status
|
||||
* [#4880](https://github.com/netbox-community/netbox/issues/4880) - Fix remove tagged vlans if not assigned in bulk interface editting
|
||||
* [#4880](https://github.com/netbox-community/netbox/issues/4880) - Fix removal of tagged VLANs if not assigned in bulk interface editing
|
||||
* [#4887](https://github.com/netbox-community/netbox/issues/4887) - Don't disable NAPALM tabs when device has no primary IP
|
||||
* [#4894](https://github.com/netbox-community/netbox/issues/4894) - Fix display of device/VM counts on platforms list
|
||||
* [#4895](https://github.com/netbox-community/netbox/issues/4895) - Force UTF-8 encoding when embedding model documentation
|
||||
* [#4910](https://github.com/netbox-community/netbox/issues/4910) - Unpin redis dependency to fix exception in RQ worker
|
||||
* [#4926](https://github.com/netbox-community/netbox/issues/4926) - Fix ordering of VM interfaces in REST API endpoint
|
||||
* [#4927](https://github.com/netbox-community/netbox/issues/4927) - Fix validation error when updating an existing secret
|
||||
* [#4929](https://github.com/netbox-community/netbox/issues/4929) - Correct log message when creating a new object
|
||||
|
||||
---
|
||||
|
||||
|
@ -1,11 +1,31 @@
|
||||
# NetBox v2.9
|
||||
|
||||
## v2.9.0 (FUTURE)
|
||||
## v2.9-beta2 (FUTURE)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#4919](https://github.com/netbox-community/netbox/issues/4919) - Allow adding/changing assigned permissions within group and user admin views
|
||||
* [#4940](https://github.com/netbox-community/netbox/issues/4940) - Add an `occupied` field to rack unit representations for rack elevation views
|
||||
* [#4945](https://github.com/netbox-community/netbox/issues/4945) - Add a user-friendly 403 error page
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#4905](https://github.com/netbox-community/netbox/issues/4905) - Fix front port count on device type view
|
||||
* [#4912](https://github.com/netbox-community/netbox/issues/4912) - Fix image attachment API endpoint
|
||||
* [#4914](https://github.com/netbox-community/netbox/issues/4914) - Fix toggling cable status under device view
|
||||
* [#4921](https://github.com/netbox-community/netbox/issues/4921) - Render non-viewable devices as unavailable space in rack elevations
|
||||
* [#4930](https://github.com/netbox-community/netbox/issues/4930) - Replicate label values when instantiating device type components
|
||||
* [#4931](https://github.com/netbox-community/netbox/issues/4931) - Fix DoesNotExist exception when deleting devices
|
||||
* [#4938](https://github.com/netbox-community/netbox/issues/4938) - Show add, import buttons on virtual chassis list view
|
||||
* [#4939](https://github.com/netbox-community/netbox/issues/4939) - Fix linking to LAG interfaces on other VC members
|
||||
* [#4950](https://github.com/netbox-community/netbox/issues/4950) - Include inventory item label in API serializer, UI view
|
||||
* [#4952](https://github.com/netbox-community/netbox/issues/4952) - Default to VM tab when creating/editing an IP address for a VM
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#4940](https://github.com/netbox-community/netbox/issues/4940) - Add an `occupied` field to rack unit representations for rack elevation views
|
||||
* [#4942](https://github.com/netbox-community/netbox/issues/4942) - Make ObjectPermission's `name` field required
|
||||
* [#4943](https://github.com/netbox-community/netbox/issues/4943) - Add a `description` field to ObjectPermission
|
||||
|
||||
---
|
||||
|
||||
@ -76,6 +96,7 @@ When running a report or custom script, its execution is now queued for backgrou
|
||||
* dcim.PowerPortTemplate: Added `description` and `label` fields
|
||||
* dcim.PowerOutlet: Added `label` field
|
||||
* dcim.PowerOutletTemplate: Added `description` and `label` fields
|
||||
* dcim.Rack: Added an `occupied` field to rack unit representations for rack elevation views
|
||||
* dcim.RackGroup: Added a `_depth` attribute indicating an object's position in the tree.
|
||||
* dcim.RackReservation: Added `tags` field
|
||||
* dcim.RearPort: Added `label` field
|
||||
|
@ -267,6 +267,10 @@ GET /api/ipam/prefixes/13980/?brief=1
|
||||
|
||||
The brief format is supported for both lists and individual objects.
|
||||
|
||||
### Excluding Config Contexts
|
||||
|
||||
When retrieving devices and virtual machines via the REST API, each will included its rendered [configuration context data](../models/extras/configcontext/) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views.
|
||||
|
||||
## Pagination
|
||||
|
||||
API responses which contain a list of many objects will be paginated for efficiency. The root JSON object returned by a list endpoint contains the following attributes:
|
||||
|
@ -165,6 +165,7 @@ class RackUnitSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(read_only=True)
|
||||
face = ChoiceField(choices=DeviceFaceChoices, read_only=True)
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
occupied = serializers.BooleanField(read_only=True)
|
||||
|
||||
|
||||
class RackReservationSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
@ -639,8 +640,8 @@ class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'id', 'url', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||
'description', 'tags',
|
||||
'id', 'url', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
'discovered', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
@ -176,6 +176,7 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
# Render and return the elevation as an SVG drawing with the correct content type
|
||||
drawing = rack.get_elevation_svg(
|
||||
face=data['face'],
|
||||
user=request.user,
|
||||
unit_width=data['unit_width'],
|
||||
unit_height=data['unit_height'],
|
||||
legend_width=data['legend_width'],
|
||||
@ -188,6 +189,7 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
# Return a JSON representation of the rack units in the elevation
|
||||
elevation = rack.get_rack_units(
|
||||
face=data['face'],
|
||||
user=request.user,
|
||||
exclude=data['exclude'],
|
||||
expand_devices=data['expand_devices']
|
||||
)
|
||||
|
@ -14,10 +14,11 @@ class RackElevationSVG:
|
||||
Use this class to render a rack elevation as an SVG image.
|
||||
|
||||
:param rack: A NetBox Rack instance
|
||||
:param user: User instance. If specified, only devices viewable by this user will be fully displayed.
|
||||
:param include_images: If true, the SVG document will embed front/rear device face images, where available
|
||||
:param base_url: Base URL for links within the SVG document. If none, links will be relative.
|
||||
"""
|
||||
def __init__(self, rack, include_images=True, base_url=None):
|
||||
def __init__(self, rack, user=None, include_images=True, base_url=None):
|
||||
self.rack = rack
|
||||
self.include_images = include_images
|
||||
if base_url is not None:
|
||||
@ -25,7 +26,14 @@ class RackElevationSVG:
|
||||
else:
|
||||
self.base_url = ''
|
||||
|
||||
def _get_device_description(self, device):
|
||||
# Determine the subset of devices within this rack that are viewable by the user, if any
|
||||
permitted_devices = self.rack.devices
|
||||
if user is not None:
|
||||
permitted_devices = permitted_devices.restrict(user, 'view')
|
||||
self.permitted_device_ids = permitted_devices.values_list('pk', flat=True)
|
||||
|
||||
@staticmethod
|
||||
def _get_device_description(device):
|
||||
return '{} ({}) — {} ({}U) {} {}'.format(
|
||||
device.name,
|
||||
device.device_role,
|
||||
@ -174,10 +182,13 @@ class RackElevationSVG:
|
||||
text_cordinates = (x_offset + (unit_width / 2), y_offset + end_y / 2)
|
||||
|
||||
# Draw the device
|
||||
if device and device.face == face:
|
||||
if device and device.face == face and device.pk in self.permitted_device_ids:
|
||||
self._draw_device_front(drawing, device, start_cordinates, end_cordinates, text_cordinates)
|
||||
elif device and device.device_type.is_full_depth:
|
||||
elif device and device.device_type.is_full_depth and device.pk in self.permitted_device_ids:
|
||||
self._draw_device_rear(drawing, device, start_cordinates, end_cordinates, text_cordinates)
|
||||
elif device:
|
||||
# Devices which the user does not have permission to view are rendered only as unavailable space
|
||||
drawing.add(drawing.rect(start_cordinates, end_cordinates, class_='blocked'))
|
||||
else:
|
||||
# Draw shallow devices, reservations, or empty units
|
||||
class_ = 'slot'
|
||||
|
@ -2703,6 +2703,10 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label='MAC address'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
|
@ -656,12 +656,14 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
def get_status_class(self):
|
||||
return self.STATUS_CLASS_MAP.get(self.status)
|
||||
|
||||
def get_rack_units(self, face=DeviceFaceChoices.FACE_FRONT, exclude=None, expand_devices=True):
|
||||
def get_rack_units(self, user=None, face=DeviceFaceChoices.FACE_FRONT, exclude=None, expand_devices=True):
|
||||
"""
|
||||
Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'}
|
||||
Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy.
|
||||
|
||||
:param face: Rack face (front or rear)
|
||||
:param user: User instance to be used for evaluating device view permissions. If None, all devices
|
||||
will be included.
|
||||
:param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack
|
||||
:param expand_devices: When True, all units that a device occupies will be listed with each containing a
|
||||
reference to the device. When False, only the bottom most unit for a device is included and that unit
|
||||
@ -670,10 +672,18 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
elevation = OrderedDict()
|
||||
for u in self.units:
|
||||
elevation[u] = {'id': u, 'name': 'U{}'.format(u), 'face': face, 'device': None}
|
||||
elevation[u] = {
|
||||
'id': u,
|
||||
'name': f'U{u}',
|
||||
'face': face,
|
||||
'device': None,
|
||||
'occupied': False
|
||||
}
|
||||
|
||||
# Add devices to rack units list
|
||||
if self.pk:
|
||||
|
||||
# Retrieve all devices installed within the rack
|
||||
queryset = Device.objects.prefetch_related(
|
||||
'device_type',
|
||||
'device_type__manufacturer',
|
||||
@ -689,12 +699,22 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
).filter(
|
||||
Q(face=face) | Q(device_type__is_full_depth=True)
|
||||
)
|
||||
|
||||
# Determine which devices the user has permission to view
|
||||
permitted_device_ids = []
|
||||
if user is not None:
|
||||
permitted_device_ids = self.devices.restrict(user, 'view').values_list('pk', flat=True)
|
||||
|
||||
for device in queryset:
|
||||
if expand_devices:
|
||||
for u in range(device.position, device.position + device.device_type.u_height):
|
||||
elevation[u]['device'] = device
|
||||
if user is None or device.pk in permitted_device_ids:
|
||||
elevation[u]['device'] = device
|
||||
elevation[u]['occupied'] = True
|
||||
else:
|
||||
elevation[device.position]['device'] = device
|
||||
if user is None or device.pk in permitted_device_ids:
|
||||
elevation[device.position]['device'] = device
|
||||
elevation[device.position]['occupied'] = True
|
||||
elevation[device.position]['height'] = device.device_type.u_height
|
||||
for u in range(device.position + 1, device.position + device.device_type.u_height):
|
||||
elevation.pop(u, None)
|
||||
@ -750,6 +770,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
def get_elevation_svg(
|
||||
self,
|
||||
face=DeviceFaceChoices.FACE_FRONT,
|
||||
user=None,
|
||||
unit_width=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH,
|
||||
unit_height=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT,
|
||||
legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT,
|
||||
@ -760,6 +781,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
Return an SVG of the rack elevation
|
||||
|
||||
:param face: Enum of [front, rear] representing the desired side of the rack elevation to render
|
||||
:param user: User instance to be used for evaluating device view permissions. If None, all devices
|
||||
will be included.
|
||||
:param unit_width: Width in pixels for the rendered drawing
|
||||
:param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total
|
||||
height of the elevation
|
||||
@ -767,7 +790,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
:param include_images: Embed front/rear device images where available
|
||||
:param base_url: Base URL for links and images. If none, URLs will be relative.
|
||||
"""
|
||||
elevation = RackElevationSVG(self, include_images=include_images, base_url=base_url)
|
||||
elevation = RackElevationSVG(self, user=user, include_images=include_images, base_url=base_url)
|
||||
|
||||
return elevation.render(face, unit_width, unit_height, legend_width)
|
||||
|
||||
|
@ -100,6 +100,7 @@ class ConsolePortTemplate(ComponentTemplateModel):
|
||||
return ConsolePort(
|
||||
device=device,
|
||||
name=self.name,
|
||||
label=self.label,
|
||||
type=self.type
|
||||
)
|
||||
|
||||
@ -122,6 +123,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
|
||||
return ConsoleServerPort(
|
||||
device=device,
|
||||
name=self.name,
|
||||
label=self.label,
|
||||
type=self.type
|
||||
)
|
||||
|
||||
@ -156,6 +158,7 @@ class PowerPortTemplate(ComponentTemplateModel):
|
||||
return PowerPort(
|
||||
device=device,
|
||||
name=self.name,
|
||||
label=self.label,
|
||||
type=self.type,
|
||||
maximum_draw=self.maximum_draw,
|
||||
allocated_draw=self.allocated_draw
|
||||
@ -205,6 +208,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
||||
return PowerOutlet(
|
||||
device=device,
|
||||
name=self.name,
|
||||
label=self.label,
|
||||
type=self.type,
|
||||
power_port=power_port,
|
||||
feed_leg=self.feed_leg
|
||||
@ -239,6 +243,7 @@ class InterfaceTemplate(ComponentTemplateModel):
|
||||
return Interface(
|
||||
device=device,
|
||||
name=self.name,
|
||||
label=self.label,
|
||||
type=self.type,
|
||||
mgmt_only=self.mgmt_only
|
||||
)
|
||||
@ -293,6 +298,7 @@ class FrontPortTemplate(ComponentTemplateModel):
|
||||
return FrontPort(
|
||||
device=device,
|
||||
name=self.name,
|
||||
label=self.label,
|
||||
type=self.type,
|
||||
rear_port=rear_port,
|
||||
rear_port_position=self.rear_port_position
|
||||
@ -320,6 +326,7 @@ class RearPortTemplate(ComponentTemplateModel):
|
||||
return RearPort(
|
||||
device=device,
|
||||
name=self.name,
|
||||
label=self.label,
|
||||
type=self.type,
|
||||
positions=self.positions
|
||||
)
|
||||
@ -336,5 +343,6 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
||||
def instantiate(self, device):
|
||||
return DeviceBay(
|
||||
device=device,
|
||||
name=self.name
|
||||
name=self.name,
|
||||
label=self.label
|
||||
)
|
||||
|
@ -71,11 +71,16 @@ class ComponentModel(models.Model):
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the parent Device
|
||||
try:
|
||||
device = self.device
|
||||
except ObjectDoesNotExist:
|
||||
# The parent Device has already been deleted
|
||||
device = None
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=self.device,
|
||||
related_object=device,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
|
||||
|
@ -56,10 +56,49 @@ DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value|default:0 }}</a>
|
||||
"""
|
||||
|
||||
VM_COUNT = """
|
||||
RACKRESERVATION_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackreservation_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
MANUFACTURER_ACTIONS = """
|
||||
<a href="{% url 'dcim:manufacturer_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_manufacturer %}
|
||||
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICEROLE_DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value|default:0 }}</a>
|
||||
"""
|
||||
|
||||
DEVICEROLE_VM_COUNT = """
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?role={{ record.slug }}">{{ value|default:0 }}</a>
|
||||
"""
|
||||
|
||||
DEVICEROLE_ACTIONS = """
|
||||
<a href="{% url 'dcim:devicerole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_devicerole %}
|
||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
PLATFORM_DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?platform={{ record.slug }}">{{ value|default:0 }}</a>
|
||||
"""
|
||||
|
||||
PLATFORM_VM_COUNT = """
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ record.slug }}">{{ value|default:0 }}</a>
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||
"""
|
||||
@ -495,11 +534,11 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||
class DeviceRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
device_count = tables.TemplateColumn(
|
||||
template_code=DEVICE_COUNT,
|
||||
template_code=DEVICEROLE_DEVICE_COUNT,
|
||||
verbose_name='Devices'
|
||||
)
|
||||
vm_count = tables.TemplateColumn(
|
||||
template_code=VM_COUNT,
|
||||
template_code=DEVICEROLE_VM_COUNT,
|
||||
verbose_name='VMs'
|
||||
)
|
||||
color = tables.TemplateColumn(
|
||||
@ -522,11 +561,11 @@ class DeviceRoleTable(BaseTable):
|
||||
class PlatformTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
device_count = tables.TemplateColumn(
|
||||
template_code=DEVICE_COUNT,
|
||||
template_code=PLATFORM_DEVICE_COUNT,
|
||||
verbose_name='Devices'
|
||||
)
|
||||
vm_count = tables.TemplateColumn(
|
||||
template_code=VM_COUNT,
|
||||
template_code=PLATFORM_VM_COUNT,
|
||||
verbose_name='VMs'
|
||||
)
|
||||
actions = ButtonsColumn(Platform, pk_field='slug')
|
||||
@ -718,8 +757,8 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'description', 'cable',
|
||||
'ip_addresses', 'untagged_vlan', 'tagged_vlans',
|
||||
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'description', 'cable', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
|
@ -953,8 +953,8 @@ class DeviceRoleBulkDeleteView(BulkDeleteView):
|
||||
|
||||
class PlatformListView(ObjectListView):
|
||||
queryset = Platform.objects.annotate(
|
||||
device_count=get_subquery(Device, 'device_role'),
|
||||
vm_count=get_subquery(VirtualMachine, 'role')
|
||||
device_count=get_subquery(Device, 'platform'),
|
||||
vm_count=get_subquery(VirtualMachine, 'platform')
|
||||
)
|
||||
table = tables.PlatformTable
|
||||
|
||||
@ -2185,7 +2185,6 @@ class VirtualChassisListView(ObjectListView):
|
||||
table = tables.VirtualChassisTable
|
||||
filterset = filters.VirtualChassisFilterSet
|
||||
filterset_form = forms.VirtualChassisFilterForm
|
||||
action_buttons = ('export',)
|
||||
|
||||
|
||||
class VirtualChassisView(ObjectView):
|
||||
|
@ -3,7 +3,6 @@ from django.contrib import admin
|
||||
|
||||
from utilities.forms import LaxURLField
|
||||
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, JobResult, Webhook
|
||||
from .reports import get_report
|
||||
|
||||
|
||||
def order_content_types(field):
|
||||
@ -160,6 +159,10 @@ class GraphForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Graph
|
||||
exclude = ()
|
||||
help_texts = {
|
||||
'template_language': "<a href=\"https://jinja.palletsprojects.com\">Jinja2</a> is strongly recommended for "
|
||||
"new graphs."
|
||||
}
|
||||
widgets = {
|
||||
'source': forms.Textarea,
|
||||
'link': forms.Textarea,
|
||||
@ -195,6 +198,11 @@ class ExportTemplateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
exclude = []
|
||||
help_texts = {
|
||||
'template_language': "<strong>Warning:</strong> Support for Django templating will be dropped in NetBox "
|
||||
"v2.10. <a href=\"https://jinja.palletsprojects.com\">Jinja2</a> is strongly "
|
||||
"recommended."
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -85,12 +85,12 @@ class ObjectChangeActionChoices(ChoiceSet):
|
||||
|
||||
class TemplateLanguageChoices(ChoiceSet):
|
||||
|
||||
LANGUAGE_DJANGO = 'django'
|
||||
LANGUAGE_JINJA2 = 'jinja2'
|
||||
LANGUAGE_DJANGO = 'django'
|
||||
|
||||
CHOICES = (
|
||||
(LANGUAGE_DJANGO, 'Django'),
|
||||
(LANGUAGE_JINJA2, 'Jinja2'),
|
||||
(LANGUAGE_DJANGO, 'Django (Legacy)'),
|
||||
)
|
||||
|
||||
|
||||
|
@ -4,6 +4,7 @@ import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import traceback
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
|
||||
import yaml
|
||||
@ -405,12 +406,16 @@ def run_script(data, request, commit=True, *args, **kwargs):
|
||||
# Add the current request as a property of the script
|
||||
script.request = request
|
||||
|
||||
# TODO: Drop backward-compatibility for absent 'commit' argument in v2.10
|
||||
# Determine whether the script accepts a 'commit' argument (this was introduced in v2.7.8)
|
||||
kwargs = {
|
||||
'data': data
|
||||
}
|
||||
if 'commit' in inspect.signature(script.run).parameters:
|
||||
kwargs['commit'] = commit
|
||||
else:
|
||||
warnings.warn(f"The run() method of script {script} should support a 'commit' argument. This will be required "
|
||||
f"beginning with NetBox v2.10.")
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
|
@ -219,6 +219,8 @@ class AggregateView(ObjectView):
|
||||
prefix__net_contained_or_equal=str(aggregate.prefix)
|
||||
).prefetch_related(
|
||||
'site', 'role'
|
||||
).order_by(
|
||||
'prefix'
|
||||
).annotate_depth(
|
||||
limit=0
|
||||
)
|
||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '2.9-beta1'
|
||||
VERSION = '2.9-beta2'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
|
@ -121,7 +121,7 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm):
|
||||
device=self.cleaned_data['device'],
|
||||
role=self.cleaned_data['role'],
|
||||
name=self.cleaned_data['name']
|
||||
).exists():
|
||||
).exclude(pk=self.instance.pk).exists():
|
||||
raise forms.ValidationError(
|
||||
"Each secret assigned to a device must have a unique combination of role and name"
|
||||
)
|
||||
|
9
netbox/templates/403.html
Normal file
9
netbox/templates/403.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends '40x.html' %}
|
||||
|
||||
{% block title %}Access Denied{% endblock %}
|
||||
|
||||
{% block icon %}<i class="glyphicon glyphicon-lock"></i>{% endblock %}
|
||||
|
||||
{% block message %}
|
||||
You do not have permission to access this page.
|
||||
{% endblock %}
|
@ -1,19 +1,9 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends '40x.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row" style="margin-top: 150px;">
|
||||
<div class="col-sm-4 col-sm-offset-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong><i class="glyphicon glyphicon-warning-sign"></i> Page Not Found</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
The requested page does not exist.
|
||||
</div>
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'home' %}" class="btn btn-xs btn-primary">Home Page</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% block title %}Page Not Found{% endblock %}
|
||||
|
||||
{% block icon %}<i class="glyphicon glyphicon-warning-sign"></i>{% endblock %}
|
||||
|
||||
{% block message %}
|
||||
The requested page does not exist.
|
||||
{% endblock %}
|
||||
|
19
netbox/templates/40x.html
Normal file
19
netbox/templates/40x.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row" style="margin-top: 150px;">
|
||||
<div class="col-sm-4 col-sm-offset-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>{% block icon %}{% endblock %} {% block title %}{% endblock %}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% block message %}{% endblock %}
|
||||
</div>
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'home' %}" class="btn btn-xs btn-primary">Home Page</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -49,11 +49,13 @@
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ cable.get_type_display }}</td>
|
||||
<td>{{ cable.get_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>{{ cable.get_status_display }}</td>
|
||||
<td>
|
||||
<span class="label label-{{ cable.get_status_class }}">{{ cable.get_status_display }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
|
@ -975,7 +975,7 @@ function toggleConnection(elem) {
|
||||
xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token }}");
|
||||
},
|
||||
data: {
|
||||
'status': 'False'
|
||||
'status': 'planned'
|
||||
},
|
||||
context: this,
|
||||
success: function() {
|
||||
@ -994,7 +994,7 @@ function toggleConnection(elem) {
|
||||
xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token }}");
|
||||
},
|
||||
data: {
|
||||
'status': 'True'
|
||||
'status': 'connected'
|
||||
},
|
||||
context: this,
|
||||
success: function() {
|
||||
|
@ -22,7 +22,7 @@
|
||||
{# LAG #}
|
||||
<td>
|
||||
{% if iface.lag %}
|
||||
<a href="#interface_{{ iface.lag }}" class="label label-primary" title="{{ iface.lag.description }}">{{ iface.lag }}</a>
|
||||
<a href="{{ iface.lag.device.get_absolute_url }}#interface_{{ iface.lag }}" class="label label-primary" title="{{ iface.lag.description }}">{{ iface.lag }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
|
@ -30,6 +30,10 @@
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manufacturer</td>
|
||||
<td>
|
||||
|
@ -33,7 +33,7 @@
|
||||
<strong>Interface Assignment</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% with vm_tab_active=obj.vminterface.exists %}
|
||||
{% with vm_tab_active=form.initial.vminterface %}
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation"{% if not vm_tab_active %} class="active"{% endif %}><a href="#device" role="tab" data-toggle="tab">Device</a></li>
|
||||
<li role="presentation"{% if vm_tab_active %} class="active"{% endif %}><a href="#virtualmachine" role="tab" data-toggle="tab">Virtual Machine</a></li>
|
||||
|
@ -9,6 +9,49 @@ from extras.admin import order_content_types
|
||||
from .models import AdminGroup, AdminUser, ObjectPermission, Token, UserConfig
|
||||
|
||||
|
||||
#
|
||||
# Inline models
|
||||
#
|
||||
|
||||
class ObjectPermissionInline(admin.TabularInline):
|
||||
exclude = None
|
||||
extra = 3
|
||||
readonly_fields = ['object_types', 'actions', 'constraints']
|
||||
verbose_name = 'Permission'
|
||||
verbose_name_plural = 'Permissions'
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).prefetch_related('objectpermission__object_types')
|
||||
|
||||
@staticmethod
|
||||
def object_types(instance):
|
||||
# Don't call .values_list() here because we want to reference the pre-fetched object_types
|
||||
return ', '.join([ot.name for ot in instance.objectpermission.object_types.all()])
|
||||
|
||||
@staticmethod
|
||||
def actions(instance):
|
||||
return ', '.join(instance.objectpermission.actions)
|
||||
|
||||
@staticmethod
|
||||
def constraints(instance):
|
||||
return instance.objectpermission.constraints
|
||||
|
||||
|
||||
class GroupObjectPermissionInline(ObjectPermissionInline):
|
||||
model = AdminGroup.object_permissions.through
|
||||
|
||||
|
||||
class UserObjectPermissionInline(ObjectPermissionInline):
|
||||
model = AdminUser.object_permissions.through
|
||||
|
||||
|
||||
class UserConfigInline(admin.TabularInline):
|
||||
model = UserConfig
|
||||
readonly_fields = ('data',)
|
||||
can_delete = False
|
||||
verbose_name = 'Preferences'
|
||||
|
||||
|
||||
#
|
||||
# Users & groups
|
||||
#
|
||||
@ -24,40 +67,13 @@ class GroupAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'user_count')
|
||||
ordering = ('name',)
|
||||
search_fields = ('name',)
|
||||
inlines = [GroupObjectPermissionInline]
|
||||
|
||||
def user_count(self, obj):
|
||||
@staticmethod
|
||||
def user_count(obj):
|
||||
return obj.user_set.count()
|
||||
|
||||
|
||||
class UserConfigInline(admin.TabularInline):
|
||||
model = UserConfig
|
||||
readonly_fields = ('data',)
|
||||
can_delete = False
|
||||
verbose_name = 'Preferences'
|
||||
|
||||
|
||||
class ObjectPermissionInline(admin.TabularInline):
|
||||
model = AdminUser.object_permissions.through
|
||||
fields = ['object_types', 'actions', 'constraints']
|
||||
readonly_fields = fields
|
||||
extra = 0
|
||||
verbose_name = 'Permission'
|
||||
verbose_name_plural = 'Permissions'
|
||||
|
||||
def object_types(self, instance):
|
||||
return ', '.join(instance.objectpermission.object_types.values_list('model', flat=True))
|
||||
|
||||
def actions(self, instance):
|
||||
return ', '.join(instance.objectpermission.actions)
|
||||
|
||||
def constraints(self, instance):
|
||||
return instance.objectpermission.constraints
|
||||
|
||||
def has_add_permission(self, request, obj):
|
||||
# Don't allow the creation of new ObjectPermission assignments via this form
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(AdminUser)
|
||||
class UserAdmin(UserAdmin_):
|
||||
list_display = [
|
||||
@ -71,9 +87,13 @@ class UserAdmin(UserAdmin_):
|
||||
}),
|
||||
('Important dates', {'fields': ('last_login', 'date_joined')}),
|
||||
)
|
||||
inlines = [ObjectPermissionInline, UserConfigInline]
|
||||
filter_horizontal = ('groups',)
|
||||
|
||||
def get_inlines(self, request, obj):
|
||||
if obj is not None:
|
||||
return (UserObjectPermissionInline, UserConfigInline)
|
||||
return ()
|
||||
|
||||
|
||||
#
|
||||
# REST API tokens
|
||||
@ -212,7 +232,7 @@ class ObjectPermissionAdmin(admin.ModelAdmin):
|
||||
actions = ('enable', 'disable')
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('name', 'enabled')
|
||||
'fields': ('name', 'description', 'enabled')
|
||||
}),
|
||||
('Actions', {
|
||||
'fields': (('can_view', 'can_add', 'can_change', 'can_delete'), 'actions')
|
||||
@ -231,19 +251,16 @@ class ObjectPermissionAdmin(admin.ModelAdmin):
|
||||
filter_horizontal = ('object_types', 'groups', 'users')
|
||||
form = ObjectPermissionForm
|
||||
list_display = [
|
||||
'get_name', 'enabled', 'list_models', 'list_users', 'list_groups', 'actions', 'constraints',
|
||||
'name', 'enabled', 'list_models', 'list_users', 'list_groups', 'actions', 'constraints', 'description',
|
||||
]
|
||||
list_filter = [
|
||||
'enabled', ActionListFilter, ObjectTypeListFilter, 'groups', 'users'
|
||||
]
|
||||
search_fields = ['actions', 'constraints', 'description', 'name']
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).prefetch_related('object_types', 'users', 'groups')
|
||||
|
||||
def get_name(self, obj):
|
||||
return obj.name or f'Permission #{obj.pk}'
|
||||
get_name.short_description = 'Name'
|
||||
|
||||
def list_models(self, obj):
|
||||
return ', '.join([f"{ct}" for ct in obj.object_types.all()])
|
||||
list_models.short_description = 'Models'
|
||||
|
@ -54,4 +54,6 @@ class ObjectPermissionSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ObjectPermission
|
||||
fields = ('id', 'url', 'name', 'enabled', 'object_types', 'groups', 'users', 'actions', 'constraints')
|
||||
fields = (
|
||||
'id', 'url', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions', 'constraints',
|
||||
)
|
||||
|
@ -18,7 +18,8 @@ class Migration(migrations.Migration):
|
||||
name='ObjectPermission',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(blank=True, max_length=100)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
('constraints', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)),
|
||||
('actions', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), size=None)),
|
||||
@ -27,6 +28,7 @@ class Migration(migrations.Migration):
|
||||
('users', models.ManyToManyField(blank=True, related_name='object_permissions', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
'verbose_name': 'permission',
|
||||
},
|
||||
),
|
||||
|
@ -13,7 +13,7 @@ def replicate_permissions(apps, schema_editor):
|
||||
|
||||
# TODO: Optimize this iteration so that ObjectPermissions with identical sets of users and groups
|
||||
# are combined into a single ObjectPermission instance.
|
||||
for perm in Permission.objects.all():
|
||||
for perm in Permission.objects.select_related('content_type'):
|
||||
if perm.codename.split('_')[0] in ACTIONS:
|
||||
action = perm.codename.split('_')[0]
|
||||
elif perm.codename == 'activate_userkey':
|
||||
@ -24,7 +24,11 @@ def replicate_permissions(apps, schema_editor):
|
||||
action = perm.codename
|
||||
|
||||
if perm.group_set.exists() or perm.user_set.exists():
|
||||
obj_perm = ObjectPermission(actions=[action])
|
||||
obj_perm = ObjectPermission(
|
||||
# Copy name from original Permission object
|
||||
name=f'{perm.content_type.app_label}.{perm.codename}'[:100],
|
||||
actions=[action]
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.object_types.add(perm.content_type)
|
||||
if perm.group_set.exists():
|
||||
|
@ -16,6 +16,8 @@ from utilities.utils import flatten_dict
|
||||
|
||||
|
||||
__all__ = (
|
||||
'AdminGroup',
|
||||
'AdminUser',
|
||||
'ObjectPermission',
|
||||
'Token',
|
||||
'UserConfig',
|
||||
@ -237,7 +239,10 @@ class ObjectPermission(models.Model):
|
||||
identified by ORM query parameters.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
max_length=100
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
@ -275,12 +280,8 @@ class ObjectPermission(models.Model):
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = "permission"
|
||||
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
return '{}: {}'.format(
|
||||
', '.join(self.object_types.values_list('model', flat=True)),
|
||||
', '.join(self.actions)
|
||||
)
|
||||
return self.name
|
||||
|
@ -97,6 +97,7 @@ class ObjectPermissionTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
for i in range(0, 3):
|
||||
objectpermission = ObjectPermission(
|
||||
name=f'Permission {i+1}',
|
||||
actions=['view', 'add', 'change', 'delete'],
|
||||
constraints={'name': f'TEST{i+1}'}
|
||||
)
|
||||
@ -107,6 +108,7 @@ class ObjectPermissionTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Permission 4',
|
||||
'object_types': ['dcim.site'],
|
||||
'groups': [groups[0].pk],
|
||||
'users': [users[0].pk],
|
||||
@ -114,6 +116,7 @@ class ObjectPermissionTest(APIViewTestCases.APIViewTestCase):
|
||||
'constraints': {'name': 'TEST4'},
|
||||
},
|
||||
{
|
||||
'name': 'Permission 5',
|
||||
'object_types': ['dcim.site'],
|
||||
'groups': [groups[1].pk],
|
||||
'users': [users[1].pk],
|
||||
@ -121,6 +124,7 @@ class ObjectPermissionTest(APIViewTestCases.APIViewTestCase):
|
||||
'constraints': {'name': 'TEST5'},
|
||||
},
|
||||
{
|
||||
'name': 'Permission 6',
|
||||
'object_types': ['dcim.site'],
|
||||
'groups': [groups[2].pk],
|
||||
'users': [users[2].pk],
|
||||
|
@ -178,7 +178,7 @@ def get_docs(model):
|
||||
model._meta.model_name
|
||||
)
|
||||
try:
|
||||
with open(path) as docfile:
|
||||
with open(path, encoding='utf-8') as docfile:
|
||||
content = docfile.read()
|
||||
except FileNotFoundError:
|
||||
return "Unable to load documentation, file not found: {}".format(path)
|
||||
|
@ -418,13 +418,14 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
object_created = form.instance.pk is None
|
||||
obj = form.save()
|
||||
|
||||
# Check that the new object conforms with any assigned object-level permissions
|
||||
self.queryset.get(pk=obj.pk)
|
||||
|
||||
msg = '{} {}'.format(
|
||||
'Created' if not form.instance.pk else 'Modified',
|
||||
'Created' if object_created else 'Modified',
|
||||
self.queryset.model._meta.verbose_name
|
||||
)
|
||||
logger.info(f"{msg} {obj} (PK: {obj.pk})")
|
||||
|
@ -1,4 +1,4 @@
|
||||
from django.db.models import Count, Prefetch
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
@ -7,7 +7,6 @@ from dcim.models import Device
|
||||
from extras.api.serializers import RenderedGraphSerializer
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from extras.models import Graph
|
||||
from ipam.models import VLAN
|
||||
from utilities.api import ModelViewSet
|
||||
from utilities.utils import get_subquery
|
||||
from virtualization import filters
|
||||
|
@ -20,5 +20,4 @@ Pillow==7.2.0
|
||||
psycopg2-binary==2.8.5
|
||||
pycryptodome==3.9.8
|
||||
PyYAML==5.3.1
|
||||
redis==3.5.3
|
||||
svgwrite==1.4
|
||||
|
@ -40,11 +40,12 @@ echo "Installing core dependencies ($COMMAND)..."
|
||||
eval $COMMAND || exit 1
|
||||
|
||||
# Install optional packages (if any)
|
||||
if [ -f "local_requirements.txt" ]
|
||||
then
|
||||
if [ -s "local_requirements.txt" ]; then
|
||||
COMMAND="pip3 install -r local_requirements.txt"
|
||||
echo "Installing local dependencies ($COMMAND)..."
|
||||
eval $COMMAND || exit 1
|
||||
elif [ -f "local_requirements.txt" ]; then
|
||||
echo "Skipping local dependencies (local_requirements.txt is empty)"
|
||||
else
|
||||
echo "Skipping local dependencies (local_requirements.txt not found)"
|
||||
fi
|
||||
|
Reference in New Issue
Block a user