mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into feature
This commit is contained in:
5
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
5
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -17,7 +17,7 @@ body:
|
||||
What version of NetBox are you currently running? (If you don't have access to the most
|
||||
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
||||
before opening a bug report to see if your issue has already been addressed.)
|
||||
placeholder: v2.11.3
|
||||
placeholder: v2.11.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@ -40,7 +40,8 @@ body:
|
||||
creation of any necessary database objects and call out every operation being
|
||||
performed explicitly. If reporting a bug in the REST API, be sure to reconstruct
|
||||
the raw HTTP request(s) being made: Don't rely on a client library such as
|
||||
pynetbox."
|
||||
pynetbox. Additionally, **do not rely on the demo instance** for reproducing
|
||||
suspected bugs, as its data is prone to modification or deletion at any time.
|
||||
placeholder: |
|
||||
1. Click on "create widget"
|
||||
2. Set foo to 12 and bar to G
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v2.11.3
|
||||
placeholder: v2.11.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
@ -22,6 +22,8 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne
|
||||
<div align="center">
|
||||
<h4>Thank you to our sponsors!</h4>
|
||||
|
||||
[](https://try.digitalocean.com/developer-cloud)
|
||||
|
||||
[](https://ns1.com/)
|
||||
|
||||
[](https://stellar.tech/)
|
||||
|
@ -6,7 +6,7 @@ If a change is made to any of the objects returned by the query within that time
|
||||
|
||||
## Invalidating Cached Data
|
||||
|
||||
Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object my its type and numeric ID:
|
||||
Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object by its type and numeric ID:
|
||||
|
||||
```no-highlight
|
||||
$ python netbox/manage.py invalidate dcim.Device.34
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Power Feed
|
||||
|
||||
A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power pot (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks.
|
||||
A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power port (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks.
|
||||
|
||||
Each power feed is assigned an operational type (primary or redundant) and one of the following statuses:
|
||||
|
||||
|
@ -1,5 +1,28 @@
|
||||
# NetBox v2.11
|
||||
|
||||
## v2.11.4 (2021-05-25)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#5121](https://github.com/netbox-community/netbox/issues/5121) - Add content type filters for tags
|
||||
* [#6358](https://github.com/netbox-community/netbox/issues/6358) - Add search field for VLAN groups
|
||||
* [#6393](https://github.com/netbox-community/netbox/issues/6393) - Add `description` filter for IP addresses
|
||||
* [#6400](https://github.com/netbox-community/netbox/issues/6400) - Add cyan color choice for plugin buttons
|
||||
* [#6422](https://github.com/netbox-community/netbox/issues/6422) - Enable filtering users by group under admin UI
|
||||
* [#6441](https://github.com/netbox-community/netbox/issues/6441) - Improve UI paginator to optimize page object count
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#6376](https://github.com/netbox-community/netbox/issues/6376) - Fix assignment of VLAN groups to clusters, cluster groups via REST API
|
||||
* [#6398](https://github.com/netbox-community/netbox/issues/6398) - Avoid exception when deleting device connected to self via circuit
|
||||
* [#6426](https://github.com/netbox-community/netbox/issues/6426) - Allow assigning virtual chassis member interfaces to LAG on VC master
|
||||
* [#6438](https://github.com/netbox-community/netbox/issues/6438) - Fix missing descriptions and label for device type imports and exports
|
||||
* [#6465](https://github.com/netbox-community/netbox/issues/6465) - Fix typo in installed plugins REST API endpoint
|
||||
* [#6467](https://github.com/netbox-community/netbox/issues/6467) - Fix access to metrics on custom `BASE_PATH` when login is required
|
||||
* [#6468](https://github.com/netbox-community/netbox/issues/6468) - Disable ordering VLAN groups list by scope object
|
||||
|
||||
---
|
||||
|
||||
## v2.11.3 (2021-05-07)
|
||||
|
||||
### Enhancements
|
||||
|
@ -20,7 +20,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Provider(PrimaryModel):
|
||||
"""
|
||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||
@ -96,7 +96,7 @@ class Provider(PrimaryModel):
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class ProviderNetwork(PrimaryModel):
|
||||
"""
|
||||
This represents a provider network which exists outside of NetBox, the details of which are unknown or
|
||||
@ -189,7 +189,7 @@ class CircuitType(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Circuit(PrimaryModel):
|
||||
"""
|
||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||
|
@ -1818,7 +1818,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type',
|
||||
'device_type', 'name', 'label', 'type', 'description',
|
||||
]
|
||||
|
||||
|
||||
@ -1827,7 +1827,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type',
|
||||
'device_type', 'name', 'label', 'type', 'description',
|
||||
]
|
||||
|
||||
|
||||
@ -1836,7 +1836,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
||||
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
]
|
||||
|
||||
|
||||
@ -1850,7 +1850,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
||||
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
]
|
||||
|
||||
|
||||
@ -1862,7 +1862,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'mgmt_only',
|
||||
'device_type', 'name', 'label', 'type', 'mgmt_only', 'description',
|
||||
]
|
||||
|
||||
|
||||
@ -1879,7 +1879,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
|
||||
'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
|
||||
]
|
||||
|
||||
|
||||
@ -1891,7 +1891,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'positions',
|
||||
'device_type', 'name', 'type', 'positions', 'label', 'description',
|
||||
]
|
||||
|
||||
|
||||
@ -1900,7 +1900,7 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = [
|
||||
'device_type', 'name',
|
||||
'device_type', 'name', 'label', 'description',
|
||||
]
|
||||
|
||||
|
||||
@ -3150,8 +3150,12 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
||||
|
||||
device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device
|
||||
|
||||
# Restrict parent/LAG interface assignment by device
|
||||
# Restrict parent/LAG interface assignment by device/VC
|
||||
self.fields['parent'].widget.add_query_param('device_id', device.pk)
|
||||
if device.virtual_chassis and device.virtual_chassis.master:
|
||||
# Get available LAG interfaces by VirtualChassis master
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||
else:
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
||||
|
||||
# Limit VLAN choices by device
|
||||
|
@ -30,7 +30,7 @@ __all__ = (
|
||||
# Cables
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Cable(PrimaryModel):
|
||||
"""
|
||||
A physical connection between two endpoints.
|
||||
|
@ -211,7 +211,7 @@ class PathEndpoint(models.Model):
|
||||
# Console ports
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||
@ -254,7 +254,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||
@ -297,7 +297,7 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
# Power ports
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||
@ -408,7 +408,7 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||
@ -512,7 +512,7 @@ class BaseInterface(models.Model):
|
||||
return self.ip_addresses.count()
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||
@ -683,7 +683,7 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
||||
# Pass-through ports
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class FrontPort(ComponentModel, CableTermination):
|
||||
"""
|
||||
A pass-through port on the front of a Device.
|
||||
@ -748,7 +748,7 @@ class FrontPort(ComponentModel, CableTermination):
|
||||
})
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class RearPort(ComponentModel, CableTermination):
|
||||
"""
|
||||
A pass-through port on the rear of a Device.
|
||||
@ -801,7 +801,7 @@ class RearPort(ComponentModel, CableTermination):
|
||||
# Device bays
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class DeviceBay(ComponentModel):
|
||||
"""
|
||||
An empty space within a Device which can house a child device
|
||||
@ -860,7 +860,7 @@ class DeviceBay(ComponentModel):
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class InventoryItem(MPTTModel, ComponentModel):
|
||||
"""
|
||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||
|
@ -75,7 +75,7 @@ class Manufacturer(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class DeviceType(PrimaryModel):
|
||||
"""
|
||||
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
||||
@ -183,6 +183,8 @@ class DeviceType(PrimaryModel):
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.consoleporttemplates.all()
|
||||
]
|
||||
@ -191,6 +193,8 @@ class DeviceType(PrimaryModel):
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.consoleserverporttemplates.all()
|
||||
]
|
||||
@ -201,6 +205,8 @@ class DeviceType(PrimaryModel):
|
||||
'type': c.type,
|
||||
'maximum_draw': c.maximum_draw,
|
||||
'allocated_draw': c.allocated_draw,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.powerporttemplates.all()
|
||||
]
|
||||
@ -211,6 +217,8 @@ class DeviceType(PrimaryModel):
|
||||
'type': c.type,
|
||||
'power_port': c.power_port.name if c.power_port else None,
|
||||
'feed_leg': c.feed_leg,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.poweroutlettemplates.all()
|
||||
]
|
||||
@ -220,6 +228,8 @@ class DeviceType(PrimaryModel):
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'mgmt_only': c.mgmt_only,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.interfacetemplates.all()
|
||||
]
|
||||
@ -230,6 +240,8 @@ class DeviceType(PrimaryModel):
|
||||
'type': c.type,
|
||||
'rear_port': c.rear_port.name,
|
||||
'rear_port_position': c.rear_port_position,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.frontporttemplates.all()
|
||||
]
|
||||
@ -239,6 +251,8 @@ class DeviceType(PrimaryModel):
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'positions': c.positions,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.rearporttemplates.all()
|
||||
]
|
||||
@ -246,6 +260,8 @@ class DeviceType(PrimaryModel):
|
||||
data['device-bays'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.devicebaytemplates.all()
|
||||
]
|
||||
@ -448,7 +464,7 @@ class Platform(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Device(PrimaryModel, ConfigContextModel):
|
||||
"""
|
||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||
@ -891,7 +907,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class VirtualChassis(PrimaryModel):
|
||||
"""
|
||||
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
|
||||
|
@ -21,7 +21,7 @@ __all__ = (
|
||||
# Power
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class PowerPanel(PrimaryModel):
|
||||
"""
|
||||
A distribution point for electrical power; e.g. a data center RPP.
|
||||
@ -71,7 +71,7 @@ class PowerPanel(PrimaryModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
|
||||
"""
|
||||
An electrical circuit delivered from a PowerPanel.
|
||||
|
@ -78,7 +78,7 @@ class RackRole(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Rack(PrimaryModel):
|
||||
"""
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
@ -463,7 +463,7 @@ class Rack(PrimaryModel):
|
||||
return int(allocated_draw_total / available_power_total * 100)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class RackReservation(PrimaryModel):
|
||||
"""
|
||||
One or more reserved units within a Rack.
|
||||
|
@ -130,7 +130,7 @@ class SiteGroup(NestedGroupModel):
|
||||
# Sites
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Site(PrimaryModel):
|
||||
"""
|
||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||
|
@ -31,8 +31,9 @@ def rebuild_paths(obj):
|
||||
|
||||
with transaction.atomic():
|
||||
for cp in cable_paths:
|
||||
invalidate_obj(cp.origin)
|
||||
cp.delete()
|
||||
if cp.origin:
|
||||
invalidate_obj(cp.origin)
|
||||
create_cablepath(cp.origin)
|
||||
|
||||
|
||||
|
@ -7,5 +7,6 @@ EXTRAS_FEATURES = [
|
||||
'custom_links',
|
||||
'export_templates',
|
||||
'job_results',
|
||||
'tags',
|
||||
'webhooks'
|
||||
]
|
||||
|
@ -6,7 +6,7 @@ from django.db.models import Q
|
||||
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.filters import ContentTypeFilter
|
||||
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
from .models import *
|
||||
@ -114,6 +114,12 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
content_type = MultiValueCharFilter(
|
||||
method='_content_type'
|
||||
)
|
||||
content_type_id = MultiValueNumberFilter(
|
||||
method='_content_type_id'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
@ -127,6 +133,32 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
|
||||
Q(slug__icontains=value)
|
||||
)
|
||||
|
||||
def _content_type(self, queryset, name, values):
|
||||
ct_filter = Q()
|
||||
|
||||
# Compile list of app_label & model pairings
|
||||
for value in values:
|
||||
try:
|
||||
app_label, model = value.lower().split('.')
|
||||
ct_filter |= Q(
|
||||
app_label=app_label,
|
||||
model=model
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Get ContentType instances
|
||||
content_types = ContentType.objects.filter(ct_filter)
|
||||
|
||||
return queryset.filter(extras_taggeditem_items__content_type__in=content_types).distinct()
|
||||
|
||||
def _content_type_id(self, queryset, name, values):
|
||||
|
||||
# Get ContentType instances
|
||||
content_types = ContentType.objects.filter(pk__in=values)
|
||||
|
||||
return queryset.filter(extras_taggeditem_items__content_type__in=content_types).distinct()
|
||||
|
||||
|
||||
class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
|
@ -8,12 +8,13 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
||||
CommentField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
CommentField, ContentTypeMultipleChoiceField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField,
|
||||
JSONField, SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
|
||||
from .utils import FeatureQuery
|
||||
|
||||
|
||||
#
|
||||
@ -177,6 +178,15 @@ class AddRemoveTagsForm(forms.Form):
|
||||
|
||||
class TagFilterForm(BootstrapMixin, forms.Form):
|
||||
model = Tag
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
|
||||
required=False,
|
||||
label=_('Tagged object type')
|
||||
)
|
||||
|
||||
|
||||
class TagBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
|
@ -42,7 +42,7 @@ class InstalledPluginsAPIView(APIView):
|
||||
'author': plugin_app_config.author,
|
||||
'author_email': plugin_app_config.author_email,
|
||||
'description': plugin_app_config.description,
|
||||
'verison': plugin_app_config.version
|
||||
'version': plugin_app_config.version
|
||||
}
|
||||
|
||||
def get(self, request, format=None):
|
||||
|
@ -5,6 +5,7 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from circuits.models import Provider
|
||||
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
|
||||
from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
|
||||
from extras.filtersets import *
|
||||
@ -537,6 +538,13 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
Tag.objects.bulk_create(tags)
|
||||
|
||||
# Apply some tags so we can filter by content type
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
|
||||
site.tags.set(tags[0])
|
||||
provider.tags.set(tags[1])
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Tag 1', 'Tag 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@ -549,6 +557,14 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'color': ['ff0000', '00ff00']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_type(self):
|
||||
params = {'content_type': ['dcim.site', 'circuits.provider']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
site_ct = ContentType.objects.get_for_model(Site).pk
|
||||
provider_ct = ContentType.objects.get_for_model(Provider).pk
|
||||
params = {'content_type_id': [site_ct, provider_ct]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = ObjectChange.objects.all()
|
||||
|
@ -7,7 +7,7 @@ from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
||||
from ipam.choices import *
|
||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
|
||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import OrganizationalModelSerializer
|
||||
@ -115,8 +115,7 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||
scope_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(
|
||||
app_label='dcim',
|
||||
model__in=['region', 'sitegroup', 'site', 'location', 'rack']
|
||||
model__in=VLANGROUP_SCOPE_TYPES
|
||||
),
|
||||
required=False
|
||||
)
|
||||
|
@ -468,7 +468,7 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'dns_name']
|
||||
fields = ['id', 'dns_name', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -536,6 +536,10 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
|
||||
|
||||
class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
scope_type = ContentTypeFilter()
|
||||
region = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
@ -563,6 +567,15 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
||||
model = VLANGroup
|
||||
fields = ['id', 'name', 'slug', 'description', 'scope_id']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_scope(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
scope_type=ContentType.objects.get(model=name),
|
||||
|
@ -1291,6 +1291,10 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||
['region', 'sitegroup', 'site'],
|
||||
['location', 'rack']
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
|
@ -77,7 +77,7 @@ class RIR(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Aggregate(PrimaryModel):
|
||||
"""
|
||||
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
|
||||
@ -228,7 +228,7 @@ class Role(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Prefix(PrimaryModel):
|
||||
"""
|
||||
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
|
||||
@ -489,7 +489,7 @@ class Prefix(PrimaryModel):
|
||||
return int(float(child_count) / prefix_size * 100)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class IPAddress(PrimaryModel):
|
||||
"""
|
||||
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
|
||||
|
@ -17,7 +17,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Service(PrimaryModel):
|
||||
"""
|
||||
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
|
||||
|
@ -100,7 +100,7 @@ class VLANGroup(OrganizationalModel):
|
||||
return None
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class VLAN(PrimaryModel):
|
||||
"""
|
||||
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
|
||||
|
@ -13,7 +13,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class VRF(PrimaryModel):
|
||||
"""
|
||||
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
|
||||
@ -88,7 +88,7 @@ class VRF(PrimaryModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class RouteTarget(PrimaryModel):
|
||||
"""
|
||||
A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.
|
||||
|
@ -449,7 +449,8 @@ class VLANGroupTable(BaseTable):
|
||||
name = tables.Column(linkify=True)
|
||||
scope_type = ContentTypeColumn()
|
||||
scope = tables.Column(
|
||||
linkify=True
|
||||
linkify=True,
|
||||
orderable=False
|
||||
)
|
||||
vlan_count = LinkedCountColumn(
|
||||
viewname='ipam:vlan_list',
|
||||
|
@ -577,12 +577,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
ipaddresses = (
|
||||
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar1'),
|
||||
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], assigned_object=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], assigned_object=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], assigned_object=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar2'),
|
||||
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], assigned_object=vminterfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], assigned_object=vminterfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], assigned_object=vminterfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
@ -598,6 +598,10 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'dns_name': ['ipaddress-a', 'ipaddress-b']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
params = {'parent': '10.0.0.0/24'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
|
@ -20,17 +20,20 @@ class LoginRequiredMiddleware(object):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
# Redirect unauthenticated requests (except those exempted) to the login page if LOGIN_REQUIRED is true
|
||||
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
|
||||
# Redirect unauthenticated requests to the login page. API requests are exempt from redirection as the API
|
||||
# performs its own authentication. Also metrics can be read without login.
|
||||
api_path = reverse('api-root')
|
||||
if not request.path_info.startswith((api_path, '/metrics')) and request.path_info != settings.LOGIN_URL:
|
||||
return HttpResponseRedirect(
|
||||
'{}?next={}'.format(
|
||||
settings.LOGIN_URL,
|
||||
parse.quote(request.get_full_path_info())
|
||||
)
|
||||
)
|
||||
# Determine exempt paths
|
||||
exempt_paths = [
|
||||
reverse('api-root')
|
||||
]
|
||||
if settings.METRICS_ENABLED:
|
||||
exempt_paths.append(reverse('prometheus-django-metrics'))
|
||||
|
||||
# Redirect unauthenticated requests
|
||||
if not request.path_info.startswith(tuple(exempt_paths)) and request.path_info != settings.LOGIN_URL:
|
||||
login_url = f'{settings.LOGIN_URL}?next={parse.quote(request.get_full_path_info())}'
|
||||
return HttpResponseRedirect(login_url)
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
|
@ -57,7 +57,7 @@ class TenantGroup(NestedGroupModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Tenant(PrimaryModel):
|
||||
"""
|
||||
A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
|
||||
|
@ -89,6 +89,7 @@ class UserAdmin(UserAdmin_):
|
||||
('Important dates', {'fields': ('last_login', 'date_joined')}),
|
||||
)
|
||||
filter_horizontal = ('groups',)
|
||||
list_filter = ('is_active', 'is_staff', 'is_superuser', 'groups__name')
|
||||
|
||||
def get_inlines(self, request, obj):
|
||||
if obj is not None:
|
||||
|
@ -130,22 +130,24 @@ class ColorChoices(ChoiceSet):
|
||||
|
||||
class ButtonColorChoices(ChoiceSet):
|
||||
"""
|
||||
Map standard button color choices to Bootstrap color classes
|
||||
Map standard button color choices to Bootstrap 3 button classes
|
||||
"""
|
||||
DEFAULT = 'outline-dark'
|
||||
BLUE = 'primary'
|
||||
GREY = 'secondary'
|
||||
CYAN = 'info'
|
||||
GREEN = 'success'
|
||||
RED = 'danger'
|
||||
YELLOW = 'warning'
|
||||
GREY = 'secondary'
|
||||
BLACK = 'dark'
|
||||
|
||||
CHOICES = (
|
||||
(DEFAULT, 'Default'),
|
||||
(BLUE, 'Blue'),
|
||||
(GREY, 'Grey'),
|
||||
(CYAN, 'Cyan'),
|
||||
(GREEN, 'Green'),
|
||||
(RED, 'Red'),
|
||||
(YELLOW, 'Yellow'),
|
||||
(GREY, 'Grey'),
|
||||
(BLACK, 'Black')
|
||||
)
|
||||
|
@ -4,7 +4,9 @@ from django.core.paginator import Paginator, Page
|
||||
|
||||
class EnhancedPaginator(Paginator):
|
||||
|
||||
def __init__(self, object_list, per_page, **kwargs):
|
||||
def __init__(self, object_list, per_page, orphans=None, **kwargs):
|
||||
|
||||
# Determine the page size
|
||||
try:
|
||||
per_page = int(per_page)
|
||||
if per_page < 1:
|
||||
@ -12,7 +14,13 @@ class EnhancedPaginator(Paginator):
|
||||
except ValueError:
|
||||
per_page = settings.PAGINATE_COUNT
|
||||
|
||||
super().__init__(object_list, per_page, **kwargs)
|
||||
# Set orphans count based on page size
|
||||
if orphans is None and per_page <= 50:
|
||||
orphans = 5
|
||||
elif orphans is None:
|
||||
orphans = 10
|
||||
|
||||
super().__init__(object_list, per_page, orphans=orphans, **kwargs)
|
||||
|
||||
def _get_page(self, *args, **kwargs):
|
||||
return EnhancedPage(*args, **kwargs)
|
||||
|
@ -5,7 +5,6 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.db.models.fields.related import RelatedField
|
||||
from django.urls import reverse
|
||||
from django.utils.html import strip_tags
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_tables2 import RequestConfig
|
||||
from django_tables2.data import TableQuerysetData
|
||||
@ -15,19 +14,6 @@ from extras.models import CustomField
|
||||
from .paginator import EnhancedPaginator, get_paginate_count
|
||||
|
||||
|
||||
def stripped_value(self, **kwargs):
|
||||
"""
|
||||
Replaces TemplateColumn's value() method to both strip HTML tags and remove any leading/trailing whitespace.
|
||||
"""
|
||||
html = super(tables.TemplateColumn, self).value(**kwargs)
|
||||
return strip_tags(html).strip() if isinstance(html, str) else html
|
||||
|
||||
|
||||
# TODO: We're monkey-patching TemplateColumn here to strip leading/trailing whitespace. This will no longer
|
||||
# be necessary under django-tables2 v2.3.5+. (See #5926)
|
||||
tables.TemplateColumn.value = stripped_value
|
||||
|
||||
|
||||
class BaseTable(tables.Table):
|
||||
"""
|
||||
Default table for object lists
|
||||
|
@ -116,7 +116,7 @@ class ClusterGroup(OrganizationalModel):
|
||||
# Clusters
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Cluster(PrimaryModel):
|
||||
"""
|
||||
A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
|
||||
@ -199,7 +199,7 @@ class Cluster(PrimaryModel):
|
||||
# Virtual machines
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class VirtualMachine(PrimaryModel, ConfigContextModel):
|
||||
"""
|
||||
A virtual machine which runs inside a Cluster.
|
||||
@ -374,7 +374,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class VMInterface(PrimaryModel, BaseInterface):
|
||||
virtual_machine = models.ForeignKey(
|
||||
to='virtualization.VirtualMachine',
|
||||
|
@ -1,4 +1,4 @@
|
||||
Django==3.2.2
|
||||
Django==3.2.3
|
||||
django-cacheops==6.0
|
||||
django-cors-headers==3.7.0
|
||||
django-debug-toolbar==3.2.1
|
||||
@ -7,13 +7,13 @@ django-mptt==0.12.0
|
||||
django-pglocks==1.0.4
|
||||
django-prometheus==2.1.0
|
||||
django-rq==2.4.1
|
||||
django-tables2==2.3.4
|
||||
django-tables2==2.4.0
|
||||
django-taggit==1.4.0
|
||||
django-timezone-field==4.1.2
|
||||
djangorestframework==3.12.4
|
||||
drf-yasg[validation]==1.20.0
|
||||
gunicorn==20.1.0
|
||||
Jinja2==2.11.3
|
||||
Jinja2==3.0.1
|
||||
Markdown==3.3.4
|
||||
netaddr==0.8.0
|
||||
Pillow==8.2.0
|
||||
|
Reference in New Issue
Block a user