Compare commits

..

237 Commits

Author SHA1 Message Date
Jeremy Stretch
6726403de9 Merge pull request #2821 from digitalocean/develop
Release v2.5.4
2019-01-29 16:38:54 -05:00
Jeremy Stretch
f6d18d243e Release v2.5.4 2019-01-29 16:32:15 -05:00
Jeremy Stretch
8bd9b258a8 Fixes #2816: Handle exception when deleting a device with connected components 2019-01-29 14:27:47 -05:00
Jeremy Stretch
0256448dd8 Closes #2810: Include description fields in interface connections export 2019-01-29 14:08:15 -05:00
Jeremy Stretch
dc70fdbe03 Force string formatting of integer values 2019-01-29 13:57:19 -05:00
Jeremy Stretch
ce1a2875bc Closes #2808: Loosen version pinning for Django to allow patch releases 2019-01-29 13:48:21 -05:00
fouram
b4c9ec27e0 Updated optional-settings.md - Webhook links (#2818)
Corrected (../miscellaneous/webhooks/) to (../additional-features/webhooks/) in two place
2019-01-29 13:38:05 -05:00
Jeremy Stretch
8977ded7b6 Changelog for #2802 2019-01-29 13:36:09 -05:00
ChrisPortman
afb4c636fe Fix #2802 (#2803) 2019-01-29 13:29:22 -05:00
John Anderson
ff40a13f29 fixed select2 paging offset calculation to index at 1 2019-01-29 00:48:41 -05:00
Jeremy Stretch
61a48320af Add pagination widget to console/power/interface connections views 2019-01-23 14:26:17 -05:00
Jeremy Stretch
d53249060d Closes #2807: Include device site/rack assignment in cable trace view 2019-01-23 13:57:26 -05:00
Jeremy Stretch
22eebbbc71 Fixes #2795: Fix duplicate display of pagination controls on child prefix/IP tables 2019-01-23 13:38:16 -05:00
Jeremy Stretch
4227c6b806 Add select2-bootstrap for consistent widget styling 2019-01-23 10:18:44 -05:00
Jeremy Stretch
14695037da Strip return characters when removing line breaks 2019-01-18 09:24:32 -05:00
Jeremy Stretch
0d717cdc82 Changelog for #2798 2019-01-18 09:23:53 -05:00
Jeremy Stretch
ae8c5ae7b8 Merge pull request #2799 from bgilmore/develop
Fixes #2798: URL encode physical_address in links
2019-01-18 09:18:00 -05:00
Brandon Gilmore
fb0ed3db2f Fixes #2798: URL encode physical_address in links 2019-01-17 17:41:46 -08:00
Jeremy Stretch
c69fad7429 Closes #2733: Enable bulk assignment of MAC addresses to interfaces 2019-01-17 16:20:14 -05:00
Jeremy Stretch
92a2f529e3 Closes #2766: Extend users admin table to include superuser and active fields 2019-01-17 16:14:06 -05:00
Jeremy Stretch
bd74e2f30b Closes #2782: Add is_pool field for prefix filtering 2019-01-17 16:02:16 -05:00
Jeremy Stretch
a950c95416 Merge pull request #2738 from candlerb/candlerb/graphviz-exception
Show exception description on failure to render graphviz
2019-01-16 16:02:11 -05:00
John Anderson
dc9e9fd08f select2 changelog items 2019-01-15 20:01:02 -05:00
John Anderson
1abbaf99dc Merge pull request #2772 from digitalocean/select2-ui
Select2 UI
2019-01-15 16:57:03 -08:00
John Anderson
af6bb53a01 removed debug log 2019-01-15 16:36:51 -05:00
John Anderson
2d7e5a57e7 handle disabled-indicator 2019-01-15 16:33:44 -05:00
Jeremy Stretch
b6737aff59 Remove invalid kwarg (conflict w/ #2779) 2019-01-15 15:47:47 -05:00
Jeremy Stretch
c5f2cbf9fa Syntax fix 2019-01-15 15:44:21 -05:00
John Anderson
d6d8b078b9 Merge branch 'develop' into select2-ui 2019-01-15 10:26:39 -08:00
Jeremy Stretch
1d7a7e2d1d Fixes #2779: Include "none" option when filter IP addresses by role 2019-01-15 11:26:41 -05:00
Jeremy Stretch
4a290f3834 Fixes #2783: Fix AttributeError exception when attempting to delete region(s) 2019-01-15 10:44:11 -05:00
Jeremy Stretch
9e492cbb4d Post-release version bump 2019-01-11 11:35:06 -05:00
Jeremy Stretch
e17d79e10f Merge pull request #2778 from digitalocean/develop
Release v2.5.3
2019-01-11 11:32:49 -05:00
Jeremy Stretch
28a2981a4f Release v2.5.3 2019-01-11 11:27:59 -05:00
Jeremy Stretch
d356e288a2 Expanded the bug report template 2019-01-11 10:30:35 -05:00
Jeremy Stretch
dd5f37391f Fixes #2777: Fix cable validation to handle duplicate connections on import 2019-01-11 10:17:06 -05:00
John Anderson
17d6584ef4 removed deprecated code 2019-01-10 21:23:22 -05:00
John Anderson
ad4fb3ce8b IPAM Select2 forms and changelog 2019-01-10 21:19:13 -05:00
John Anderson
5f1f8ee73b Circuits Select2 forms 2019-01-10 17:50:06 -05:00
John Anderson
60224be272 Merge branch 'develop' of github.com:digitalocean/netbox into select2-ui 2019-01-10 17:32:38 -05:00
John Anderson
6dcd48fef1 Virtulization Select2 forms 2019-01-10 17:32:23 -05:00
John Anderson
951e7a68e9 Secrets Select2 forms 2019-01-09 23:46:45 -05:00
John Anderson
86bafbb760 Tenancy Select2 forms 2019-01-09 23:33:08 -05:00
John Anderson
a6564c49e2 DCIM removed annotates from querysets 2019-01-09 13:44:39 -05:00
Jeremy Stretch
c89735cd4e Disable wrapping of report labels 2019-01-09 11:47:34 -05:00
John Anderson
f3216abebf Merge branch 'develop' of github.com:digitalocean/netbox into select2-ui 2019-01-09 11:25:56 -05:00
Jeremy Stretch
5676bd15dd Closes #2682: Add DAC and AOC cable types 2019-01-09 09:28:44 -05:00
John Anderson
bf8d57c7d1 DCIM filter forms select2 2019-01-08 15:35:34 -08:00
Jeremy Stretch
0d415d94a5 Fixes #2757: Always treat first/last IPs within a /31 or /127 as usable 2019-01-08 12:04:32 -05:00
Jeremy Stretch
73a1d6a7ba Fixes #2762: Add missing DCIM field values to API _choices endpoint 2019-01-08 11:51:13 -05:00
John Anderson
72d5c6fd1b #2590 changelog 2019-01-05 17:46:23 -05:00
John Anderson
6d5d9c8af3 implemented #2590 - improved color picker with actual colors 2019-01-05 17:43:42 -05:00
John Anderson
c27cea981c select2 for all create/edit forms in dcim 2019-01-04 14:41:36 -05:00
Jeremy Stretch
f7f6704fc1 Preserve filtering/ordering parameters when modifying per_page count 2019-01-04 13:17:24 -05:00
John Anderson
fca97f9768 Merge branch 'develop' of github.com:digitalocean/netbox into select2-ui 2019-01-04 12:44:41 -05:00
John Anderson
7a5a73ce34 fixed whitespace 2019-01-04 12:44:31 -05:00
Jeremy Stretch
170e01b549 Closes #1983: Enable regular expressions when bulk renaming device components 2019-01-04 12:30:38 -05:00
Jeremy Stretch
99dc46a89e Fixes #2742: Preserve cluster assignment when editing a device 2019-01-04 11:46:53 -05:00
Jeremy Stretch
848aa0b098 Closes #1870: Add per-page toggle to object lists 2019-01-04 11:07:55 -05:00
John Anderson
81a0889568 initial static select2 fields 2019-01-03 23:02:05 -05:00
Jeremy Stretch
0a820d9c98 Closes #1871: Enable filtering sites by parent region 2019-01-03 16:59:49 -05:00
Jeremy Stretch
209a9f0ffc Closes #1630: Enable bulk editing of prefix/IP mask length 2019-01-03 16:21:21 -05:00
Jeremy Stretch
3101a86381 Changelog updates; import cleanup 2019-01-03 15:30:12 -05:00
Jeremy Stretch
27ca0d0930 Merge pull request #2737 from TakeMeNL/feature/2726
Closes #2726 - Search for Cable Labels
2019-01-03 15:27:21 -05:00
Jeremy Stretch
6ca045e1a9 Merge pull request #2694 from DanSheps/2693-fiber-cable-colors
Closes #2693 - Adds additional colors for cables & roles
2019-01-03 15:24:36 -05:00
dansheps
0c86693dc4 Closes #2693
* Revert CSS Background Hack
2019-01-03 13:20:29 -06:00
John Anderson
5285b6926f updated filter-for attr handling 2019-01-03 03:00:27 -05:00
John Anderson
f3cfc17a52 fix processing of initial tags 2019-01-03 02:25:12 -05:00
Brian Candler
c58166137c Show exception description on failure to render graphviz
This means that problems give a more specific reason.  In the event
that dot is not found, the error is now:

There was an error generating the requested graph: failed to execute ['dot',
'-Tpng'], make sure the Graphviz executables are on your systems' PATH
2019-01-01 11:41:54 +00:00
John Anderson
28a02e9943 initial select2 support 2018-12-30 02:35:18 -05:00
TakeMeNL
c6d9206dd1 Added ability to search for cables in global search 2018-12-29 22:22:12 +01:00
Jeremy Stretch
d144d3a584 Post-release version bump 2018-12-21 11:48:12 -05:00
Jeremy Stretch
8cf8710130 Merge pull request #2725 from digitalocean/develop
Release v2.5.2
2018-12-21 11:46:31 -05:00
Jeremy Stretch
3705e37678 Release v2.5.2 2018-12-21 11:44:30 -05:00
Jeremy Stretch
ebe5193348 Fixes #2724: Limit rear port choices to current device when editing a front port 2018-12-21 11:09:44 -05:00
Jeremy Stretch
a3097d254e Fixes #2721: Detect loops when tracing front/rear ports 2018-12-21 10:54:20 -05:00
Jeremy Stretch
38276d9539 Fixes #2723: Correct permission evaluation when bulk deleting tags 2018-12-21 09:11:07 -05:00
Jeremy Stretch
91a2168952 Fixes #2717: Fix bulk deletion of tags 2018-12-21 09:08:00 -05:00
Jeremy Stretch
4a10b4ece0 Fixes #2704: Fix form select widget population on parent with null value 2018-12-20 15:49:35 -05:00
Jeremy Stretch
853b1fad15 Fixes #2712: Preserve list filtering after editing objects in bulk 2018-12-20 15:33:53 -05:00
Jeremy Stretch
7acbeb55bc Minor tweaks 2018-12-20 09:54:59 -05:00
Jeremy Stretch
8498e0088b Merge pull request #2667 from Jemikwa/develop
#2656 Updating LDAP documentation
2018-12-20 09:53:21 -05:00
Jeremy Stretch
aae10f7d71 Tweaked 200GE and 400GE interface type labels 2018-12-20 09:18:18 -05:00
Jeremy Stretch
6b19a2b101 Fixes #2709: Update example report for compatibility with v2.5 2018-12-19 16:34:35 -05:00
Jeremy Stretch
b44a76e6bd Closes #2537: Added AUTH_LDAP_MIRROR_GROUPS setting to LDAP docs 2018-12-19 16:24:41 -05:00
Jeremy Stretch
7f71fc1d42 Closes #2561: Add 200G and 400G interface types 2018-12-19 16:13:04 -05:00
Jeremy Stretch
ba9fe408bc #2675: Added InventoryItem search form field for 'discovered' 2018-12-19 14:15:22 -05:00
Jeremy Stretch
40cb576e11 Fixes #2673: Fix exception on LLDP neighbors view for device with a circuit connected 2018-12-19 14:04:22 -05:00
Jeremy Stretch
2f1db2fdf3 Fixes #2691: Cable trace should follow circuits 2018-12-19 12:48:20 -05:00
Jeremy Stretch
f4a22e5af3 Introduced fgcolor template filter to render ideal foreground color for any background color 2018-12-19 12:17:40 -05:00
Jeremy Stretch
aca57ec281 Fixes #2698: Remove pagination restriction on bulk component creation for devices/VMs 2018-12-19 10:59:12 -05:00
Jeremy Stretch
68cb8b6895 Closes #2701: Enable filtering of prefixes by exact prefix value 2018-12-19 10:02:18 -05:00
Jeremy Stretch
82e8c0152e Fixes #2707: Correct permission evaluation for circuit termination cabling 2018-12-19 09:36:45 -05:00
dansheps
f499f2dd66 Closes #2693 2018-12-14 11:51:20 -06:00
Jeremy Stretch
d4a9318826 Post-release version bump 2018-12-13 15:24:13 -05:00
Jeremy Stretch
27a893a9a1 Merge pull request #2688 from digitalocean/develop
Release v2.5.1
2018-12-13 15:20:09 -05:00
Jeremy Stretch
9f1fcca5ea Release v2.5.1 2018-12-13 15:03:08 -05:00
Jeremy Stretch
bb564363d5 Fix regression from #2683 2018-12-13 14:59:54 -05:00
Jeremy Stretch
dd2a6a41da Fixes #2687: Correct naming of before/after filters for changelog entries 2018-12-13 14:43:05 -05:00
Jeremy Stretch
a6c8c615eb Closes #2674: Enable filtering changelog by object type under web UI 2018-12-13 14:37:03 -05:00
Jeremy Stretch
0d3b1bfca4 Fixes #2683: Fix exception when connecting a cable to a RearPort with no corresponding FrontPort 2018-12-12 16:40:34 -05:00
Jeremy Stretch
edd763b1aa Fixes #2684: Fix custom field filtering 2018-12-12 16:06:50 -05:00
Jeremy Stretch
2418fed65b Fixes #2663: Prevent duplicate interfaces from appearing under VLAN members view 2018-12-12 13:18:42 -05:00
Jeremy Stretch
785cdcefd6 Closes #2671: Add documentation of API brief format 2018-12-12 10:27:18 -05:00
Jeremy Stretch
3480832bf5 Added changelog for #2662 (fixed under #2680) 2018-12-12 09:59:54 -05:00
Jeremy Stretch
ee038bd77b Closes #2655: Add 128GFC Fibrechannel interface type 2018-12-12 09:48:17 -05:00
Jeremy Stretch
6460c95e00 Fixes #2678: Fix error when viewing webhook in admin UI without write permission 2018-12-12 09:30:31 -05:00
Jeremy Stretch
b0a6781623 Fixes #2680: Disallow POST requests to /dcim/interface-connections/ API endpoint 2018-12-12 09:20:07 -05:00
Jeremy Stretch
8364e56e86 Fixes #2676: Fix exception when passing dictionary value to a ChoiceField 2018-12-11 17:00:20 -05:00
Jeremy Stretch
b8a4316297 Changelog for #2666 2018-12-11 13:47:24 -05:00
Jeremy Stretch
24d1707693 Merge pull request #2670 from DanSheps/2666-fix-length-display
Fixes #2666: Uses correct function for displaying choices label
2018-12-11 13:45:16 -05:00
dansheps
b4f79f1667 Fixes #2666: Uses correct function for displaying choices label
* Changes record.length_type to record.get_length_type_display
2018-12-11 12:40:07 -06:00
Jemikwa
064dd9bef2 Updating LDAP documentation
Adding information on service restarts and logging LDAP queries for troubleshooting.
2018-12-11 11:45:45 -06:00
Jeremy Stretch
b697c30941 #2627: Removed reference to provider from Circuit.__str__() 2018-12-11 11:15:45 -05:00
Jeremy Stretch
93c95fdfa8 Post-release version bump 2018-12-10 10:29:51 -05:00
Jeremy Stretch
8863a3126d Merge pull request #2660 from digitalocean/develop
Release v2.5.0
2018-12-10 10:27:24 -05:00
Jeremy Stretch
acbe5f6418 Release v2.5.0 2018-12-10 10:22:32 -05:00
Jeremy Stretch
4e6652d811 Change pip command to pip3 2018-12-10 09:57:37 -05:00
Jeremy Stretch
7d4fa69595 Fixes #2657: Fix typo 2018-12-10 09:54:30 -05:00
Jeremy Stretch
baeb7937fc Updated requirements for v2.5 release 2018-12-07 15:52:25 -05:00
Jeremy Stretch
2bd9f8a11f Updated installation docs for v2.5 release 2018-12-07 15:29:18 -05:00
Jeremy Stretch
44a2919a29 Django 2.1 requires Python 3.5+ 2018-12-07 14:44:36 -05:00
Jeremy Stretch
77fbc42f75 Relax Python version requirement to 3.4 2018-12-07 14:29:17 -05:00
Jeremy Stretch
65edffea63 Merge v2.5 work 2018-12-07 10:51:28 -05:00
Jeremy Stretch
bf0083552d Merge pull request #2653 from digitalocean/develop
Release v2.4.9
2018-12-07 10:25:46 -05:00
Jeremy Stretch
869194354c Release v2.4.9 2018-12-07 10:19:57 -05:00
Jeremy Stretch
aa8c836b94 Closes #2611: Fix error handling when assigning a clustered device to a different site 2018-12-07 09:57:55 -05:00
Jeremy Stretch
9689ba2c4f Fix representation of connected_endpoint_type for non-connected components 2018-12-06 16:39:03 -05:00
Jeremy Stretch
703be259fd Normalize connection_status for non-connected device components during migration 2018-12-06 16:32:42 -05:00
Jeremy Stretch
45a1dfbd8a Closes #2649: Add connected_endpoint_type to connectable device component API representations 2018-12-06 16:14:03 -05:00
Jeremy Stretch
360303f86c Closes #2474: Add cabled and connection_status filters for device components 2018-12-06 12:39:12 -05:00
Jeremy Stretch
64d37cd450 Closes #2648: Include the connection_status field in nested represenations of connectable device components 2018-12-06 12:14:54 -05:00
Jeremy Stretch
71dee2758b Simplified filter_device() for Interfaces 2018-12-06 11:33:24 -05:00
Jeremy Stretch
870edbb44a Fixes #2626: Remove extraneous permissions generated from proxy models 2018-12-05 16:53:58 -05:00
Jeremy Stretch
2a07e8f3f0 Move queryset_to_csv() utility into ObjectListView to allow overriding by individual views 2018-12-05 16:35:59 -05:00
Jeremy Stretch
686a65880e Closes #2495: Enable deep-merging of config context data 2018-12-05 14:34:49 -05:00
Jeremy Stretch
ab4cb46d94 Additional API change notes 2018-12-05 11:13:29 -05:00
Jeremy Stretch
d3d6c83fbb Fixes #2634: Enforce consistent representation of unnamed devices in rack view 2018-12-04 15:29:58 -05:00
Jeremy Stretch
4e3567659a Add reminder to update static field choices 2018-12-04 15:19:38 -05:00
Jeremy Stretch
f0874f4be0 Add missing choices for new cable and rack fields 2018-12-04 15:15:40 -05:00
Jeremy Stretch
dffa2d3556 Closes #2632: Change representation of null values from 0 to 'null' 2018-12-04 15:09:07 -05:00
Jeremy Stretch
7bbf33ee39 Don't force the docs to open in a new window 2018-12-04 09:44:25 -05:00
Jeremy Stretch
90e7080b63 Closes #2641: Restored link to NetBox shell documentation 2018-12-04 09:19:32 -05:00
John Anderson
e6ee26cf0e CHANGELOG.md 2018-12-04 00:46:36 -05:00
John Anderson
0dcab07519 fixes #2623 - model class being passed to rqworker 2018-12-04 00:40:54 -05:00
Jeremy Stretch
a3ade01224 Fixes #2639: Fix preservation of length/dimensions unit for racks and cables 2018-12-03 11:13:37 -05:00
mmahacek
232e6f5076 #2635 - Update documentation for python3 update (#2636)
Add reference to reinstalling the django-rq module
2018-12-03 09:56:43 -05:00
Jeremy Stretch
d1cd366dc9 Fixes #2616: Convert Rack outer_unit and Cable length_unit to integer-based choice fields 2018-11-30 12:26:28 -05:00
Jeremy Stretch
a1a9396287 Closes #2594: upgrade.sh no longer invokes sudo 2018-11-30 11:12:10 -05:00
Jeremy Stretch
ca0248c3a2 Closes #2089: Add SONET interface form factors 2018-11-30 09:28:56 -05:00
Jeremy Stretch
a43fc0d3d3 Closes #2560: Add slug to DeviceType UI view 2018-11-28 16:19:05 -05:00
Jeremy Stretch
08b4b24296 Fixes #2622: Enable filtering cables by multiple types/colors 2018-11-28 14:22:55 -05:00
Jeremy Stretch
5acd429c55 Fixes #2624: Delete associated content type and permissions when removing InterfaceConnection model 2018-11-28 13:45:02 -05:00
Jeremy Stretch
6c2a9107dd Closes #2597: Add FibreChannel SFP28 (32GFC) interface form factor 2018-11-28 09:56:48 -05:00
Jeremy Stretch
879d879e56 Closes #2617: Explicitly mention that test service runs on port 8000 2018-11-28 09:35:33 -05:00
Jeremy Stretch
c6d048ca51 Fixes #2576: Correct type for count_* fields in site API representation 2018-11-27 16:27:47 -05:00
Jeremy Stretch
112aaea51f Updated changelog for #2400 2018-11-27 16:18:57 -05:00
Tatsushi Demachi
c3cdf8e97e Fix type mismatches in API view (#2429)
* Fix tags field to be shown as array in API view

`tags` field in serializers is defineded as `TagListSerializerField`.
It should be shown as an array value in API view but actually, it is a
simple string value.

This fixes it by introducing a new `FieldInspector` to handle
`TagListSerializerField` type field as an array. It doesn't affects any
other type fields.

* Fix SerializedPKRelatedField type API expression

A field definded as `SerializedPKRelatedField` should be shown as an
array of child serializer objects in a response value definition in API
view but it is shown as an array of primary key values (usually
`integer` type) of a child serializer.

This fixes it by introducing a new `FieldInspector` to handle the field.
It doesn't affect any other type fields.

* Fix request parameter representation in API view

In API view, representation of a parameter defined as a sub class of
`WritableNestedSerializer` should be vary between a request and a
response. For example, `tenant` field in `IPAddressSerializer` should be
shown like following as a request body:

```
tenant: integer ...
```

while it should be shown like following as a response body:

```
tenant: {
    id: integer ...,
    url: string ...,
    name: string ...,
    slug: string ...
}
```

But in both cases, it is shown as a response body type expression. This
causes an error at sending an API request with that type value.

It is only an API view issue, API can handle a request if a request
parameter is structured as an expected request body by ignoring the
wrong expression.

This fixes the issue by replacing an implicitly used default auto schema
generator class by its sub class and returning a pseudo serializer with
'Writable' prefix at generating a request body. The reason to introduce
a new generator class is that there is no other point which can
distinguish a request and a response. It is not enough to distinguish
POST, PUT, PATCH methods from GET because former cases may return a JSON
object as a response but it is also represented as same as a request
body, causes another mismatch.

This also fixes `SerializedPKRelatedField` type field representation. It
should be shown as an array of primary keys in a request body.

Fixed #2400
2018-11-27 16:14:45 -05:00
Jeremy Stretch
d2744700c6 Fixes #2615: Tweak live search widget to use brief format for API requests 2018-11-27 12:41:00 -05:00
Jeremy Stretch
5d07a5a670 Fixes #2613: Decrease live search minimum characters to three 2018-11-27 12:20:52 -05:00
Jeremy Stretch
4da755e75f Formatting cleanup 2018-11-27 11:57:29 -05:00
Jeremy Stretch
bd7aee7c1f Closes #2614: Simplify calls of super() for Python 3 2018-11-27 10:52:24 -05:00
Jeremy Stretch
f3aef37163 Add developer guidance for the introduction of new dependencies 2018-11-27 10:45:10 -05:00
Jeremy Stretch
7d262296e1 Added a description and repo URL for each dependency 2018-11-27 09:51:48 -05:00
Jeremy Stretch
90a4b62976 Changelog for #2606 2018-11-26 14:41:09 -05:00
Daniel Sheppard
7346083b26 Fixes #2606 - Added MultipleChoiceFilter for form_factor (#2610)
* Fixes #2606 - Added MultipleChoiceFilter for form_factor

* Fixes #2606 - Add MultipleChoiceField for form_factor
Fixes error with too many lines.
2018-11-26 14:19:05 -05:00
Tyler Bigler
f052bbc36e Refactor Extras Migration Version Check (#2604)
* Add constant for DB_MINIMUM_VERSION

* Refactor verify_postgresql_version to use Django connection pg_version method for comparing versions.

* Remove StrictVersion import

* Remove DB_MINIMUM_VERSION as not necessary in constants.

* Define DB_MINIMUM_VERSION locally to freeze to migration.

* Refactor database version verification to use django builtin methods.
2018-11-26 14:16:37 -05:00
Jeremy Stretch
8d4329197a Merge pull request #2600 from digitalocean/develop
Release v2.4.8
2018-11-20 11:58:29 -05:00
Jeremy Stretch
cb83eb204b Merge pull request #2552 from digitalocean/develop
Release v2.4.7
2018-11-06 10:55:29 -05:00
Jeremy Stretch
74d525364a Merge pull request #2494 from digitalocean/develop
Release v2.4.6
2018-10-05 15:48:11 -04:00
Jeremy Stretch
125975832b Merge pull request #2478 from digitalocean/develop
Release v2.4.5
2018-10-02 15:29:13 -04:00
Jeremy Stretch
bcf22831e2 Merge pull request #2387 from digitalocean/develop
Release v2.4.4
2018-08-22 11:53:56 -04:00
Jeremy Stretch
3b26ce6501 Merge pull request #2386 from digitalocean/revert-2376-patch-1
Revert "Add missing library"
2018-08-22 11:44:31 -04:00
Jeremy Stretch
1b2d3bf08b Revert "Add missing library" 2018-08-22 11:44:07 -04:00
Jeremy Stretch
492bc9f86e Merge pull request #2376 from craig/patch-1
Add missing library
2018-08-22 11:43:46 -04:00
Craig
967feb6931 Add missing library
WSGIPassAuthorization fails if libapache2-mod-wsgi-py3 is missing
2018-08-21 00:41:29 +02:00
Jeremy Stretch
f224ad2959 Merge pull request #2346 from digitalocean/develop
Release v2.4.3
2018-08-09 16:39:45 -04:00
Jeremy Stretch
242cb7c7cb Merge pull request #2332 from digitalocean/develop
Release v2.4.2
2018-08-08 09:16:50 -04:00
Jeremy Stretch
ea7386b04b Merge pull request #2316 from digitalocean/develop
Release v2.4.1
2018-08-07 09:25:10 -04:00
Jeremy Stretch
7a27dbb374 Merge pull request #2307 from digitalocean/develop
Release v2.4.0
2018-08-06 12:40:00 -04:00
Jeremy Stretch
a85e6370a8 Merge pull request #2275 from digitalocean/develop
Release v2.3.7
2018-07-26 14:29:15 -04:00
Jeremy Stretch
09a03565d7 Merge pull request #2244 from digitalocean/develop
Release v2.3.6
2018-07-16 11:54:12 -04:00
Jeremy Stretch
6159994552 Merge pull request #2212 from digitalocean/develop
Release v2.3.5
2018-07-02 15:55:25 -04:00
Jeremy Stretch
a1f624c1cc Merge pull request #2152 from digitalocean/develop
Release v2.3.4
2018-06-07 16:14:18 -04:00
Jeremy Stretch
328958876a Merge pull request #2041 from digitalocean/develop
Release v2.3.3
2018-04-19 11:15:48 -04:00
Jeremy Stretch
68f73c7f94 Merge pull request #1987 from digitalocean/develop
Release v2.3.2
2018-03-22 15:05:59 -04:00
Jeremy Stretch
ec4d28ac6c Merge pull request #1937 from digitalocean/develop
Release v2.3.1
2018-03-01 15:36:10 -05:00
Jeremy Stretch
957074a134 Merge pull request #1913 from digitalocean/develop
Release v2.3.0
2018-02-26 14:23:03 -05:00
Jeremy Stretch
c4f7e8121a Merge pull request #1903 from digitalocean/develop
Release v2.2.10
2018-02-21 16:05:45 -05:00
Jeremy Stretch
6436d703f5 Merge pull request #1852 from digitalocean/develop
Release v2.2.9
2018-01-31 10:43:20 -05:00
Jeremy Stretch
ec0cb7a8bc Merge pull request #1789 from digitalocean/develop
Release v2.2.8
2017-12-20 15:27:22 -05:00
Jeremy Stretch
e98f0c39d1 Merge pull request #1757 from digitalocean/develop
Release v2.2.7
2017-12-07 14:52:28 -05:00
Jeremy Stretch
50a451eddc Merge pull request #1720 from digitalocean/develop
Release v2.2.6
2017-11-16 12:00:34 -05:00
Jeremy Stretch
a5a7358d26 Merge pull request #1708 from digitalocean/develop
Release v2.2.5
2017-11-14 13:25:11 -05:00
Jeremy Stretch
f9452163c5 Merge pull request #1671 from digitalocean/develop
Release v2.2.4
2017-10-31 15:21:23 -04:00
Jeremy Stretch
3067c3f262 Merge pull request #1668 from digitalocean/develop
Release v2.2.3
2017-10-31 14:02:15 -04:00
Jeremy Stretch
7a64404299 Merge pull request #1614 from digitalocean/develop
Release v2.2.2
2017-10-17 11:24:02 -04:00
Jeremy Stretch
2bda399982 Merge pull request #1577 from digitalocean/develop
Release v2.2.1
2017-10-12 16:11:17 -04:00
Jeremy Stretch
74731bc6ae Merge pull request #1575 from digitalocean/develop
Release v2.2.0
2017-10-12 14:01:28 -04:00
Jeremy Stretch
7cb287d6c6 Merge pull request #1572 from digitalocean/develop
Release v2.1.6
2017-10-11 13:02:32 -04:00
Jeremy Stretch
aa8f734bd1 Merge pull request #1537 from digitalocean/develop
Release v2.1.5
2017-09-25 14:52:43 -04:00
Jeremy Stretch
f6d1163ddd Merge pull request #1461 from digitalocean/develop
Release v2.1.4
2017-08-30 14:43:01 -04:00
Jeremy Stretch
5be30bd278 Merge pull request #1428 from digitalocean/develop
Release v2.1.3
2017-08-15 15:52:34 -04:00
Jeremy Stretch
fa7b7288c9 Merge pull request #1398 from digitalocean/develop
Release v2.1.2
2017-08-04 10:54:29 -04:00
Jeremy Stretch
9cc03aaa9a Merge pull request #1387 from digitalocean/develop
Release v2.1.1
2017-08-02 14:22:30 -04:00
Jeremy Stretch
1bda56ea23 Merge pull request #1372 from digitalocean/develop
Release v2.1.0
2017-07-25 11:21:44 -04:00
Jeremy Stretch
64a34ced72 Merge pull request #1346 from digitalocean/develop
Release v2.0.10
2017-07-14 10:09:16 -04:00
Jeremy Stretch
e05d379101 Merge pull request #1327 from digitalocean/develop
Release v2.0.9
2017-07-10 09:43:59 -04:00
Jeremy Stretch
a355783377 Merge pull request #1316 from digitalocean/develop
Release v2.0.8
2017-07-05 14:36:08 -04:00
Jeremy Stretch
88239e0b0d Merge pull request #1278 from digitalocean/develop
Release v2.0.7
2017-06-15 14:26:38 -04:00
Jeremy Stretch
5c63a499d5 Merge pull request #1259 from digitalocean/develop
Release v2.0.6
2017-06-12 09:51:15 -04:00
Jeremy Stretch
50496b1a59 Merge pull request #1251 from digitalocean/develop
Release v2.0.5
2017-06-08 10:10:41 -04:00
Jeremy Stretch
f7b0d22f86 Merge pull request #1230 from digitalocean/develop
Release v2.0.4
2017-05-25 14:45:13 -04:00
Jeremy Stretch
ad95b86fdd Merge pull request #1201 from digitalocean/develop
Release v2.0.3
2017-05-18 14:37:19 -04:00
Jeremy Stretch
43e1e0dbc8 Merge pull request #1181 from digitalocean/develop
Release v2.0.2
2017-05-15 13:23:33 -04:00
Jeremy Stretch
f731900e2f Merge pull request #1154 from digitalocean/develop
Release v2.0.1
2017-05-09 22:47:52 -04:00
Jeremy Stretch
b1bcaa33e7 Merge pull request #1148 from digitalocean/develop
Release v2.0.0
2017-05-09 15:09:28 -04:00
Jeremy Stretch
17873706b7 Merge pull request #1094 from digitalocean/develop
Release v1.9.6
2017-04-21 14:52:53 -04:00
Jeremy Stretch
e0ad2b4555 Merge pull request #1054 from digitalocean/develop
Release v1.9.5
2017-04-06 16:35:15 -04:00
Jeremy Stretch
f89d91783b Merge pull request #1035 from digitalocean/develop
Release v1.9.4-r1
2017-04-04 15:50:28 -04:00
Jeremy Stretch
3ffe36e5ed Merge pull request #1032 from digitalocean/develop
Release v1.9.4
2017-04-04 12:01:58 -04:00
Jeremy Stretch
be393a9d10 Merge pull request #989 from digitalocean/develop
Release v1.9.3
2017-03-23 16:27:06 -04:00
Jeremy Stretch
27eefd8705 Merge pull request #966 from digitalocean/develop
Release v1.9.2
2017-03-14 17:14:19 -04:00
Jeremy Stretch
097e0f38ff Merge pull request #949 from digitalocean/develop
Release v1.9.1
2017-03-08 14:40:16 -05:00
Jeremy Stretch
ce26b566a4 Merge pull request #939 from digitalocean/develop
Release v1.9.0-r1
2017-03-03 11:28:02 -05:00
Jeremy Stretch
0e14bc1e02 Merge pull request #933 from digitalocean/develop
Release v1.9.0
2017-03-02 13:27:10 -05:00
Jeremy Stretch
ce6796ed9b Merge pull request #870 from digitalocean/develop
Release v1.8.4
2017-02-03 13:59:02 -05:00
Jeremy Stretch
c90cecc2fb Merge pull request #849 from digitalocean/develop
Release v1.8.3
2017-01-26 13:58:52 -05:00
Jeremy Stretch
b6bbcb0609 Merge pull request #814 from digitalocean/develop
Release v1.8.2
2017-01-18 16:23:28 -05:00
Jeremy Stretch
23f6832d9c Merge pull request #774 from digitalocean/develop
Release v1.8.1
2017-01-04 15:30:54 -05:00
Jeremy Stretch
88dace75a1 Merge pull request #766 from digitalocean/develop
Release v1.8.0
2017-01-03 15:13:36 -05:00
Jeremy Stretch
8eb140fd65 Merge pull request #736 from digitalocean/develop
Release v1.7.3
2016-12-08 12:34:53 -05:00
Jeremy Stretch
1f09f3d096 Merge pull request #728 from digitalocean/develop
Release v1.7.2-r1
2016-12-06 15:38:52 -05:00
Jeremy Stretch
66be85a41f Merge pull request #726 from digitalocean/develop
Release v1.7.2
2016-12-06 14:55:19 -05:00
Jeremy Stretch
814c11167e Merge pull request #694 from digitalocean/develop
Release v1.7.1
2016-11-15 12:34:09 -05:00
Jeremy Stretch
57ddd5086f Merge pull request #666 from digitalocean/develop
Release v1.7.0
2016-11-03 15:12:33 -04:00
Jeremy Stretch
c171547037 Merge pull request #625 from digitalocean/develop
Release v1.6.3
2016-10-19 16:25:50 -04:00
196 changed files with 18900 additions and 3470 deletions

View File

@@ -17,15 +17,20 @@ about: Report a reproducible bug in the current release of NetBox
-->
### Environment
* Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.3.6 -->
* NetBox version: <!-- Example: 2.5.2 -->
<!--
Describe in detail the steps that someone else can take to reproduce this
bug using the current stable release of NetBox (or the current beta release
where applicable).
Describe in detail the exact steps that someone else can take to reproduce
this bug using the current stable release of NetBox (or the current beta
release where applicable). Begin with the 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 wrapper like pynetbox.
-->
### Steps to Reproduce
1.
2.
3.
<!-- What did you expect to happen? -->
### Expected Behavior

2
.gitignore vendored
View File

@@ -10,3 +10,5 @@
fabfile.py
*.swp
gunicorn_config.py
.DS_Store
.vscode

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,72 @@
# The Python web framework on which NetBox is built
# https://github.com/django/django
Django
# Django middleware which permits cross-domain API requests
# https://github.com/OttoYiu/django-cors-headers
django-cors-headers
# Runtime UI tool for debugging Django
# https://github.com/jazzband/django-debug-toolbar
django-debug-toolbar
# Library for writing reusable URL query filters
# https://github.com/carltongibson/django-filter
django-filter
# Modified Preorder Tree Traversal (recursive nesting of objects)
# https://github.com/django-mptt/django-mptt
django-mptt
# Abstraction models for rendering and paginating HTML tables
# https://github.com/jieter/django-tables2
django-tables2
# User-defined tags for objects
# https://github.com/alex/django-taggit
django-taggit
# A Django REST Framework serializer which represents tags
# https://github.com/glemmaPaul/django-taggit-serializer
django-taggit-serializer
# A Django field for representing time zones
# https://github.com/mfogel/django-timezone-field/
django-timezone-field
# A REST API framework for Django projects
# https://github.com/encode/django-rest-framework
djangorestframework
# Swagger/OpenAPI schema generation for REST APIs
# https://github.com/axnsan12/drf-yasg
drf-yasg[validation]
# Python interface to the graphviz graph rendering utility
# https://github.com/xflr6/graphviz
graphviz
# Simple markup language for rendering HTML
# https://github.com/Python-Markdown/markdown
# py-gfm requires Markdown<3.0
Markdown<3.0
# Library for manipulating IP prefixes and addresses
# https://github.com/drkjam/netaddr
netaddr
# Fork of PIL (Python Imaging Library) for image processing
# https://github.com/python-pillow/Pillow
Pillow
# PostgreSQL database adapter for Python
# https://github.com/psycopg/psycopg2
psycopg2-binary
# GitHub-flavored Markdown extensions
# https://github.com/zopieux/py-gfm
py-gfm
# Extensive cryptographic library (fork of pycrypto)
# https://github.com/Legrandin/pycryptodome
pycryptodome

View File

@@ -44,7 +44,7 @@ class DeviceConnectionsReport(Report):
# Check that every console port for every active device has a connection defined.
for console_port in ConsolePort.objects.select_related('device').filter(device__status=DEVICE_STATUS_ACTIVE):
if console_port.cs_port is None:
if console_port.connected_endpoint is None:
self.log_failure(
console_port.device,
"No console connection defined for {}".format(console_port.name)
@@ -63,7 +63,7 @@ class DeviceConnectionsReport(Report):
for device in Device.objects.filter(status=DEVICE_STATUS_ACTIVE):
connected_ports = 0
for power_port in PowerPort.objects.filter(device=device):
if power_port.power_outlet is not None:
if power_port.connected_endpoint is not None:
connected_ports += 1
if power_port.connection_status == CONNECTION_STATUS_PLANNED:
self.log_warning(

View File

@@ -104,7 +104,7 @@ The base serializer is used to represent the default view of a model. This inclu
}
```
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.
@@ -122,6 +122,52 @@ When a base serializer includes one or more nested serializers, the hierarchical
}
```
## Brief Format
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of the objects themselves without any related data, such as when populating a drop-down list in a form.
For example, the default (complete) format of an IP address looks like this:
```
GET /api/ipam/prefixes/13980/
{
"id": 13980,
"family": 4,
"prefix": "192.0.2.0/24",
"site": null,
"vrf": null,
"tenant": null,
"vlan": null,
"status": {
"value": 1,
"label": "Active"
},
"role": null,
"is_pool": false,
"description": "",
"tags": [],
"custom_fields": {},
"created": "2018-12-11",
"last_updated": "2018-12-11T16:27:55.073174-05:00"
}
```
The brief format is much more terse, but includes a link to the object's full representation:
```
GET /api/ipam/prefixes/13980/?brief=1
{
"id": 13980,
"url": "https://netbox/api/ipam/prefixes/13980/",
"family": 4,
"prefix": "192.0.2.0/24"
}
```
The brief format is supported for both lists and individual objects.
## Static Choice Fields
Some model fields, such as the `status` field in the above example, utilize static integers corresponding to static choices. The available choices can be retrieved from the read-only `_choices` endpoint within each app. A specific `model:field` tuple may optionally be specified in the URL.

View File

@@ -251,7 +251,7 @@ The time zone NetBox will use when dealing with dates and times. It is recommend
Default: False
Enable this option to run the webhook backend. See the docs section on the webhook backend [here](../miscellaneous/webhooks/) for more information on setup and use.
Enable this option to run the webhook backend. See the docs section on the webhook backend [here](../additional-features/webhooks/) for more information on setup and use.
---
@@ -274,7 +274,7 @@ SHORT_DATETIME_FORMAT = 'Y-m-d H:i' # 2016-06-27 13:23
## Redis Connection Settings
[Redis](https://redis.io/) is a key-value store which functions as a very lightweight database. It is required when enabling NetBox [webhooks](../miscellaneous/webhooks/). A Redis connection is configured using a dictionary similar to the following:
[Redis](https://redis.io/) is a key-value store which functions as a very lightweight database. It is required when enabling NetBox [webhooks](../additional-features/webhooks/). A Redis connection is configured using a dictionary similar to the following:
```
REDIS = {

View File

@@ -44,7 +44,11 @@ If you're adding a relational field (e.g. `ForeignKey`) and intend to include th
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal represenation of the model.
### 6. Add field to forms
### 6. Add choices to API view
If the new field has static choices, add it to the `FieldChoicesViewSet` for the app.
### 7. Add field to forms
Extend any forms to include the new field as appropriate. Common forms include:
@@ -53,18 +57,18 @@ Extend any forms to include the new field as appropriate. Common forms include:
* **CSV import** - The form used when bulk importing objects in CSV format
* **Filter** - Displays the options available for filtering a list of objects (both UI and API)
### 7. Extend object filter set
### 8. Extend object filter set
If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to reference it in the FilterSet's `search()` method.
### 8. Add column to object table
### 9. Add column to object table
If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require explicitly declaring a new column.
### 9. Update the UI templates
### 10. Update the UI templates
Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated.
### 10. Adjust API and model tests
### 11. Adjust API and model tests
Extend the model and/or API tests to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields.

View File

@@ -28,6 +28,19 @@ To invoke `pycodestyle` manually, run:
pycodestyle --ignore=W504,E501 netbox/
```
## Introducing New Dependencies
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and attacks.
If there's a strong case for introducing a new depdency, it must meet the following criteria:
* Its complete source code must be published and freely accessible without registration.
* Its license must be conducive to inclusion in an open source project.
* It must be actively maintained, with no longer than one year between releases.
* It must be available via the [Python Package Index](https://pypi.org/) (PyPI).
When adding a new dependency, a short description of the package and the URL of its code repository must be added to `base_requirements.txt`. Additionally, a line specifying the package name pinned to the current stable release must be added to `requirements.txt`. This ensures that NetBox will install only the known-good release and simplify support efforts.
## General Guidance
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point.

View File

@@ -1,7 +1,7 @@
NetBox requires a PostgreSQL database to store data. This can be hosted locally or on a remote server. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/current/static/datatype-net-types.html).)
!!! note
The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 7.4. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
!!! warning
NetBox v2.2 and later requires PostgreSQL 9.4 or higher.
@@ -19,7 +19,7 @@ If a recent enough version of PostgreSQL is not available through your distribut
**CentOS**
CentOS 7.4 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6.
CentOS 7.5 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6.
```no-highlight
# yum install https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm

View File

@@ -5,16 +5,16 @@ This section of the documentation discusses installing and configuring the NetBo
**Ubuntu**
```no-highlight
# apt-get install -y python3 python3-dev python3-setuptools build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# easy_install3 pip
# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
```
**CentOS**
```no-highlight
# yum install -y epel-release
# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
# easy_install-3.4 pip
# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
# easy_install-3.6 pip
# ln -s /usr/bin/python36 /usr/bin/python3
```
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
@@ -246,13 +246,13 @@ At this point, NetBox should be able to run. We can verify this by starting a de
Performing system checks...
System check identified no issues (0 silenced).
June 17, 2016 - 16:17:36
Django version 1.9.7, using settings 'netbox.settings'
November 28, 2018 - 09:33:45
Django version 2.0.9, using settings 'netbox.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.
```
Now if we navigate to the name or IP of the server (as defined in `ALLOWED_HOSTS`) we should be greeted with the NetBox home page. Note that this built-in web service is for development and testing purposes only. **It is not suited for production use.**
Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page. Note that this built-in web service is for development and testing purposes only. **It is not suited for production use.**
!!! warning
If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected.

View File

@@ -1,7 +1,7 @@
We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence.
!!! info
For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
For the sake of brevity, only Ubuntu 18.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
# Web Server Installation

View File

@@ -19,7 +19,7 @@ sudo yum install -y openldap-devel
## Install django-auth-ldap
```no-highlight
sudo pip install django-auth-ldap
pip3 install django-auth-ldap
```
# Configuration
@@ -95,6 +95,9 @@ AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
# Define a group required to login.
AUTH_LDAP_REQUIRE_GROUP = "CN=NETBOX_USERS,DC=example,DC=com"
# Mirror LDAP group assignments.
AUTH_LDAP_MIRROR_GROUPS = True
# Define special user types using groups. Exercise great caution when assigning superuser status.
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
"is_active": "cn=active,ou=groups,dc=example,dc=com",
@@ -113,3 +116,21 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
# Troubleshooting LDAP
`supervisorctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/supervisor/`.
For troubleshooting LDAP user/group queries, add the following lines to the start of `ldap_config.py` after `import ldap`.
```python
import logging, logging.handlers
logfile = "/opt/netbox/logs/django-ldap-debug.log"
my_logger = logging.getLogger('django_auth_ldap')
my_logger.setLevel(logging.DEBUG)
handler = logging.handlers.RotatingFileHandler(
logfile, maxBytes=1024 * 500, backupCount=5)
my_logger.addHandler(handler)
```
Ensure the file and path specified in logfile exist and are writable and executable by the application service account. Restart the netbox service and attempt to log into the site to trigger log entries to this file.

View File

@@ -11,4 +11,4 @@ The following sections detail how to set up a new instance of NetBox:
If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md).
NetBox v2.5 and later requires Python 3. Please see the instruction for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2.
NetBox v2.5 and later requires Python 3.5 or higher. Please see the instructions for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2.

View File

@@ -36,3 +36,9 @@ If using LDAP authentication, install the `django-auth-ldap` package:
```no-highlight
# pip3 install django-auth-ldap
```
If using Webhooks, install the `django-rq` package:
```no-highlight
# pip3 install django-rq
```

View File

@@ -38,6 +38,7 @@ pages:
- Change Logging: 'additional-features/change-logging.md'
- Administration:
- Replicating NetBox: 'administration/replicating-netbox.md'
- NetBox Shell: 'administration/netbox-shell.md'
- API:
- Overview: 'api/overview.md'
- Authentication: 'api/authentication.md'

View File

@@ -60,5 +60,5 @@ class CircuitTerminationSerializer(ConnectedEndpointSerializer):
model = CircuitTermination
fields = [
'id', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
'description', 'connected_endpoint', 'connection_status', 'cable',
'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
]

View File

@@ -1,5 +1,4 @@
from django import forms
from django.db.models import Count
from taggit.forms import TagField
from dcim.models import Site
@@ -7,8 +6,8 @@ from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEdit
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
AnnotatedMultipleChoiceField, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, FilterChoiceField,
SmallTextarea, SlugField,
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple
)
from .constants import CIRCUIT_STATUS_CHOICES
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -21,14 +20,22 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
class ProviderForm(BootstrapMixin, CustomFieldForm):
slug = SlugField()
comments = CommentField()
tags = TagField(required=False)
tags = TagField(
required=False
)
class Meta:
model = Provider
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags']
fields = [
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
]
widgets = {
'noc_contact': SmallTextarea(attrs={'rows': 5}),
'admin_contact': SmallTextarea(attrs={'rows': 5}),
'noc_contact': SmallTextarea(
attrs={'rows': 5}
),
'admin_contact': SmallTextarea(
attrs={'rows': 5}
),
}
help_texts = {
'name': "Full name of the provider",
@@ -54,23 +61,61 @@ class ProviderCSVForm(forms.ModelForm):
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
asn = forms.IntegerField(required=False, label='ASN')
account = forms.CharField(max_length=30, required=False, label='Account number')
portal_url = forms.URLField(required=False, label='Portal')
noc_contact = forms.CharField(required=False, widget=SmallTextarea, label='NOC contact')
admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact')
comments = CommentField(widget=SmallTextarea)
pk = forms.ModelMultipleChoiceField(
queryset=Provider.objects.all(),
widget=forms.MultipleHiddenInput
)
asn = forms.IntegerField(
required=False,
label='ASN'
)
account = forms.CharField(
max_length=30,
required=False,
label='Account number'
)
portal_url = forms.URLField(
required=False,
label='Portal'
)
noc_contact = forms.CharField(
required=False,
widget=SmallTextarea,
label='NOC contact'
)
admin_contact = forms.CharField(
required=False,
widget=SmallTextarea,
label='Admin contact'
)
comments = CommentField(
widget=SmallTextarea()
)
class Meta:
nullable_fields = ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
nullable_fields = [
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
]
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Provider
q = forms.CharField(required=False, label='Search')
site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug')
asn = forms.IntegerField(required=False, label='ASN')
q = forms.CharField(
required=False,
label='Search'
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelect(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
asn = forms.IntegerField(
required=False,
label='ASN'
)
#
@@ -82,7 +127,9 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = CircuitType
fields = ['name', 'slug']
fields = [
'name', 'slug',
]
class CircuitTypeCSVForm(forms.ModelForm):
@@ -102,7 +149,9 @@ class CircuitTypeCSVForm(forms.ModelForm):
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
comments = CommentField()
tags = TagField(required=False)
tags = TagField(
required=False
)
class Meta:
model = Circuit
@@ -115,6 +164,16 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
'install_date': "Format: YYYY-MM-DD",
'commit_rate': "Committed rate",
}
widgets = {
'provider': APISelect(
api_url="/api/circuits/providers/"
),
'type': APISelect(
api_url="/api/circuits/circuit-types/"
),
'status': StaticSelect2(),
}
class CircuitCSVForm(forms.ModelForm):
@@ -157,46 +216,105 @@ class CircuitCSVForm(forms.ModelForm):
class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
status = forms.ChoiceField(choices=add_blank_choice(CIRCUIT_STATUS_CHOICES), required=False, initial='')
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
description = forms.CharField(max_length=100, required=False)
comments = CommentField(widget=SmallTextarea)
pk = forms.ModelMultipleChoiceField(
queryset=Circuit.objects.all(),
widget=forms.MultipleHiddenInput
)
type = forms.ModelChoiceField(
queryset=CircuitType.objects.all(),
required=False,
widget=APISelect(
api_url="/api/circuits/circuit-types/"
)
)
provider = forms.ModelChoiceField(
queryset=Provider.objects.all(),
required=False,
widget=APISelect(
api_url="/api/circuits/providers/"
)
)
status = forms.ChoiceField(
choices=add_blank_choice(CIRCUIT_STATUS_CHOICES),
required=False,
initial='',
widget=StaticSelect2()
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
widget=APISelect(
api_url="/api/tenancy/tenants/"
)
)
commit_rate = forms.IntegerField(
required=False,
label='Commit rate (Kbps)'
)
description = forms.CharField(
max_length=100,
required=False
)
comments = CommentField(
widget=SmallTextarea
)
class Meta:
nullable_fields = ['tenant', 'commit_rate', 'description', 'comments']
nullable_fields = [
'tenant', 'commit_rate', 'description', 'comments',
]
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Circuit
q = forms.CharField(required=False, label='Search')
q = forms.CharField(
required=False,
label='Search'
)
type = FilterChoiceField(
queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
to_field_name='slug'
queryset=CircuitType.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/circuits/circuit-types/",
value_field="slug",
)
)
provider = FilterChoiceField(
queryset=Provider.objects.annotate(filter_count=Count('circuits')),
to_field_name='slug'
queryset=Provider.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/circuits/providers/",
value_field="slug",
)
)
status = AnnotatedMultipleChoiceField(
status = forms.MultipleChoiceField(
choices=CIRCUIT_STATUS_CHOICES,
annotate=Circuit.objects.all(),
annotate_field='status',
required=False
required=False,
widget=StaticSelect2Multiple()
)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --'
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
to_field_name='slug'
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
commit_rate = forms.IntegerField(
required=False,
min_value=0,
label='Commit rate (Kbps)'
)
commit_rate = forms.IntegerField(required=False, min_value=0, label='Commit rate (Kbps)')
#
@@ -217,4 +335,7 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
}
widgets = {
'term_side': forms.HiddenInput(),
'site': APISelect(
api_url="/api/dcim/sites/"
)
}

View File

@@ -176,7 +176,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
unique_together = ['provider', 'cid']
def __str__(self):
return '{} {}'.format(self.provider, self.cid)
return self.cid
def get_absolute_url(self):
return reverse('circuits:circuit', args=[self.pk])

View File

@@ -13,7 +13,7 @@ class ProviderTest(APITestCase):
def setUp(self):
super(ProviderTest, self).setUp()
super().setUp()
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
@@ -135,7 +135,7 @@ class CircuitTypeTest(APITestCase):
def setUp(self):
super(CircuitTypeTest, self).setUp()
super().setUp()
self.circuittype1 = CircuitType.objects.create(name='Test Circuit Type 1', slug='test-circuit-type-1')
self.circuittype2 = CircuitType.objects.create(name='Test Circuit Type 2', slug='test-circuit-type-2')
@@ -210,7 +210,7 @@ class CircuitTest(APITestCase):
def setUp(self):
super(CircuitTest, self).setUp()
super().setUp()
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
@@ -326,7 +326,7 @@ class CircuitTerminationTest(APITestCase):
def setUp(self):
super(CircuitTerminationTest, self).setUp()
super().setUp()
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')

View File

@@ -1,11 +1,12 @@
from rest_framework import serializers
from dcim.constants import CONNECTION_STATUS_CHOICES
from dcim.models import (
Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate,
Interface, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, RearPort, RearPortTemplate,
Region, Site, VirtualChassis,
)
from utilities.api import WritableNestedSerializer
from utilities.api import ChoiceField, WritableNestedSerializer
__all__ = [
'NestedCableSerializer',
@@ -149,46 +150,51 @@ class NestedDeviceSerializer(WritableNestedSerializer):
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
model = ConsoleServerPort
fields = ['id', 'url', 'device', 'name', 'cable']
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
class NestedConsolePortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
model = ConsolePort
fields = ['id', 'url', 'device', 'name', 'cable']
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
class NestedPowerOutletSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
model = PowerOutlet
fields = ['id', 'url', 'device', 'name', 'cable']
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
class NestedPowerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
model = PowerPort
fields = ['id', 'url', 'device', 'name', 'cable']
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
class NestedInterfaceSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
model = Interface
fields = ['id', 'url', 'device', 'name', 'cable']
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
class NestedRearPortSerializer(WritableNestedSerializer):

View File

@@ -23,9 +23,18 @@ from .nested_serializers import *
class ConnectedEndpointSerializer(ValidatedModelSerializer):
connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
connected_endpoint = serializers.SerializerMethodField(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
def get_connected_endpoint_type(self, obj):
if hasattr(obj, 'connected_endpoint') and obj.connected_endpoint is not None:
return '{}.{}'.format(
obj.connected_endpoint._meta.app_label,
obj.connected_endpoint._meta.model_name
)
return None
def get_connected_endpoint(self, obj):
"""
Return the appropriate serializer for the type of connected object.
@@ -58,6 +67,11 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneField(required=False)
tags = TagListSerializerField(required=False)
count_prefixes = serializers.IntegerField(read_only=True)
count_vlans = serializers.IntegerField(read_only=True)
count_racks = serializers.IntegerField(read_only=True)
count_devices = serializers.IntegerField(read_only=True)
count_circuits = serializers.IntegerField(read_only=True)
class Meta:
model = Site
@@ -121,7 +135,7 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
validator(data)
# Enforce model validation
super(RackSerializer, self).validate(data)
super().validate(data)
return data
@@ -294,7 +308,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
validator(data)
# Enforce model validation
super(DeviceSerializer, self).validate(data)
super().validate(data)
return data
@@ -331,7 +345,10 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
class Meta:
model = ConsoleServerPort
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
fields = [
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
'tags',
]
class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
@@ -341,7 +358,10 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta:
model = ConsolePort
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
fields = [
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
'tags',
]
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
@@ -351,7 +371,10 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta:
model = PowerOutlet
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
fields = [
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
'tags',
]
class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
@@ -361,7 +384,10 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta:
model = PowerPort
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
fields = [
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
'tags',
]
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
@@ -383,8 +409,8 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
model = Interface
fields = [
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
'count_ipaddresses',
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan',
'tagged_vlans', 'tags', 'count_ipaddresses',
]
# TODO: This validation should be handled by Interface.clean()
@@ -405,7 +431,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
"be global.".format(vlan)
})
return super(InterfaceSerializer, self).validate(data)
return super().validate(data)
class RearPortSerializer(ValidatedModelSerializer):

View File

@@ -35,12 +35,18 @@ from .exceptions import MissingFilterException
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(Device, ['face', 'status']),
(Cable, ['length_unit', 'status', 'type']),
(ConsolePort, ['connection_status']),
(Interface, ['connection_status', 'form_factor', 'mode']),
(Device, ['face', 'status']),
(DeviceType, ['subdevice_role']),
(FrontPort, ['type']),
(FrontPortTemplate, ['type']),
(Interface, ['form_factor', 'mode']),
(InterfaceTemplate, ['form_factor']),
(PowerPort, ['connection_status']),
(Rack, ['type', 'width']),
(Rack, ['outer_unit', 'status', 'type', 'width']),
(RearPort, ['type']),
(RearPortTemplate, ['type']),
(Site, ['status']),
)
@@ -59,7 +65,7 @@ class CableTraceMixin(object):
# Initialize the path array
path = []
for near_end, cable, far_end in obj.trace():
for near_end, cable, far_end in obj.trace(follow_circuits=True):
# Serialize each object
serializer_a = get_serializer_for_model(near_end, prefix='Nested')
@@ -483,7 +489,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
filterset_class = filters.PowerConnectionFilter
class InterfaceConnectionViewSet(ModelViewSet):
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = Interface.objects.select_related(
'device', '_connected_interface', '_connected_circuittermination'
).filter(

View File

@@ -82,18 +82,31 @@ IFACE_FF_100GE_CFP2 = 1510
IFACE_FF_100GE_CFP4 = 1520
IFACE_FF_100GE_CPAK = 1550
IFACE_FF_100GE_QSFP28 = 1600
IFACE_FF_200GE_CFP2 = 1650
IFACE_FF_200GE_QSFP56 = 1700
IFACE_FF_400GE_QSFP_DD = 1750
# Wireless
IFACE_FF_80211A = 2600
IFACE_FF_80211G = 2610
IFACE_FF_80211N = 2620
IFACE_FF_80211AC = 2630
IFACE_FF_80211AD = 2640
# SONET
IFACE_FF_SONET_OC3 = 6100
IFACE_FF_SONET_OC12 = 6200
IFACE_FF_SONET_OC48 = 6300
IFACE_FF_SONET_OC192 = 6400
IFACE_FF_SONET_OC768 = 6500
IFACE_FF_SONET_OC1920 = 6600
IFACE_FF_SONET_OC3840 = 6700
# Fibrechannel
IFACE_FF_1GFC_SFP = 3010
IFACE_FF_2GFC_SFP = 3020
IFACE_FF_4GFC_SFP = 3040
IFACE_FF_8GFC_SFP_PLUS = 3080
IFACE_FF_16GFC_SFP_PLUS = 3160
IFACE_FF_32GFC_SFP28 = 3320
IFACE_FF_128GFC_QSFP28 = 3400
# Serial
IFACE_FF_T1 = 4000
IFACE_FF_E1 = 4010
@@ -143,9 +156,12 @@ IFACE_FF_CHOICES = [
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
[IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'],
[IFACE_FF_200GE_CFP2, 'CFP2 (200GE)'],
[IFACE_FF_100GE_CFP4, 'CFP4 (100GE)'],
[IFACE_FF_100GE_CPAK, 'Cisco CPAK (100GE)'],
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
[IFACE_FF_200GE_QSFP56, 'QSFP56 (200GE)'],
[IFACE_FF_400GE_QSFP_DD, 'QSFP-DD (400GE)'],
]
],
[
@@ -158,6 +174,18 @@ IFACE_FF_CHOICES = [
[IFACE_FF_80211AD, 'IEEE 802.11ad'],
]
],
[
'SONET',
[
[IFACE_FF_SONET_OC3, 'OC-3/STM-1'],
[IFACE_FF_SONET_OC12, 'OC-12/STM-4'],
[IFACE_FF_SONET_OC48, 'OC-48/STM-16'],
[IFACE_FF_SONET_OC192, 'OC-192/STM-64'],
[IFACE_FF_SONET_OC768, 'OC-768/STM-256'],
[IFACE_FF_SONET_OC1920, 'OC-1920/STM-640'],
[IFACE_FF_SONET_OC3840, 'OC-3840/STM-1234'],
]
],
[
'FibreChannel',
[
@@ -166,6 +194,8 @@ IFACE_FF_CHOICES = [
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
[IFACE_FF_32GFC_SFP28, 'SFP28 (32GFC)'],
[IFACE_FF_128GFC_QSFP28, 'QSFP28 (128GFC)'],
]
],
[
@@ -309,11 +339,14 @@ CABLE_TYPE_CAT5E = 1510
CABLE_TYPE_CAT6 = 1600
CABLE_TYPE_CAT6A = 1610
CABLE_TYPE_CAT7 = 1700
CABLE_TYPE_DAC_ACTIVE = 1800
CABLE_TYPE_DAC_PASSIVE = 1810
CABLE_TYPE_MMF_OM1 = 3010
CABLE_TYPE_MMF_OM2 = 3020
CABLE_TYPE_MMF_OM3 = 3030
CABLE_TYPE_MMF_OM4 = 3040
CABLE_TYPE_SMF = 3500
CABLE_TYPE_AOC = 3800
CABLE_TYPE_POWER = 5000
CABLE_TYPE_CHOICES = (
(
@@ -324,6 +357,8 @@ CABLE_TYPE_CHOICES = (
(CABLE_TYPE_CAT6, 'CAT6'),
(CABLE_TYPE_CAT6A, 'CAT6a'),
(CABLE_TYPE_CAT7, 'CAT7'),
(CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
(CABLE_TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
),
),
(
@@ -333,6 +368,7 @@ CABLE_TYPE_CHOICES = (
(CABLE_TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
(CABLE_TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
(CABLE_TYPE_SMF, 'Singlemode Fiber'),
(CABLE_TYPE_AOC, 'Active Optical Cabling (AOC)'),
),
),
(CABLE_TYPE_POWER, 'Power'),
@@ -360,11 +396,11 @@ COMPATIBLE_TERMINATION_TYPES = {
'circuittermination': ['interface', 'frontport', 'rearport'],
}
LENGTH_UNIT_METER = 'm'
LENGTH_UNIT_CENTIMETER = 'cm'
LENGTH_UNIT_MILLIMETER = 'mm'
LENGTH_UNIT_FOOT = 'ft'
LENGTH_UNIT_INCH = 'in'
LENGTH_UNIT_METER = 1200
LENGTH_UNIT_CENTIMETER = 1100
LENGTH_UNIT_MILLIMETER = 1000
LENGTH_UNIT_FOOT = 2100
LENGTH_UNIT_INCH = 2000
CABLE_LENGTH_UNIT_CHOICES = (
(LENGTH_UNIT_METER, 'Meters'),
(LENGTH_UNIT_CENTIMETER, 'Centimeters'),

View File

@@ -0,0 +1,5 @@
class LoopDetected(Exception):
"""
A loop has been detected while tracing a cable path.
"""
pass

View File

@@ -7,6 +7,7 @@ from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.constants import COLOR_CHOICES
from utilities.filters import NullableCharFieldFilter, NumericInFilter, TagFilter
from virtualization.models import Cluster
from .constants import *
@@ -61,14 +62,14 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
choices=SITE_STATUS_CHOICES,
null_value=None
)
region_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(),
region_id = django_filters.NumberFilter(
method='filter_region',
field_name='pk',
label='Region (ID)',
)
region = django_filters.ModelMultipleChoiceFilter(
field_name='region__slug',
queryset=Region.objects.all(),
to_field_name='slug',
region = django_filters.CharFilter(
method='filter_region',
field_name='slug',
label='Region (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
@@ -107,6 +108,16 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
pass
return queryset.filter(qs_filter)
def filter_region(self, queryset, name, value):
try:
region = Region.objects.get(**{name: value})
except ObjectDoesNotExist:
return queryset.none()
return queryset.filter(
Q(region=region) |
Q(region__in=region.get_descendants())
)
class RackGroupFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
@@ -698,38 +709,61 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
class ConsolePortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta:
model = ConsolePort
fields = ['name']
fields = ['name', 'connection_status']
class ConsoleServerPortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta:
model = ConsoleServerPort
fields = ['name']
fields = ['name', 'connection_status']
class PowerPortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta:
model = PowerPort
fields = ['name']
fields = ['name', 'connection_status']
class PowerOutletFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta:
model = PowerOutlet
fields = ['name']
fields = ['name', 'connection_status']
class InterfaceFilter(django_filters.FilterSet):
"""
Not using DeviceComponentFilterSet for Interfaces because we need to glean the ordering logic from the parent
Device's DeviceType.
Not using DeviceComponentFilterSet for Interfaces because we need to check for VirtualChassis membership.
"""
q = django_filters.CharFilter(
method='search',
label='Search',
)
device = django_filters.CharFilter(
method='filter_device',
field_name='name',
@@ -740,6 +774,11 @@ class InterfaceFilter(django_filters.FilterSet):
field_name='pk',
label='Device (ID)',
)
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
type = django_filters.CharFilter(
method='filter_type',
label='Interface type',
@@ -762,15 +801,26 @@ class InterfaceFilter(django_filters.FilterSet):
method='filter_vlan',
label='Assigned VID'
)
form_factor = django_filters.MultipleChoiceFilter(
choices=IFACE_FF_CHOICES,
null_value=None
)
class Meta:
model = Interface
fields = ['name', 'form_factor', 'enabled', 'mtu', 'mgmt_only']
fields = ['name', 'connection_status', 'form_factor', 'enabled', 'mtu', 'mgmt_only']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value)
).distinct()
def filter_device(self, queryset, name, value):
try:
device = Device.objects.get(**{name: value})
vc_interface_ids = [i['id'] for i in device.vc_interfaces.values('id')]
vc_interface_ids = device.vc_interfaces.values_list('id', flat=True)
return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist:
return queryset.none()
@@ -814,6 +864,11 @@ class InterfaceFilter(django_filters.FilterSet):
class FrontPortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta:
model = FrontPort
@@ -821,6 +876,11 @@ class FrontPortFilter(DeviceComponentFilterSet):
class RearPortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta:
model = RearPort
@@ -929,6 +989,12 @@ class CableFilter(django_filters.FilterSet):
method='search',
label='Search',
)
type = django_filters.MultipleChoiceFilter(
choices=CABLE_TYPE_CHOICES
)
color = django_filters.MultipleChoiceFilter(
choices=COLOR_CHOICES
)
class Meta:
model = Cable

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ class DeviceComponentManager(Manager):
def get_queryset(self):
queryset = super(DeviceComponentManager, self).get_queryset()
queryset = super().get_queryset()
table_name = self.model._meta.db_table
sql = r"CONCAT(REGEXP_REPLACE({}.name, '\d+$', ''), LPAD(SUBSTRING({}.name FROM '\d+$'), 8, '0'))"

View File

@@ -19,11 +19,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
),
]

View File

@@ -46,6 +46,9 @@ def console_connections_to_cables(apps, schema_editor):
if 'test' not in sys.argv:
print("{} cables created".format(cable_count))
# Normalize connection_status for all non-connected ConsolePorts
ConsolePort.objects.filter(connected_endpoint__isnull=True).update(connection_status=None)
def power_connections_to_cables(apps, schema_editor):
"""
@@ -87,6 +90,9 @@ def power_connections_to_cables(apps, schema_editor):
if 'test' not in sys.argv:
print("{} cables created".format(cable_count))
# Normalize connection_status for all non-connected PowerPorts
PowerPort.objects.filter(connected_endpoint__isnull=True).update(connection_status=None)
def interface_connections_to_cables(apps, schema_editor):
"""
@@ -131,6 +137,15 @@ def interface_connections_to_cables(apps, schema_editor):
print("{} cables created".format(cable_count))
def delete_interfaceconnection_content_type(apps, schema_editor):
"""
Delete the ContentType for the InterfaceConnection model. (This is not done automatically upon model deletion.)
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
InterfaceConnection = apps.get_model('dcim', 'InterfaceConnection')
ContentType.objects.get_for_model(InterfaceConnection).delete()
class Migration(migrations.Migration):
atomic = False
@@ -157,7 +172,7 @@ class Migration(migrations.Migration):
('label', models.CharField(blank=True, max_length=100)),
('color', utilities.fields.ColorField(blank=True, max_length=6)),
('length', models.PositiveSmallIntegerField(blank=True, null=True)),
('length_unit', models.CharField(blank=True, max_length=2)),
('length_unit', models.PositiveSmallIntegerField(blank=True, null=True)),
('_abs_length', models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True)),
('termination_a_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
('termination_b_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
@@ -291,7 +306,8 @@ class Migration(migrations.Migration):
migrations.RunPython(power_connections_to_cables),
migrations.RunPython(interface_connections_to_cables),
# Delete the InterfaceConnection model
# Delete the InterfaceConnection model and its ContentType
migrations.RunPython(delete_interfaceconnection_content_type),
migrations.RemoveField(
model_name='interfaceconnection',
name='interface_a',
@@ -303,36 +319,4 @@ class Migration(migrations.Migration):
migrations.DeleteModel(
name='InterfaceConnection',
),
# Proxy models
migrations.CreateModel(
name='ConsoleConnection',
fields=[
],
options={
'proxy': True,
'indexes': [],
},
bases=('dcim.consoleport',),
),
migrations.CreateModel(
name='InterfaceConnection',
fields=[
],
options={
'proxy': True,
'indexes': [],
},
bases=('dcim.interface',),
),
migrations.CreateModel(
name='PowerConnection',
fields=[
],
options={
'proxy': True,
'indexes': [],
},
bases=('dcim.powerport',),
),
]

View File

@@ -28,7 +28,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='rack',
name='outer_unit',
field=models.CharField(blank=True, max_length=2),
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='rack',

View File

@@ -21,6 +21,7 @@ from utilities.managers import NaturalOrderingManager
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object, to_meters
from .constants import *
from .exceptions import LoopDetected
from .fields import ASNField, MACAddressField
from .managers import DeviceComponentManager, InterfaceManager
@@ -53,7 +54,11 @@ class ComponentModel(models.Model):
"""
Log an ObjectChange including the parent Device/VM.
"""
parent = self.device if self.device is not None else getattr(self, 'virtual_machine', None)
try:
parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None)
except ObjectDoesNotExist:
# The parent device/VM has already been deleted
parent = None
ObjectChange(
user=user,
request_id=request_id,
@@ -88,7 +93,7 @@ class CableTermination(models.Model):
class Meta:
abstract = True
def trace(self, position=1, follow_circuits=False):
def trace(self, position=1, follow_circuits=False, cable_history=None):
"""
Return a list representing a complete cable path, with each individual segment represented as a three-tuple:
[
@@ -110,11 +115,14 @@ class CableTermination(models.Model):
raise Exception("Invalid position for {} ({} positions): {})".format(
termination, termination.positions, position
))
peer_port = FrontPort.objects.get(
rear_port=termination,
rear_port_position=position,
)
return peer_port, 1
try:
peer_port = FrontPort.objects.get(
rear_port=termination,
rear_port_position=position,
)
return peer_port, 1
except ObjectDoesNotExist:
return None, None
# Follow a circuit to its other termination
elif isinstance(termination, CircuitTermination) and follow_circuits:
@@ -130,6 +138,13 @@ class CableTermination(models.Model):
if not self.cable:
return [(self, None, None)]
# Record cable history to detect loops
if cable_history is None:
cable_history = []
elif self.cable in cable_history:
raise LoopDetected()
cable_history.append(self.cable)
far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a
path = [(self, self.cable, far_end)]
@@ -137,7 +152,11 @@ class CableTermination(models.Model):
if peer_port is None:
return path
next_segment = peer_port.trace(position)
try:
next_segment = peer_port.trace(position, follow_circuits, cable_history)
except LoopDetected:
return path
if next_segment is None:
return path + [(peer_port, None, None)]
@@ -186,6 +205,13 @@ class Region(MPTTModel, ChangeLoggedModel):
self.parent.name if self.parent else None,
)
@property
def site_count(self):
return Site.objects.filter(
Q(region=self) |
Q(region__in=self.get_descendants())
).count()
#
# Sites
@@ -511,10 +537,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
blank=True,
null=True
)
outer_unit = models.CharField(
outer_unit = models.PositiveSmallIntegerField(
choices=RACK_DIMENSION_UNIT_CHOICES,
max_length=2,
blank=True
blank=True,
null=True
)
comments = models.TextField(
blank=True
@@ -544,7 +570,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
]
def __str__(self):
return self.display_name or super(Rack, self).__str__()
return self.display_name or super().__str__()
def get_absolute_url(self):
return reverse('dcim:rack', args=[self.pk])
@@ -552,10 +578,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
def clean(self):
# Validate outer dimensions and unit
if (self.outer_width or self.outer_depth) and not self.outer_unit:
if (self.outer_width is not None or self.outer_depth is not None) and self.outer_unit is None:
raise ValidationError("Must specify a unit when setting an outer width/depth")
else:
self.outer_unit = ''
elif self.outer_width is None and self.outer_depth is None:
self.outer_unit = None
if self.pk:
# Validate that Rack is tall enough to house the installed Devices
@@ -582,7 +608,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
if self.pk:
_site_id = Rack.objects.get(pk=self.pk).site_id
super(Rack, self).save(*args, **kwargs)
super().save(*args, **kwargs)
# Update racked devices if the assigned Site has been changed.
if _site_id is not None and self.site_id != _site_id:
@@ -894,7 +920,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
return self.model
def __init__(self, *args, **kwargs):
super(DeviceType, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
# Save a copy of u_height for validation in clean()
self._original_u_height = self.u_height
@@ -1437,7 +1463,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
)
def __str__(self):
return self.display_name or super(Device, self).__str__()
return self.display_name or super().__str__()
def get_absolute_url(self):
return reverse('dcim:device', args=[self.pk])
@@ -1552,7 +1578,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
is_new = not bool(self.pk)
super(Device, self).save(*args, **kwargs)
super().save(*args, **kwargs)
# If this is a new Device, instantiate all of the related components per the DeviceType definition
if is_new:
@@ -2055,7 +2081,7 @@ class Interface(CableTermination, ComponentModel):
if self.pk and self.mode is not IFACE_MODE_TAGGED:
self.tagged_vlans.clear()
return super(Interface, self).save(*args, **kwargs)
return super().save(*args, **kwargs)
def log_change(self, user, request_id, action):
"""
@@ -2495,10 +2521,10 @@ class Cable(ChangeLoggedModel):
blank=True,
null=True
)
length_unit = models.CharField(
length_unit = models.PositiveSmallIntegerField(
choices=CABLE_LENGTH_UNIT_CHOICES,
max_length=2,
blank=True
blank=True,
null=True
)
# Stores the normalized length (in meters) for database ordering
_abs_length = models.DecimalField(
@@ -2522,7 +2548,7 @@ class Cable(ChangeLoggedModel):
def __init__(self, *args, **kwargs):
super(Cable, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
# Create an ID string for use by __str__(). We have to save a copy of pk since it's nullified after .delete()
# is called.
@@ -2536,58 +2562,61 @@ class Cable(ChangeLoggedModel):
def clean(self):
# Check that termination types are compatible
type_a = self.termination_a_type.model
type_b = self.termination_b_type.model
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
raise ValidationError("Incompatible termination types: {} and {}".format(
self.termination_a_type, self.termination_b_type
))
if self.termination_a and self.termination_b:
# A termination point cannot be connected to itself
if self.termination_a == self.termination_b:
raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
type_a = self.termination_a_type.model
type_b = self.termination_b_type.model
# A front port cannot be connected to its corresponding rear port
if (
type_a in ['frontport', 'rearport'] and
type_b in ['frontport', 'rearport'] and
(
getattr(self.termination_a, 'rear_port', None) == self.termination_b or
getattr(self.termination_b, 'rear_port', None) == self.termination_a
)
):
raise ValidationError("A front port cannot be connected to it corresponding rear port")
# Check that termination types are compatible
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
raise ValidationError("Incompatible termination types: {} and {}".format(
self.termination_a_type, self.termination_b_type
))
# Check for an existing Cable connected to either termination object
if self.termination_a.cable not in (None, self):
raise ValidationError("{} already has a cable attached (#{})".format(
self.termination_a, self.termination_a.cable_id
))
if self.termination_b.cable not in (None, self):
raise ValidationError("{} already has a cable attached (#{})".format(
self.termination_b, self.termination_b.cable_id
))
# A termination point cannot be connected to itself
if self.termination_a == self.termination_b:
raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
# Virtual interfaces cannot be connected
endpoint_a, endpoint_b, _ = self.get_path_endpoints()
if (
(
isinstance(endpoint_a, Interface) and
endpoint_a.form_factor == IFACE_FF_VIRTUAL
) or
(
isinstance(endpoint_b, Interface) and
endpoint_b.form_factor == IFACE_FF_VIRTUAL
)
):
raise ValidationError("Cannot connect to a virtual interface")
# A front port cannot be connected to its corresponding rear port
if (
type_a in ['frontport', 'rearport'] and
type_b in ['frontport', 'rearport'] and
(
getattr(self.termination_a, 'rear_port', None) == self.termination_b or
getattr(self.termination_b, 'rear_port', None) == self.termination_a
)
):
raise ValidationError("A front port cannot be connected to it corresponding rear port")
# Check for an existing Cable connected to either termination object
if self.termination_a.cable not in (None, self):
raise ValidationError("{} already has a cable attached (#{})".format(
self.termination_a, self.termination_a.cable_id
))
if self.termination_b.cable not in (None, self):
raise ValidationError("{} already has a cable attached (#{})".format(
self.termination_b, self.termination_b.cable_id
))
# Virtual interfaces cannot be connected
endpoint_a, endpoint_b, _ = self.get_path_endpoints()
if (
(
isinstance(endpoint_a, Interface) and
endpoint_a.form_factor == IFACE_FF_VIRTUAL
) or
(
isinstance(endpoint_b, Interface) and
endpoint_b.form_factor == IFACE_FF_VIRTUAL
)
):
raise ValidationError("Cannot connect to a virtual interface")
# Validate length and length_unit
if self.length and not self.length_unit:
if self.length is not None and self.length_unit is None:
raise ValidationError("Must specify a unit when setting a cable length")
if self.length_unit and self.length is None:
self.length_unit = ''
elif self.length is None:
self.length_unit = None
def save(self, *args, **kwargs):
@@ -2595,7 +2624,7 @@ class Cable(ChangeLoggedModel):
if self.length and self.length_unit:
self._abs_length = to_meters(self.length, self.length_unit)
super(Cable, self).save(*args, **kwargs)
super().save(*args, **kwargs)
def to_csv(self):
return (
@@ -2629,66 +2658,7 @@ class Cable(ChangeLoggedModel):
path_status = CONNECTION_STATUS_PLANNED
break
# (A path end, B path end, connected/planned)
return a_path[-1][2], b_path[-1][2], path_status
a_endpoint = a_path[-1][2]
b_endpoint = b_path[-1][2]
#
# Connection proxy models
#
class ConsoleConnection(ConsolePort):
csv_headers = [
'console_server', 'port', 'device', 'console_port', 'connection_status',
]
class Meta:
proxy = True
def to_csv(self):
return (
self.connected_endpoint.device.identifier if self.connected_endpoint else None,
self.connected_endpoint.name if self.connected_endpoint else None,
self.device.identifier,
self.name,
self.get_connection_status_display(),
)
class PowerConnection(PowerPort):
csv_headers = [
'pdu', 'outlet', 'device', 'power_port', 'connection_status',
]
class Meta:
proxy = True
def to_csv(self):
return (
self.connected_endpoint.device.identifier if self.connected_endpoint else None,
self.connected_endpoint.name if self.connected_endpoint else None,
self.device.identifier,
self.name,
self.get_connection_status_display(),
)
class InterfaceConnection(Interface):
csv_headers = [
'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status',
]
class Meta:
proxy = True
def to_csv(self):
return (
self.connected_endpoint.device.identifier if self.connected_endpoint else None,
self.connected_endpoint.name if self.connected_endpoint else None,
self.device.identifier,
self.name,
self.get_connection_status_display(),
)
return a_endpoint, b_endpoint, path_status

View File

@@ -62,7 +62,7 @@ def nullify_connected_endpoints(instance, **kwargs):
instance.termination_b.save()
# If this Cable was part of a complete path, tear it down
if endpoint_a is not None and endpoint_b is not None:
if hasattr(endpoint_a, 'connected_endpoint') and hasattr(endpoint_b, 'connected_endpoint'):
endpoint_a.connected_endpoint = None
endpoint_a.connection_status = None
endpoint_a.save()

View File

@@ -4,11 +4,10 @@ from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
from .models import (
Cable, ConsoleConnection, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceConnection,
InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerConnection, PowerOutlet, PowerOutletTemplate,
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
REGION_LINK = """
@@ -30,7 +29,8 @@ SITE_REGION_LINK = """
"""
COLOR_LABEL = """
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
{% load helpers %}
<label class="label" style="color: {{ record.color|fgcolor }}; background-color: #{{ record.color }}">{{ record }}</label>
"""
DEVICE_LINK = """
@@ -180,7 +180,7 @@ CABLE_TERMINATION_PARENT = """
"""
CABLE_LENGTH = """
{% if record.length %}{{ record.length }}{{ record.length_unit }}{% else %}&mdash;{% endif %}
{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% else %}&mdash;{% endif %}
"""
@@ -683,7 +683,7 @@ class ConsoleConnectionTable(BaseTable):
)
class Meta(BaseTable.Meta):
model = ConsoleConnection
model = ConsolePort
fields = ('console_server', 'connected_endpoint', 'device', 'name', 'connection_status')
@@ -706,7 +706,7 @@ class PowerConnectionTable(BaseTable):
)
class Meta(BaseTable.Meta):
model = PowerConnection
model = PowerPort
fields = ('pdu', 'connected_endpoint', 'device', 'name', 'connection_status')
@@ -745,7 +745,7 @@ class InterfaceConnectionTable(BaseTable):
)
class Meta(BaseTable.Meta):
model = InterfaceConnection
model = Interface
fields = (
'device_a', 'interface_a', 'description_a', 'device_b', 'interface_b', 'description_b', 'connection_status',
)

View File

@@ -20,7 +20,7 @@ class RegionTest(APITestCase):
def setUp(self):
super(RegionTest, self).setUp()
super().setUp()
self.region1 = Region.objects.create(name='Test Region 1', slug='test-region-1')
self.region2 = Region.objects.create(name='Test Region 2', slug='test-region-2')
@@ -121,7 +121,7 @@ class SiteTest(APITestCase):
def setUp(self):
super(SiteTest, self).setUp()
super().setUp()
self.region1 = Region.objects.create(name='Test Region 1', slug='test-region-1')
self.region2 = Region.objects.create(name='Test Region 2', slug='test-region-2')
@@ -256,7 +256,7 @@ class RackGroupTest(APITestCase):
def setUp(self):
super(RackGroupTest, self).setUp()
super().setUp()
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
@@ -366,7 +366,7 @@ class RackRoleTest(APITestCase):
def setUp(self):
super(RackRoleTest, self).setUp()
super().setUp()
self.rackrole1 = RackRole.objects.create(name='Test Rack Role 1', slug='test-rack-role-1', color='ff0000')
self.rackrole2 = RackRole.objects.create(name='Test Rack Role 2', slug='test-rack-role-2', color='00ff00')
@@ -474,7 +474,7 @@ class RackTest(APITestCase):
def setUp(self):
super(RackTest, self).setUp()
super().setUp()
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
@@ -608,7 +608,7 @@ class RackReservationTest(APITestCase):
def setUp(self):
super(RackReservationTest, self).setUp()
super().setUp()
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.rack1 = Rack.objects.create(site=self.site1, name='Test Rack 1')
@@ -719,7 +719,7 @@ class ManufacturerTest(APITestCase):
def setUp(self):
super(ManufacturerTest, self).setUp()
super().setUp()
self.manufacturer1 = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
self.manufacturer2 = Manufacturer.objects.create(name='Test Manufacturer 2', slug='test-manufacturer-2')
@@ -820,7 +820,7 @@ class DeviceTypeTest(APITestCase):
def setUp(self):
super(DeviceTypeTest, self).setUp()
super().setUp()
self.manufacturer1 = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
self.manufacturer2 = Manufacturer.objects.create(name='Test Manufacturer 2', slug='test-manufacturer-2')
@@ -936,7 +936,7 @@ class ConsolePortTemplateTest(APITestCase):
def setUp(self):
super(ConsolePortTemplateTest, self).setUp()
super().setUp()
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
self.devicetype = DeviceType.objects.create(
@@ -1036,7 +1036,7 @@ class ConsoleServerPortTemplateTest(APITestCase):
def setUp(self):
super(ConsoleServerPortTemplateTest, self).setUp()
super().setUp()
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
self.devicetype = DeviceType.objects.create(
@@ -1136,7 +1136,7 @@ class PowerPortTemplateTest(APITestCase):
def setUp(self):
super(PowerPortTemplateTest, self).setUp()
super().setUp()
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
self.devicetype = DeviceType.objects.create(
@@ -1236,7 +1236,7 @@ class PowerOutletTemplateTest(APITestCase):
def setUp(self):
super(PowerOutletTemplateTest, self).setUp()
super().setUp()
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
self.devicetype = DeviceType.objects.create(
@@ -1336,7 +1336,7 @@ class InterfaceTemplateTest(APITestCase):
def setUp(self):
super(InterfaceTemplateTest, self).setUp()
super().setUp()
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
self.devicetype = DeviceType.objects.create(
@@ -1436,7 +1436,7 @@ class DeviceBayTemplateTest(APITestCase):
def setUp(self):
super(DeviceBayTemplateTest, self).setUp()
super().setUp()
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
self.devicetype = DeviceType.objects.create(
@@ -1536,7 +1536,7 @@ class DeviceRoleTest(APITestCase):
def setUp(self):
super(DeviceRoleTest, self).setUp()
super().setUp()
self.devicerole1 = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@@ -1650,7 +1650,7 @@ class PlatformTest(APITestCase):
def setUp(self):
super(PlatformTest, self).setUp()
super().setUp()
self.platform1 = Platform.objects.create(name='Test Platform 1', slug='test-platform-1')
self.platform2 = Platform.objects.create(name='Test Platform 2', slug='test-platform-2')
@@ -1751,7 +1751,7 @@ class DeviceTest(APITestCase):
def setUp(self):
super(DeviceTest, self).setUp()
super().setUp()
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
@@ -1913,7 +1913,7 @@ class ConsolePortTest(APITestCase):
def setUp(self):
super(ConsolePortTest, self).setUp()
super().setUp()
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
@@ -1951,7 +1951,7 @@ class ConsolePortTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['cable', 'device', 'id', 'name', 'url']
['cable', 'connection_status', 'device', 'id', 'name', 'url']
)
def test_create_consoleport(self):
@@ -2026,7 +2026,7 @@ class ConsoleServerPortTest(APITestCase):
def setUp(self):
super(ConsoleServerPortTest, self).setUp()
super().setUp()
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
@@ -2064,7 +2064,7 @@ class ConsoleServerPortTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['cable', 'device', 'id', 'name', 'url']
['cable', 'connection_status', 'device', 'id', 'name', 'url']
)
def test_create_consoleserverport(self):
@@ -2137,7 +2137,7 @@ class PowerPortTest(APITestCase):
def setUp(self):
super(PowerPortTest, self).setUp()
super().setUp()
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
@@ -2175,7 +2175,7 @@ class PowerPortTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['cable', 'device', 'id', 'name', 'url']
['cable', 'connection_status', 'device', 'id', 'name', 'url']
)
def test_create_powerport(self):
@@ -2250,7 +2250,7 @@ class PowerOutletTest(APITestCase):
def setUp(self):
super(PowerOutletTest, self).setUp()
super().setUp()
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
@@ -2288,7 +2288,7 @@ class PowerOutletTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['cable', 'device', 'id', 'name', 'url']
['cable', 'connection_status', 'device', 'id', 'name', 'url']
)
def test_create_poweroutlet(self):
@@ -2361,7 +2361,7 @@ class InterfaceTest(APITestCase):
def setUp(self):
super(InterfaceTest, self).setUp()
super().setUp()
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
@@ -2425,7 +2425,7 @@ class InterfaceTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['cable', 'device', 'id', 'name', 'url']
['cable', 'connection_status', 'device', 'id', 'name', 'url']
)
def test_create_interface(self):
@@ -2560,7 +2560,7 @@ class DeviceBayTest(APITestCase):
def setUp(self):
super(DeviceBayTest, self).setUp()
super().setUp()
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
@@ -2683,7 +2683,7 @@ class InventoryItemTest(APITestCase):
def setUp(self):
super(InventoryItemTest, self).setUp()
super().setUp()
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
@@ -2799,7 +2799,7 @@ class CableTest(APITestCase):
def setUp(self):
super(CableTest, self).setUp()
super().setUp()
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
@@ -2940,7 +2940,7 @@ class ConnectionTest(APITestCase):
def setUp(self):
super(ConnectionTest, self).setUp()
super().setUp()
self.site = Site.objects.create(
name='Test Site 1', slug='test-site-1'
@@ -3304,7 +3304,7 @@ class ConnectedDeviceTest(APITestCase):
def setUp(self):
super(ConnectedDeviceTest, self).setUp()
super().setUp()
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
@@ -3346,7 +3346,7 @@ class VirtualChassisTest(APITestCase):
def setUp(self):
super(VirtualChassisTest, self).setUp()
super().setUp()
site = Site.objects.create(name='Test Site', slug='test-site')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer', slug='test-manufacturer')

View File

@@ -1,3 +1,5 @@
import re
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.paginator import EmptyPage, PageNotAnInteger
@@ -17,6 +19,7 @@ from ipam.models import Prefix, VLAN
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
from utilities.utils import csv_format
from utilities.views import (
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
ObjectDeleteView, ObjectEditView, ObjectListView,
@@ -24,11 +27,10 @@ from utilities.views import (
from virtualization.models import VirtualMachine
from . import filters, forms, tables
from .models import (
Cable, ConsoleConnection, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceConnection,
InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerConnection, PowerOutlet, PowerOutletTemplate,
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
@@ -50,7 +52,16 @@ class BulkRenameView(GetReturnURLMixin, View):
if form.is_valid():
for obj in selected_objects:
obj.new_name = obj.name.replace(form.cleaned_data['find'], form.cleaned_data['replace'])
find = form.cleaned_data['find']
replace = form.cleaned_data['replace']
if form.cleaned_data['use_regex']:
try:
obj.new_name = re.sub(find, replace, obj.name)
# Catch regex group reference errors
except re.error:
obj.new_name = obj.name
else:
obj.new_name = obj.name.replace(find, replace)
if '_apply' in request.POST:
for obj in selected_objects:
@@ -124,7 +135,7 @@ class BulkDisconnectView(GetReturnURLMixin, View):
#
class RegionListView(ObjectListView):
queryset = Region.objects.annotate(site_count=Count('sites'))
queryset = Region.objects.all()
filter = filters.RegionFilter
filter_form = forms.RegionFilterForm
table = tables.RegionTable
@@ -151,7 +162,7 @@ class RegionBulkImportView(PermissionRequiredMixin, BulkImportView):
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_region'
queryset = Region.objects.annotate(site_count=Count('sites'))
queryset = Region.objects.all()
filter = filters.RegionFilter
table = tables.RegionTable
default_return_url = 'dcim:region_list'
@@ -1530,6 +1541,7 @@ class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateV
form = forms.DeviceBulkAddComponentForm
model = ConsolePort
model_form = forms.ConsolePortForm
filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
@@ -1541,6 +1553,7 @@ class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentC
form = forms.DeviceBulkAddComponentForm
model = ConsoleServerPort
model_form = forms.ConsoleServerPortForm
filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
@@ -1552,6 +1565,7 @@ class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateVie
form = forms.DeviceBulkAddComponentForm
model = PowerPort
model_form = forms.PowerPortForm
filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
@@ -1563,6 +1577,7 @@ class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateV
form = forms.DeviceBulkAddComponentForm
model = PowerOutlet
model_form = forms.PowerOutletForm
filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
@@ -1574,6 +1589,7 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie
form = forms.DeviceBulkAddInterfaceForm
model = Interface
model_form = forms.InterfaceForm
filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
@@ -1585,6 +1601,7 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie
form = forms.DeviceBulkAddComponentForm
model = DeviceBay
model_form = forms.DeviceBayForm
filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
@@ -1688,7 +1705,7 @@ class CableBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class ConsoleConnectionsListView(ObjectListView):
queryset = ConsoleConnection.objects.select_related(
queryset = ConsolePort.objects.select_related(
'device', 'connected_endpoint__device'
).filter(
connected_endpoint__isnull=False
@@ -1700,9 +1717,25 @@ class ConsoleConnectionsListView(ObjectListView):
table = tables.ConsoleConnectionTable
template_name = 'dcim/console_connections_list.html'
def queryset_to_csv(self):
csv_data = [
# Headers
','.join(['console_server', 'port', 'device', 'console_port', 'connection_status'])
]
for obj in self.queryset:
csv = csv_format([
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
obj.connected_endpoint.name if obj.connected_endpoint else None,
obj.device.identifier,
obj.name,
obj.get_connection_status_display(),
])
csv_data.append(csv)
return csv_data
class PowerConnectionsListView(ObjectListView):
queryset = PowerConnection.objects.select_related(
queryset = PowerPort.objects.select_related(
'device', 'connected_endpoint__device'
).filter(
connected_endpoint__isnull=False
@@ -1714,9 +1747,25 @@ class PowerConnectionsListView(ObjectListView):
table = tables.PowerConnectionTable
template_name = 'dcim/power_connections_list.html'
def queryset_to_csv(self):
csv_data = [
# Headers
','.join(['pdu', 'outlet', 'device', 'power_port', 'connection_status'])
]
for obj in self.queryset:
csv = csv_format([
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
obj.connected_endpoint.name if obj.connected_endpoint else None,
obj.device.identifier,
obj.name,
obj.get_connection_status_display(),
])
csv_data.append(csv)
return csv_data
class InterfaceConnectionsListView(ObjectListView):
queryset = InterfaceConnection.objects.select_related(
queryset = Interface.objects.select_related(
'device', 'cable', '_connected_interface__device'
).filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair
@@ -1730,6 +1779,28 @@ class InterfaceConnectionsListView(ObjectListView):
table = tables.InterfaceConnectionTable
template_name = 'dcim/interface_connections_list.html'
def queryset_to_csv(self):
csv_data = [
# Headers
','.join([
'device_a', 'interface_a', 'interface_a_description',
'device_b', 'interface_b', 'interface_b_description',
'connection_status'
])
]
for obj in self.queryset:
csv = csv_format([
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
obj.connected_endpoint.name if obj.connected_endpoint else None,
obj.connected_endpoint.description if obj.connected_endpoint else None,
obj.device.identifier,
obj.name,
obj.description,
obj.get_connection_status_display(),
])
csv_data.append(csv)
return csv_data
#
# Inventory items

View File

@@ -28,9 +28,10 @@ class WebhookForm(forms.ModelForm):
exclude = []
def __init__(self, *args, **kwargs):
super(WebhookForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
order_content_types(self.fields['obj_type'])
if 'obj_type' in self.fields:
order_content_types(self.fields['obj_type'])
@admin.register(Webhook, site=admin_site)
@@ -56,7 +57,7 @@ class CustomFieldForm(forms.ModelForm):
exclude = []
def __init__(self, *args, **kwargs):
super(CustomFieldForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
order_content_types(self.fields['obj_type'])
@@ -96,7 +97,7 @@ class ExportTemplateForm(forms.ModelForm):
exclude = []
def __init__(self, *args, **kwargs):
super(ExportTemplateForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
# Format ContentType choices
order_content_types(self.fields['content_type'])

View File

@@ -105,7 +105,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
custom_fields[cfv.field.name] = cfv.value
instance.custom_fields = custom_fields
super(CustomFieldModelSerializer, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
if self.instance is not None:
@@ -137,7 +137,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
with transaction.atomic():
instance = super(CustomFieldModelSerializer, self).create(validated_data)
instance = super().create(validated_data)
# Save custom fields
if custom_fields is not None:
@@ -152,7 +152,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
with transaction.atomic():
instance = super(CustomFieldModelSerializer, self).update(instance, validated_data)
instance = super().update(instance, validated_data)
# Save custom fields
if custom_fields is not None:

View File

@@ -108,7 +108,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
)
# Enforce model validation
super(ImageAttachmentSerializer, self).validate(data)
super().validate(data)
return data

View File

@@ -50,7 +50,7 @@ class CustomFieldModelViewSet(ModelViewSet):
custom_field_choices[cfc.id] = cfc.value
custom_field_choices = custom_field_choices
context = super(CustomFieldModelViewSet, self).get_serializer_context()
context = super().get_serializer_context()
context.update({
'custom_fields': custom_fields,
'custom_field_choices': custom_field_choices,
@@ -59,7 +59,7 @@ class CustomFieldModelViewSet(ModelViewSet):
def get_queryset(self):
# Prefetch custom field values
return super(CustomFieldModelViewSet, self).get_queryset().prefetch_related('custom_field_values__field')
return super().get_queryset().prefetch_related('custom_field_values__field')
#
@@ -99,10 +99,9 @@ class TopologyMapViewSet(ModelViewSet):
try:
data = tmap.render(img_format=img_format)
except Exception:
except Exception as e:
return HttpResponse(
"There was an error generating the requested graph. Ensure that the GraphViz executables have been "
"installed correctly."
"There was an error generating the requested graph: %s" % e
)
response = HttpResponse(data, content_type='image/{}'.format(img_format))

View File

@@ -17,7 +17,7 @@ class CustomFieldFilter(django_filters.Filter):
def __init__(self, custom_field, *args, **kwargs):
self.cf_type = custom_field.type
self.filter_logic = custom_field.filter_logic
super(CustomFieldFilter, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
def filter(self, queryset, value):
@@ -31,12 +31,12 @@ class CustomFieldFilter(django_filters.Filter):
# Treat 0 as None
if int(value) == 0:
return queryset.exclude(
custom_field_values__field__name=self.name,
custom_field_values__field__name=self.field_name,
)
# Match on exact CustomFieldChoice PK
else:
return queryset.filter(
custom_field_values__field__name=self.name,
custom_field_values__field__name=self.field_name,
custom_field_values__serialized_value=value,
)
except ValueError:
@@ -45,12 +45,12 @@ class CustomFieldFilter(django_filters.Filter):
# Apply the assigned filter logic (exact or loose)
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
queryset = queryset.filter(
custom_field_values__field__name=self.name,
custom_field_values__field__name=self.field_name,
custom_field_values__serialized_value=value
)
else:
queryset = queryset.filter(
custom_field_values__field__name=self.name,
custom_field_values__field__name=self.field_name,
custom_field_values__serialized_value__icontains=value
)
@@ -63,7 +63,7 @@ class CustomFieldFilterSet(django_filters.FilterSet):
"""
def __init__(self, *args, **kwargs):
super(CustomFieldFilterSet, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
obj_type = ContentType.objects.get_for_model(self._meta.model)
custom_fields = CustomField.objects.filter(obj_type=obj_type).exclude(filter_logic=CF_FILTER_DISABLED)

View File

@@ -11,7 +11,7 @@ from taggit.models import Tag
from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, FilterChoiceField,
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, FilterChoiceField,
FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
)
from .constants import (
@@ -102,7 +102,7 @@ class CustomFieldForm(forms.ModelForm):
self.custom_fields = []
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
super(CustomFieldForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
# Add all applicable CustomFields to the form
custom_fields = []
@@ -138,7 +138,7 @@ class CustomFieldForm(forms.ModelForm):
cfv.save()
def save(self, commit=True):
obj = super(CustomFieldForm, self).save(commit)
obj = super().save(commit)
# Handle custom fields the same way we do M2M fields
if commit:
@@ -152,7 +152,7 @@ class CustomFieldForm(forms.ModelForm):
class CustomFieldBulkEditForm(BulkEditForm):
def __init__(self, *args, **kwargs):
super(CustomFieldBulkEditForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.custom_fields = []
self.obj_type = ContentType.objects.get_for_model(self.model)
@@ -175,7 +175,7 @@ class CustomFieldFilterForm(forms.Form):
self.obj_type = ContentType.objects.get_for_model(self.model)
super(CustomFieldFilterForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
# Add all applicable CustomFields to the form
custom_fields = get_custom_fields_for_model(self.obj_type, filterable_only=True).items()
@@ -193,13 +193,15 @@ class TagForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = Tag
fields = ['name', 'slug']
fields = [
'name', 'slug',
]
class AddRemoveTagsForm(forms.Form):
def __init__(self, *args, **kwargs):
super(AddRemoveTagsForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
# Add add/remove tags fields
self.fields['add_tags'] = TagField(required=False)
@@ -208,7 +210,10 @@ class AddRemoveTagsForm(forms.Form):
class TagFilterForm(BootstrapMixin, forms.Form):
model = Tag
q = forms.CharField(required=False, label='Search')
q = forms.CharField(
required=False,
label='Search'
)
#
@@ -249,7 +254,9 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
)
class Meta:
nullable_fields = ['description']
nullable_fields = [
'description',
]
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
@@ -291,28 +298,29 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ImageAttachment
fields = ['name', 'image']
fields = [
'name', 'image',
]
#
# Change logging
#
class ObjectChangeFilterForm(BootstrapMixin, CustomFieldFilterForm):
class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
model = ObjectChange
q = forms.CharField(
required=False,
label='Search'
)
# TODO: Change time_0 and time_1 to time_after and time_before for django-filter==2.0
time_0 = forms.DateTimeField(
time_after = forms.DateTimeField(
label='After',
required=False,
widget=forms.TextInput(
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
)
)
time_1 = forms.DateTimeField(
time_before = forms.DateTimeField(
label='Before',
required=False,
widget=forms.TextInput(
@@ -327,3 +335,9 @@ class ObjectChangeFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=User.objects.order_by('username'),
required=False
)
changed_object_type = forms.ModelChoiceField(
queryset=ContentType.objects.order_by('model'),
required=False,
widget=ContentTypeSelect(),
label='Object Type'
)

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-07-31 02:19
import re
from distutils.version import StrictVersion
from django.conf import settings
import django.contrib.postgres.fields.jsonb
@@ -17,13 +15,14 @@ def verify_postgresql_version(apps, schema_editor):
"""
Verify that PostgreSQL is version 9.4 or higher.
"""
# https://www.postgresql.org/docs/current/libpq-status.html#LIBPQ-PQSERVERVERSION
DB_MINIMUM_VERSION = 90400 # 9.4.0
try:
with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()")
row = cursor.fetchone()
pg_version = re.match(r'^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
pg_version = connection.pg_version
if pg_version < DB_MINIMUM_VERSION:
raise Exception("PostgreSQL 9.4.0 ({}) or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(DB_MINIMUM_VERSION, pg_version))
# Skip if the database is missing (e.g. for CI testing) or misconfigured.
except OperationalError:

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-26 21:25
from distutils.version import StrictVersion
import re
from django.conf import settings
import django.contrib.postgres.fields.jsonb
@@ -14,13 +12,14 @@ def verify_postgresql_version(apps, schema_editor):
"""
Verify that PostgreSQL is version 9.4 or higher.
"""
# https://www.postgresql.org/docs/current/libpq-status.html#LIBPQ-PQSERVERVERSION
DB_MINIMUM_VERSION = 90400 # 9.4.0
try:
with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()")
row = cursor.fetchone()
pg_version = re.match(r'^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
pg_version = connection.pg_version
if pg_version < DB_MINIMUM_VERSION:
raise Exception("PostgreSQL 9.4.0 ({}) or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(DB_MINIMUM_VERSION, pg_version))
# Skip if the database is missing (e.g. for CI testing) or misconfigured.
except OperationalError:

View File

@@ -14,7 +14,7 @@ from django.template import Template, Context
from django.urls import reverse
from dcim.constants import CONNECTION_STATUS_CONNECTED
from utilities.utils import foreground_color
from utilities.utils import deepmerge, foreground_color
from .constants import *
from .querysets import ConfigContextQuerySet
@@ -261,7 +261,7 @@ class CustomFieldValue(models.Model):
if self.pk and self.value is None:
self.delete()
else:
super(CustomFieldValue, self).save(*args, **kwargs)
super().save(*args, **kwargs)
class CustomFieldChoice(models.Model):
@@ -293,7 +293,7 @@ class CustomFieldChoice(models.Model):
def delete(self, using=None, keep_parents=False):
# When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
pk = self.pk
super(CustomFieldChoice, self).delete(using, keep_parents)
super().delete(using, keep_parents)
CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
@@ -603,7 +603,7 @@ class ImageAttachment(models.Model):
_name = self.image.name
super(ImageAttachment, self).delete(*args, **kwargs)
super().delete(*args, **kwargs)
# Delete file from disk
self.image.delete(save=False)
@@ -717,11 +717,11 @@ class ConfigContextModel(models.Model):
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
data = OrderedDict()
for context in ConfigContext.objects.get_for_object(self):
data.update(context.data)
data = deepmerge(data, context.data)
# If the object has local config context data defined, that data overwrites all rendered data
# If the object has local config context data defined, merge it last
if self.local_context_data is not None:
data.update(self.local_context_data)
data = deepmerge(data, self.local_context_data)
return data
@@ -841,7 +841,7 @@ class ObjectChange(models.Model):
self.user_name = self.user.username
self.object_repr = str(self.changed_object)
return super(ObjectChange, self).save(*args, **kwargs)
return super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('extras:objectchange', args=[self.pk])

View File

@@ -14,7 +14,7 @@ class GraphTest(APITestCase):
def setUp(self):
super(GraphTest, self).setUp()
super().setUp()
self.graph1 = Graph.objects.create(
type=GRAPH_TYPE_SITE, name='Test Graph 1', source='http://example.com/graphs.py?site={{ obj.name }}&foo=1'
@@ -118,7 +118,7 @@ class ExportTemplateTest(APITestCase):
def setUp(self):
super(ExportTemplateTest, self).setUp()
super().setUp()
self.content_type = ContentType.objects.get_for_model(Device)
self.exporttemplate1 = ExportTemplate.objects.create(
@@ -225,7 +225,7 @@ class TagTest(APITestCase):
def setUp(self):
super(TagTest, self).setUp()
super().setUp()
self.tag1 = Tag.objects.create(name='Test Tag 1', slug='test-tag-1')
self.tag2 = Tag.objects.create(name='Test Tag 2', slug='test-tag-2')
@@ -316,7 +316,7 @@ class ConfigContextTest(APITestCase):
def setUp(self):
super(ConfigContextTest, self).setUp()
super().setUp()
self.configcontext1 = ConfigContext.objects.create(
name='Test Config Context 1',

View File

@@ -101,7 +101,7 @@ class CustomFieldAPITest(APITestCase):
def setUp(self):
super(CustomFieldAPITest, self).setUp()
super().setUp()
content_type = ContentType.objects.get_for_model(Site)

View File

@@ -12,7 +12,7 @@ class TaggedItemTest(APITestCase):
def setUp(self):
super(TaggedItemTest, self).setUp()
super().setUp()
def test_create_tagged_item(self):

View File

@@ -7,19 +7,19 @@ urlpatterns = [
# Tags
url(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
url(r'^tags/(?P<slug>[\w-]+)/$', views.TagView.as_view(), name='tag'),
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
# Config contexts
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
url(r'^config-contexts/edit/$', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
url(r'^config-contexts/(?P<pk>\d+)/$', views.ConfigContextView.as_view(), name='configcontext'),
url(r'^config-contexts/(?P<pk>\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
url(r'^config-contexts/(?P<pk>\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
# Image attachments
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),

View File

@@ -82,7 +82,7 @@ class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuittype'
permission_required = 'taggit.delete_tag'
queryset = Tag.objects.annotate(
items=Count('taggit_taggeditem_items')
).order_by(

View File

@@ -45,7 +45,7 @@ def enqueue_webhooks(instance, action):
"extras.webhooks_worker.process_webhook",
webhook,
serializer.data,
instance.__class__,
instance._meta.model_name,
action,
str(datetime.datetime.now())
)

View File

@@ -10,14 +10,14 @@ from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED, OBJ
@job('default')
def process_webhook(webhook, data, model_class, event, timestamp):
def process_webhook(webhook, data, model_name, event, timestamp):
"""
Make a POST request to the defined Webhook
"""
payload = {
'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(),
'timestamp': timestamp,
'model': model_class._meta.model_name,
'model': model_name,
'data': data
}
headers = {

View File

@@ -87,7 +87,7 @@ class VLANGroupSerializer(ValidatedModelSerializer):
validator(data)
# Enforce model validation
super(VLANGroupSerializer, self).validate(data)
super().validate(data)
return data
@@ -118,7 +118,7 @@ class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
validator(data)
# Enforce model validation
super(VLANSerializer, self).validate(data)
super().validate(data)
return data

View File

@@ -40,7 +40,7 @@ class BaseIPField(models.Field):
def formfield(self, **kwargs):
defaults = {'form_class': self.form_class()}
defaults.update(kwargs)
return super(BaseIPField, self).formfield(**defaults)
return super().formfield(**defaults)
class IPNetworkField(BaseIPField):

View File

@@ -112,6 +112,10 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search',
label='Search',
)
prefix = django_filters.CharFilter(
method='filter_prefix',
label='Prefix',
)
within = django_filters.CharFilter(
method='search_within',
label='Within prefix',
@@ -197,6 +201,15 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
pass
return queryset.filter(qs_filter)
def filter_prefix(self, queryset, name, value):
if not value.strip():
return queryset
try:
query = str(netaddr.IPNetwork(value).cidr)
return queryset.filter(prefix=query)
except ValidationError:
return queryset.none()
def search_within(self, queryset, name, value):
value = value.strip()
if not value:

File diff suppressed because it is too large Load Diff

View File

@@ -63,7 +63,7 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
verbose_name_plural = 'VRFs'
def __str__(self):
return self.display_name or super(VRF, self).__str__()
return self.display_name or super().__str__()
def get_absolute_url(self):
return reverse('ipam:vrf', args=[self.pk])
@@ -198,7 +198,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
if self.prefix:
# Infer address family from IPNetwork object
self.family = self.prefix.version
super(Aggregate, self).save(*args, **kwargs)
super().save(*args, **kwargs)
def to_csv(self):
return (
@@ -369,7 +369,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
self.prefix = self.prefix.cidr
# Infer address family from IPNetwork object
self.family = self.prefix.version
super(Prefix, self).save(*args, **kwargs)
super().save(*args, **kwargs)
def to_csv(self):
return (
@@ -385,6 +385,15 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
self.description,
)
def _set_prefix_length(self, value):
"""
Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly,
e.g. for bulk editing.
"""
if self.prefix is not None:
self.prefix.prefixlen = value
prefix_length = property(fset=_set_prefix_length)
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]
@@ -429,12 +438,23 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
available_ips = prefix - child_ips
# Remove unusable IPs from non-pool prefixes
if not self.is_pool:
available_ips -= netaddr.IPSet([
netaddr.IPAddress(self.prefix.first),
netaddr.IPAddress(self.prefix.last),
])
# All IP addresses within a pool are considered usable
if self.is_pool:
return available_ips
# All IP addresses within a point-to-point prefix (IPv4 /31 or IPv6 /127) are considered usable
if (
self.family == 4 and self.prefix.prefixlen == 31 # RFC 3021
) or (
self.family == 6 and self.prefix.prefixlen == 127 # RFC 6164
):
return available_ips
# Omit first and last IP address from the available set
available_ips -= netaddr.IPSet([
netaddr.IPAddress(self.prefix.first),
netaddr.IPAddress(self.prefix.last),
])
return available_ips
@@ -484,7 +504,7 @@ class IPAddressManager(models.Manager):
then re-cast this value to INET() so that records will be ordered properly. We are essentially re-casting each
IP address as a /32 or /128.
"""
qs = super(IPAddressManager, self).get_queryset()
qs = super().get_queryset()
return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host')
@@ -605,7 +625,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
if self.address:
# Infer address family from IPAddress object
self.family = self.address.version
super(IPAddress, self).save(*args, **kwargs)
super().save(*args, **kwargs)
def to_csv(self):
@@ -630,6 +650,15 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
self.description,
)
def _set_mask_length(self, value):
"""
Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly,
e.g. for bulk editing.
"""
if self.address is not None:
self.address.prefixlen = value
mask_length = property(fset=_set_mask_length)
@property
def device(self):
if self.interface:
@@ -773,7 +802,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
verbose_name_plural = 'VLANs'
def __str__(self):
return self.display_name or super(VLAN, self).__str__()
return self.display_name or super().__str__()
def get_absolute_url(self):
return reverse('ipam:vlan', args=[self.pk])
@@ -812,7 +841,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
return Interface.objects.filter(
Q(untagged_vlan_id=self.pk) |
Q(tagged_vlans=self.pk)
)
).distinct()
class Service(ChangeLoggedModel, CustomFieldModel):

View File

@@ -430,7 +430,7 @@ class VLANDetailTable(VLANTable):
class VLANMemberTable(BaseTable):
parent = tables.LinkColumn(order_by=['device', 'virtual_machine'])
name = tables.Column(verbose_name='Interface')
name = tables.LinkColumn(verbose_name='Interface')
untagged = tables.TemplateColumn(
template_code=VLAN_MEMBER_UNTAGGED,
orderable=False
@@ -464,7 +464,7 @@ class InterfaceVLANTable(BaseTable):
def __init__(self, interface, *args, **kwargs):
self.interface = interface
super(InterfaceVLANTable, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
#

View File

@@ -12,7 +12,7 @@ class VRFTest(APITestCase):
def setUp(self):
super(VRFTest, self).setUp()
super().setUp()
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
self.vrf2 = VRF.objects.create(name='Test VRF 2', rd='65000:2')
@@ -113,7 +113,7 @@ class RIRTest(APITestCase):
def setUp(self):
super(RIRTest, self).setUp()
super().setUp()
self.rir1 = RIR.objects.create(name='Test RIR 1', slug='test-rir-1')
self.rir2 = RIR.objects.create(name='Test RIR 2', slug='test-rir-2')
@@ -214,7 +214,7 @@ class AggregateTest(APITestCase):
def setUp(self):
super(AggregateTest, self).setUp()
super().setUp()
self.rir1 = RIR.objects.create(name='Test RIR 1', slug='test-rir-1')
self.rir2 = RIR.objects.create(name='Test RIR 2', slug='test-rir-2')
@@ -317,7 +317,7 @@ class RoleTest(APITestCase):
def setUp(self):
super(RoleTest, self).setUp()
super().setUp()
self.role1 = Role.objects.create(name='Test Role 1', slug='test-role-1')
self.role2 = Role.objects.create(name='Test Role 2', slug='test-role-2')
@@ -418,7 +418,7 @@ class PrefixTest(APITestCase):
def setUp(self):
super(PrefixTest, self).setUp()
super().setUp()
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
@@ -657,7 +657,7 @@ class IPAddressTest(APITestCase):
def setUp(self):
super(IPAddressTest, self).setUp()
super().setUp()
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
self.ipaddress1 = IPAddress.objects.create(address=IPNetwork('192.168.0.1/24'))
@@ -756,7 +756,7 @@ class VLANGroupTest(APITestCase):
def setUp(self):
super(VLANGroupTest, self).setUp()
super().setUp()
self.vlangroup1 = VLANGroup.objects.create(name='Test VLAN Group 1', slug='test-vlan-group-1')
self.vlangroup2 = VLANGroup.objects.create(name='Test VLAN Group 2', slug='test-vlan-group-2')
@@ -857,7 +857,7 @@ class VLANTest(APITestCase):
def setUp(self):
super(VLANTest, self).setUp()
super().setUp()
self.vlan1 = VLAN.objects.create(vid=1, name='Test VLAN 1')
self.vlan2 = VLAN.objects.create(vid=2, name='Test VLAN 2')
@@ -958,7 +958,7 @@ class ServiceTest(APITestCase):
def setUp(self):
super(ServiceTest, self).setUp()
super().setUp()
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')

View File

@@ -715,7 +715,7 @@ class IPAddressAssignView(PermissionRequiredMixin, View):
if 'interface' not in request.GET:
return redirect('ipam:ipaddress_add')
return super(IPAddressAssignView, self).dispatch(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)
def get(self, request):

View File

@@ -58,14 +58,14 @@ class TokenPermissions(DjangoModelPermissions):
def __init__(self):
# LOGIN_REQUIRED determines whether read-only access is provided to anonymous users.
self.authenticated_users_only = settings.LOGIN_REQUIRED
super(TokenPermissions, self).__init__()
super().__init__()
def has_permission(self, request, view):
# If token authentication is in use, verify that the token allows write operations (for unsafe methods).
if request.method not in SAFE_METHODS and isinstance(request.auth, Token):
if not request.auth.write_enabled:
return False
return super(TokenPermissions, self).has_permission(request, view)
return super().has_permission(request, view)
#
@@ -132,7 +132,7 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
if not self.limit:
return None
return super(OptionalLimitOffsetPagination, self).get_next_link()
return super().get_next_link()
def get_previous_link(self):
@@ -140,7 +140,7 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
if not self.limit:
return None
return super(OptionalLimitOffsetPagination, self).get_previous_link()
return super().get_previous_link()
#

View File

@@ -15,6 +15,7 @@ OBJ_TYPE_CHOICES = (
('devicetype', 'Device types'),
('device', 'Devices'),
('virtualchassis', 'Virtual Chassis'),
('cable', 'Cables'),
)),
('IPAM', (
('vrf', 'VRFs'),

View File

@@ -7,7 +7,7 @@ import warnings
from django.contrib.messages import constants as messages
from django.core.exceptions import ImproperlyConfigured
# Check for Python 3.5+
# Django 2.1 requires Python 3.5+
if sys.version_info < (3, 5):
raise RuntimeError(
"NetBox requires Python 3.5 or higher (current: Python {})".format(sys.version.split()[0])
@@ -21,7 +21,8 @@ except ImportError:
"Configuration file is not present. Please define netbox/netbox/configuration.py per the documentation."
)
VERSION = '2.5-beta2'
VERSION = '2.5.4'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -245,9 +246,17 @@ LOGIN_URL = '/{}login/'.format(BASE_PATH)
# Secrets
SECRETS_MIN_PUBKEY_SIZE = 2048
# Pagination
PER_PAGE_DEFAULTS = [
25, 50, 100, 250, 500, 1000
]
if PAGINATE_COUNT not in PER_PAGE_DEFAULTS:
PER_PAGE_DEFAULTS.append(PAGINATE_COUNT)
PER_PAGE_DEFAULTS = sorted(PER_PAGE_DEFAULTS)
# Django filters
FILTERS_NULL_CHOICE_LABEL = 'None'
FILTERS_NULL_CHOICE_VALUE = '0' # Must be a string
FILTERS_NULL_CHOICE_VALUE = 'null'
# Django REST framework (API)
REST_FRAMEWORK_VERSION = VERSION[0:3] # Use major.minor as API version
@@ -287,9 +296,12 @@ RQ_QUEUES = {
# drf_yasg settings for Swagger
SWAGGER_SETTINGS = {
'DEFAULT_AUTO_SCHEMA_CLASS': 'utilities.custom_inspectors.NetBoxSwaggerAutoSchema',
'DEFAULT_FIELD_INSPECTORS': [
'utilities.custom_inspectors.NullableBooleanFieldInspector',
'utilities.custom_inspectors.CustomChoiceFieldInspector',
'utilities.custom_inspectors.TagListFieldInspector',
'utilities.custom_inspectors.SerializedPKRelatedFieldInspector',
'drf_yasg.inspectors.CamelCaseJSONFilter',
'drf_yasg.inspectors.ReferencingSerializerInspector',
'drf_yasg.inspectors.RelatedFieldInspector',

View File

@@ -11,13 +11,13 @@ from circuits.filters import CircuitFilter, ProviderFilter
from circuits.models import Circuit, Provider
from circuits.tables import CircuitTable, ProviderTable
from dcim.filters import (
DeviceFilter, DeviceTypeFilter, RackFilter, RackGroupFilter, SiteFilter, VirtualChassisFilter
CableFilter, DeviceFilter, DeviceTypeFilter, RackFilter, RackGroupFilter, SiteFilter, VirtualChassisFilter
)
from dcim.models import (
Cable, ConsolePort, Device, DeviceType, Interface, PowerPort, Rack, RackGroup, Site, VirtualChassis
)
from dcim.tables import (
DeviceDetailTable, DeviceTypeTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable
CableTable, DeviceDetailTable, DeviceTypeTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable
)
from extras.models import ObjectChange, ReportResult, TopologyMap
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
@@ -88,6 +88,12 @@ SEARCH_TYPES = OrderedDict((
'table': VirtualChassisTable,
'url': 'dcim:virtualchassis_list',
}),
('cable', {
'queryset': Cable.objects.all(),
'filter': CableFilter,
'table': CableTable,
'url': 'dcim:cable_list',
}),
# IPAM
('vrf', {
'queryset': VRF.objects.select_related('tenant'),

View File

@@ -120,6 +120,117 @@ input[name="pk"] {
margin-top: 0;
}
/* Color Selections */
.color-selection-aa1409 {
background-color: #aa1409;
color: #ffffff;
}
.color-selection-f44336 {
background-color: #f44336;
color: #ffffff;
}
.color-selection-e91e63 {
background-color: #e91e63;
color: #ffffff;
}
.color-selection-ffe4e1 {
background-color: #ffe4e1;
color: #000000;
}
.color-selection-ff66ff {
background-color: #ff66ff;
color: #ffffff;
}
.color-selection-9c27b0 {
background-color: #9c27b0;
color: #ffffff;
}
.color-selection-673ab7 {
background-color: #673ab7;
color: #ffffff;
}
.color-selection-3f51b5 {
background-color: #3f51b5;
color: #ffffff;
}
.color-selection-2196f3 {
background-color: #2196f3;
color: #ffffff;
}
.color-selection-03a9f4 {
background-color: #03a9f4;
color: #ffffff;
}
.color-selection-00bcd4 {
background-color: #00bcd4;
color: #ffffff;
}
.color-selection-009688 {
background-color: #009688;
color: #ffffff;
}
.color-selection-00ffff {
background-color: #00ffff;
color: #ffffff;
}
.color-selection-2f6a31 {
background-color: #2f6a31;
color: #ffffff;
}
.color-selection-4caf50 {
background-color: #4caf50;
color: #ffffff;
}
.color-selection-8bc34a {
background-color: #8bc34a;
color: #ffffff;
}
.color-selection-cddc39 {
background-color: #cddc39;
color: #000000;
}
.color-selection-ffeb3b {
background-color: #ffeb3b;
color: #000000;
}
.color-selection-ffc107 {
background-color: #ffc107;
color: #000000;
}
.color-selection-ff9800 {
background-color: #ff9800;
color: #ffffff;
}
.color-selection-ff5722 {
background-color: #ff5722;
color: #ffffff;
}
.color-selection-795548 {
background-color: #795548;
color: #ffffff;
}
.color-selection-c0c0c0 {
background-color: #c0c0c0;
color: #000000;
}
.color-selection-9e9e9e {
background-color: #9e9e9e;
color: #ffffff;
}
.color-selection-607d8b {
background-color: #607d8b;
color: #ffffff;
}
.color-selection-111111 {
background-color: #111111;
color: #ffffff;
}
.color-selection-ffffff {
background-color: #ffffff;
color: #000000;
}
/* Tables */
th.pk, td.pk {
padding-bottom: 6px;
@@ -140,6 +251,9 @@ table.attr-table td:nth-child(1) {
div.paginator {
margin-bottom: 20px;
}
div.paginator form {
margin-bottom: 6px;
}
nav ul.pagination {
margin-top: 0;
margin-bottom: 8px !important;

View File

@@ -1,5 +1,10 @@
$(document).ready(function() {
// Pagination
$('select#per_page').change(function() {
this.form.submit();
});
// "Toggle" checkbox for object lists (PK column)
$('input:checkbox.toggle').click(function() {
$(this).closest('table').find('input:checkbox[name=pk]').prop('checked', $(this).prop('checked'));
@@ -62,135 +67,223 @@ $(document).ready(function() {
form.submit();
});
// API select widget
$('select[filter-for]').change(function() {
// Resolve child field by ID specified in parent
var child_names = $(this).attr('filter-for');
var parent = this;
// allow more than one child
$.each(child_names.split(" "), function(_, child_name){
var child_field = $('#id_' + child_name);
var child_selected = child_field.val();
// Wipe out any existing options within the child field and create a default option
child_field.empty();
if (!child_field.attr('multiple')) {
child_field.append($("<option></option>").attr("value", "").text("---------"));
// Parse URLs which may contain variable refrences to other field values
function parseURL(url) {
var filter_regex = /\{\{([a-z_]+)\}\}/g;
var match;
var rendered_url = url;
var filter_field;
while (match = filter_regex.exec(url)) {
filter_field = $('#id_' + match[1]);
var custom_attr = $('option:selected', filter_field).attr('api-value');
if (custom_attr) {
rendered_url = rendered_url.replace(match[0], custom_attr);
} else if (filter_field.val()) {
rendered_url = rendered_url.replace(match[0], filter_field.val());
} else if (filter_field.attr('nullable') == 'true') {
rendered_url = rendered_url.replace(match[0], 'null');
}
}
return rendered_url
}
if ($(parent).val() || $(parent).attr('nullable') == 'true') {
var api_url = child_field.attr('api-url') + '&limit=0&brief=1';
var disabled_indicator = child_field.attr('disabled-indicator');
var initial_value = child_field.attr('initial');
var display_field = child_field.attr('display-field') || 'name';
// Determine the filter fields needed to make an API call
var filter_regex = /\{\{([a-z_]+)\}\}/g;
var match;
var rendered_url = api_url;
var filter_field;
while (match = filter_regex.exec(api_url)) {
filter_field = $('#id_' + match[1]);
var custom_attr = $('option:selected', filter_field).attr('api-value');
if (custom_attr) {
rendered_url = rendered_url.replace(match[0], custom_attr);
} else if (filter_field.val()) {
rendered_url = rendered_url.replace(match[0], filter_field.val());
} else if (filter_field.attr('nullable') == 'true') {
rendered_url = rendered_url.replace(match[0], '0');
}
}
// Account for any conditional URL append strings
$.each(child_field[0].attributes, function(index, attr){
if (attr.name.includes("data-url-conditional-append-")){
var conditional = attr.name.split("data-url-conditional-append-")[1].split("__");
var field = $("#id_" + conditional[0]);
var field_value = conditional[1];
if ($('option:selected', field).attr('api-value') === field_value){
rendered_url = rendered_url + attr.value;
}
}
})
// If all URL variables have been replaced, make the API call
if (rendered_url.search('{{') < 0) {
console.log(child_name + ": Fetching " + rendered_url);
$.ajax({
url: rendered_url,
dataType: 'json',
success: function(response, status) {
$.each(response.results, function(index, choice) {
var option = $("<option></option>").attr("value", choice.id).text(choice[display_field]);
if (disabled_indicator && choice[disabled_indicator] && choice.id != initial_value) {
option.attr("disabled", "disabled");
} else if (choice.id == child_selected) {
option.attr("selected", "selected");
}
child_field.append(option);
});
}
});
}
}
// Trigger change event in case the child field is the parent of another field
child_field.change();
});
// Assign color picker selection classes
function colorPickerClassCopy(data, container) {
if (data.element) {
$(container).addClass($(data.element).attr("class"));
}
return data.text;
}
// Color Picker
$('.netbox-select2-color-picker').select2({
allowClear: true,
placeholder: "---------",
theme: "bootstrap",
templateResult: colorPickerClassCopy,
templateSelection: colorPickerClassCopy
});
// Auto-complete tags
function split_tags(val) {
return val.split(/,\s*/);
}
$("#id_tags")
.on("keydown", function(event) {
if (event.keyCode === $.ui.keyCode.TAB &&
$(this).autocomplete("instance").menu.active) {
event.preventDefault();
}
})
.autocomplete({
source: function(request, response) {
$.ajax({
type: 'GET',
url: netbox_api_path + 'extras/tags/',
data: 'q=' + split_tags(request.term).pop(),
success: function(data) {
var choices = [];
$.each(data.results, function (index, choice) {
choices.push(choice.name);
});
response(choices);
// Static choice selection
$('.netbox-select2-static').select2({
allowClear: true,
placeholder: "---------",
theme: "bootstrap"
});
// API backed selection
// Includes live search and chained fields
// The `multiple` setting may be controled via a data-* attribute
$('.netbox-select2-api').select2({
allowClear: true,
placeholder: "---------",
theme: "bootstrap",
ajax: {
delay: 500,
url: function(params) {
var element = this[0];
var url = parseURL(element.getAttribute("data-url"));
if (url.includes("{{")) {
// URL is not fully rendered yet, abort the request
return false;
}
});
},
search: function() {
// Need 3 or more characters to begin searching
var term = split_tags(this.value).pop();
if (term.length < 3) {
return false;
}
},
focus: function() {
// prevent value inserted on focus
return false;
},
select: function(event, ui) {
var terms = split_tags(this.value);
// remove the current input
terms.pop();
// add the selected item
terms.push(ui.item.value);
// add placeholder to get the comma-and-space at the end
terms.push("");
this.value = terms.join(", ");
return false;
return url;
},
data: function(params) {
var element = this[0];
// Paging. Note that `params.page` indexes at 1
var offset = (params.page - 1) * 50 || 0;
// Base query params
var parameters = {
q: params.term,
brief: 1,
limit: 50,
offset: offset,
};
// filter-for fields from a chain
var attr_name = "data-filter-for-" + $(element).attr("name");
var form = $(element).closest('form');
var filter_for_elements = form.find("select[" + attr_name + "]");
filter_for_elements.each(function(index, filter_for_element) {
var param_name = $(filter_for_element).attr(attr_name);
var value = $(filter_for_element).val();
if (param_name && value) {
parameters[param_name] = value;
}
});
// Conditional query params
$.each(element.attributes, function(index, attr){
if (attr.name.includes("data-conditional-query-param-")){
var conditional = attr.name.split("data-conditional-query-param-")[1].split("__");
var field = $("#id_" + conditional[0]);
var field_value = conditional[1];
if ($('option:selected', field).attr('api-value') === field_value){
var _val = attr.value.split("=");
parameters[_val[0]] = _val[1];
}
}
});
// Additional query params
$.each(element.attributes, function(index, attr){
if (attr.name.includes("data-additional-query-param-")){
var param_name = attr.name.split("data-additional-query-param-")[1]
parameters[param_name] = attr.value;
}
});
// This will handle params with multiple values (i.e. for list filter forms)
return $.param(parameters, true);
},
processResults: function (data) {
var element = this.$element[0];
var results = $.map(data.results, function (obj) {
obj.text = obj[element.getAttribute('display-field')] || obj.name;
obj.id = obj[element.getAttribute('value-field')] || obj.id;
if(element.getAttribute('disabled-indicator') && obj[element.getAttribute('disabled-indicator')]) {
// The disabled-indicator equated to true, so we disable this option
obj.disabled = true;
}
return obj;
});
// Handle the null option
if (element.getAttribute('data-null-option')) {
var null_option = $(element).children()[0]
results.unshift({
id: null_option.value,
text: null_option.text
});
}
// Check if there are more results to page
var page = data.next !== null;
return {
results: results,
pagination: {
more: page
}
};
}
}
});
});
// API backed tags
var tags = $('#id_tags');
if (tags.length > 0 && tags.val().length > 0){
tags = $('#id_tags').val().split(/,\s*/);
} else {
tags = [];
}
tag_objs = $.map(tags, function (tag) {
return {
id: tag,
text: tag,
selected: true
}
});
// Replace the django issued text input with a select element
$('#id_tags').replaceWith('<select name="tags" id="id_tags" class="form-control"></select>');
$('#id_tags').select2({
tags: true,
data: tag_objs,
multiple: true,
allowClear: true,
placeholder: "Tags",
ajax: {
delay: 250,
url: "/api/extras/tags/",
data: function(params) {
// Paging. Note that `params.page` indexes at 1
var offset = (params.page - 1) * 50 || 0;
var parameters = {
q: params.term,
brief: 1,
limit: 50,
offset: offset,
};
return parameters;
},
processResults: function (data) {
var results = $.map(data.results, function (obj) {
return {
id: obj.name,
text: obj.name
}
});
// Check if there are more results to page
var page = data.next !== null;
return {
results: results,
pagination: {
more: page
}
};
}
}
});
$('#id_tags').closest('form').submit(function(event){
// django-taggit can only accept a single comma seperated string value
var value = $('#id_tags').val();
if (value.length > 0){
var final_tags = value.join(', ');
$('#id_tags').val(null).trigger('change');
var option = new Option(final_tags, final_tags, true, true);
$('#id_tags').append(option).trigger('change');
}
});
});

View File

@@ -24,7 +24,7 @@ $(document).ready(function() {
source: function(request, response) {
$.ajax({
type: 'GET',
url: search_field.attr('data-source'),
url: search_field.attr('data-source') + '?brief=1',
data: search_key + '=' + request.term,
success: function(data) {
var choices = [];
@@ -49,7 +49,7 @@ $(document).ready(function() {
// Disable parent selection fields
// $('select[filter-for="' + real_field.attr('name') + '"]').val('');
},
minLength: 4,
minLength: 3,
delay: 500
});

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,123 @@
Select2
=======
[![Build Status][travis-ci-image]][travis-ci-status]
Select2 is a jQuery-based replacement for select boxes. It supports searching,
remote data sets, and pagination of results.
To get started, checkout examples and documentation at
https://select2.org/
Use cases
---------
* Enhancing native selects with search.
* Enhancing native selects with a better multi-select interface.
* Loading data from JavaScript: easily load items via AJAX and have them
searchable.
* Nesting optgroups: native selects only support one level of nesting. Select2
does not have this restriction.
* Tagging: ability to add new items on the fly.
* Working with large, remote datasets: ability to partially load a dataset based
on the search term.
* Paging of large datasets: easy support for loading more pages when the results
are scrolled to the end.
* Templating: support for custom rendering of results and selections.
Browser compatibility
---------------------
* IE 8+
* Chrome 8+
* Firefox 10+
* Safari 3+
* Opera 10.6+
Select2 is automatically tested on the following browsers.
[![Sauce Labs Test Status][saucelabs-matrix]][saucelabs-status]
Usage
-----
You can source Select2 directly from a CDN like [JSDliver][jsdelivr] or
[CDNJS][cdnjs], [download it from this GitHub repo][releases], or use one of
the integrations below.
Integrations
------------
Third party developers have created plugins for platforms which allow Select2 to be integrated more natively and quickly. For many platforms, additional plugins are not required because Select2 acts as a standard `<select>` box.
Plugins
* [Django]
- [django-autocomplete-light]
- [django-easy-select2]
- [django-select2]
* [Meteor] - [meteor-select2]
* [Ruby on Rails][ruby-on-rails] - [select2-rails]
* [Wicket] - [wicketstuff-select2]
* [Yii 2][yii2] - [yii2-widget-select2]
Themes
- [Bootstrap 3][bootstrap3] - [select2-bootstrap-theme]
- [Flat UI][flat-ui] - [select2-flat-theme]
- [Metro UI][metro-ui] - [select2-metro]
Missing an integration? Modify this `README` and make a pull request back here to Select2 on GitHub.
Internationalization (i18n)
---------------------------
Select2 supports multiple languages by simply including the right language JS
file (`dist/js/i18n/it.js`, `dist/js/i18n/nl.js`, etc.) after
`dist/js/select2.js`.
Missing a language? Just copy `src/js/select2/i18n/en.js`, translate it, and
make a pull request back to Select2 here on GitHub.
Documentation
-------------
The documentation for Select2 is available
[through GitHub Pages][documentation] and is located within this repository
in the [`docs` folder][documentation-folder].
Community
---------
You can find out about the different ways to get in touch with the Select2
community at the [Select2 community page][community].
Copyright and license
---------------------
The license is available within the repository in the [LICENSE][license] file.
[cdnjs]: http://www.cdnjs.com/libraries/select2
[community]: https://select2.org/getting-help
[documentation]: https://select2.org
[documentation-folder]: https://github.com/select2/select2/tree/master/docs
[freenode]: https://freenode.net/
[jsdelivr]: http://www.jsdelivr.com/#!select2
[license]: LICENSE.md
[releases]: https://github.com/select2/select2/releases
[saucelabs-matrix]: https://saucelabs.com/browser-matrix/select2.svg
[saucelabs-status]: https://saucelabs.com/u/select2
[travis-ci-image]: https://img.shields.io/travis/select2/select2/master.svg
[travis-ci-status]: https://travis-ci.org/select2/select2
[bootstrap3]: https://getbootstrap.com/
[django]: https://www.djangoproject.com/
[django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light
[django-easy-select2]: https://github.com/asyncee/django-easy-select2
[django-select2]: https://github.com/applegrew/django-select2
[flat-ui]: http://designmodo.github.io/Flat-UI/
[meteor]: https://www.meteor.com/
[meteor-select2]: https://github.com/nate-strauser/meteor-select2
[metro-ui]: http://metroui.org.ua/
[select2-metro]: http://metroui.org.ua/select2.html
[ruby-on-rails]: http://rubyonrails.org/
[select2-bootstrap-theme]: https://github.com/select2/select2-bootstrap-theme
[select2-flat-theme]: https://github.com/techhysahil/select2-Flat_Theme
[select2-rails]: https://github.com/argerim/select2-rails
[vue.js]: http://vuejs.org/
[select2-vue]: http://vuejs.org/examples/select2.html
[wicket]: https://wicket.apache.org/
[wicketstuff-select2]: https://github.com/wicketstuff/core/tree/master/select2-parent
[yii2]: http://www.yiiframework.com/
[yii2-widget-select2]: https://github.com/kartik-v/yii2-widget-select2

View File

@@ -0,0 +1,484 @@
.select2-container {
box-sizing: border-box;
display: inline-block;
margin: 0;
position: relative;
vertical-align: middle; }
.select2-container .select2-selection--single {
box-sizing: border-box;
cursor: pointer;
display: block;
height: 28px;
user-select: none;
-webkit-user-select: none; }
.select2-container .select2-selection--single .select2-selection__rendered {
display: block;
padding-left: 8px;
padding-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
.select2-container .select2-selection--single .select2-selection__clear {
position: relative; }
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
padding-right: 8px;
padding-left: 20px; }
.select2-container .select2-selection--multiple {
box-sizing: border-box;
cursor: pointer;
display: block;
min-height: 32px;
user-select: none;
-webkit-user-select: none; }
.select2-container .select2-selection--multiple .select2-selection__rendered {
display: inline-block;
overflow: hidden;
padding-left: 8px;
text-overflow: ellipsis;
white-space: nowrap; }
.select2-container .select2-search--inline {
float: left; }
.select2-container .select2-search--inline .select2-search__field {
box-sizing: border-box;
border: none;
font-size: 100%;
margin-top: 5px;
padding: 0; }
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
-webkit-appearance: none; }
.select2-dropdown {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
box-sizing: border-box;
display: block;
position: absolute;
left: -100000px;
width: 100%;
z-index: 1051; }
.select2-results {
display: block; }
.select2-results__options {
list-style: none;
margin: 0;
padding: 0; }
.select2-results__option {
padding: 6px;
user-select: none;
-webkit-user-select: none; }
.select2-results__option[aria-selected] {
cursor: pointer; }
.select2-container--open .select2-dropdown {
left: 0; }
.select2-container--open .select2-dropdown--above {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--open .select2-dropdown--below {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-search--dropdown {
display: block;
padding: 4px; }
.select2-search--dropdown .select2-search__field {
padding: 4px;
width: 100%;
box-sizing: border-box; }
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
-webkit-appearance: none; }
.select2-search--dropdown.select2-search--hide {
display: none; }
.select2-close-mask {
border: 0;
margin: 0;
padding: 0;
display: block;
position: fixed;
left: 0;
top: 0;
min-height: 100%;
min-width: 100%;
height: auto;
width: auto;
opacity: 0;
z-index: 99;
background-color: #fff;
filter: alpha(opacity=0); }
.select2-hidden-accessible {
border: 0 !important;
clip: rect(0 0 0 0) !important;
height: 1px !important;
margin: -1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important; }
.select2-container--default .select2-selection--single {
background-color: #fff;
border: 1px solid #aaa;
border-radius: 4px; }
.select2-container--default .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 28px; }
.select2-container--default .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold; }
.select2-container--default .select2-selection--single .select2-selection__placeholder {
color: #999; }
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px; }
.select2-container--default .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0; }
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left; }
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 1px;
right: auto; }
.select2-container--default.select2-container--disabled .select2-selection--single {
background-color: #eee;
cursor: default; }
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
display: none; }
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px; }
.select2-container--default .select2-selection--multiple {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
cursor: text; }
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0 5px;
width: 100%; }
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
list-style: none; }
.select2-container--default .select2-selection--multiple .select2-selection__placeholder {
color: #999;
margin-top: 5px;
float: left; }
.select2-container--default .select2-selection--multiple .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin-top: 5px;
margin-right: 10px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: #999;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #333; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto; }
.select2-container--default.select2-container--focus .select2-selection--multiple {
border: solid black 1px;
outline: 0; }
.select2-container--default.select2-container--disabled .select2-selection--multiple {
background-color: #eee;
cursor: default; }
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
display: none; }
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--default .select2-search--dropdown .select2-search__field {
border: 1px solid #aaa; }
.select2-container--default .select2-search--inline .select2-search__field {
background: transparent;
border: none;
outline: 0;
box-shadow: none;
-webkit-appearance: textfield; }
.select2-container--default .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto; }
.select2-container--default .select2-results__option[role=group] {
padding: 0; }
.select2-container--default .select2-results__option[aria-disabled=true] {
color: #999; }
.select2-container--default .select2-results__option[aria-selected=true] {
background-color: #ddd; }
.select2-container--default .select2-results__option .select2-results__option {
padding-left: 1em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
margin-left: -1em;
padding-left: 2em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -2em;
padding-left: 3em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -3em;
padding-left: 4em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -4em;
padding-left: 5em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -5em;
padding-left: 6em; }
.select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: #5897fb;
color: white; }
.select2-container--default .select2-results__group {
cursor: default;
display: block;
padding: 6px; }
.select2-container--classic .select2-selection--single {
background-color: #f7f7f7;
border: 1px solid #aaa;
border-radius: 4px;
outline: 0;
background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
.select2-container--classic .select2-selection--single:focus {
border: 1px solid #5897fb; }
.select2-container--classic .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 28px; }
.select2-container--classic .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin-right: 10px; }
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
color: #999; }
.select2-container--classic .select2-selection--single .select2-selection__arrow {
background-color: #ddd;
border: none;
border-left: 1px solid #aaa;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
.select2-container--classic .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0; }
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left; }
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
border: none;
border-right: 1px solid #aaa;
border-radius: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
left: 1px;
right: auto; }
.select2-container--classic.select2-container--open .select2-selection--single {
border: 1px solid #5897fb; }
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
background: transparent;
border: none; }
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px; }
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
.select2-container--classic .select2-selection--multiple {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
cursor: text;
outline: 0; }
.select2-container--classic .select2-selection--multiple:focus {
border: 1px solid #5897fb; }
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
list-style: none;
margin: 0;
padding: 0 5px; }
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
display: none; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
color: #888;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #555; }
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
float: right; }
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto; }
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto; }
.select2-container--classic.select2-container--open .select2-selection--multiple {
border: 1px solid #5897fb; }
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--classic .select2-search--dropdown .select2-search__field {
border: 1px solid #aaa;
outline: 0; }
.select2-container--classic .select2-search--inline .select2-search__field {
outline: 0;
box-shadow: none; }
.select2-container--classic .select2-dropdown {
background-color: white;
border: 1px solid transparent; }
.select2-container--classic .select2-dropdown--above {
border-bottom: none; }
.select2-container--classic .select2-dropdown--below {
border-top: none; }
.select2-container--classic .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto; }
.select2-container--classic .select2-results__option[role=group] {
padding: 0; }
.select2-container--classic .select2-results__option[aria-disabled=true] {
color: grey; }
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
background-color: #3875d7;
color: white; }
.select2-container--classic .select2-results__group {
cursor: default;
display: block;
padding: 6px; }
.select2-container--classic.select2-container--open .select2-dropdown {
border-color: #5897fb; }

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/af",[],function(){return{errorLoading:function(){return"Die resultate kon nie gelaai word nie."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Verwyders asseblief "+t+" character";return t!=1&&(n+="s"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Voer asseblief "+t+" of meer karakters";return n},loadingMore:function(){return"Meer resultate word gelaai…"},maximumSelected:function(e){var t="Kies asseblief net "+e.maximum+" item";return e.maximum!=1&&(t+="s"),t},noResults:function(){return"Geen resultate gevind"},searching:function(){return"Besig…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ar",[],function(){return{errorLoading:function(){return"لا يمكن تحميل النتائج"},inputTooLong:function(e){var t=e.input.length-e.maximum;return"الرجاء حذف "+t+" عناصر"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"الرجاء إضافة "+t+" عناصر"},loadingMore:function(){return"جاري تحميل نتائج إضافية..."},maximumSelected:function(e){return"تستطيع إختيار "+e.maximum+" بنود فقط"},noResults:function(){return"لم يتم العثور على أي نتائج"},searching:function(){return"جاري البحث…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/az",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum;return t+" simvol silin"},inputTooShort:function(e){var t=e.minimum-e.input.length;return t+" simvol daxil edin"},loadingMore:function(){return"Daha çox nəticə yüklənir…"},maximumSelected:function(e){return"Sadəcə "+e.maximum+" element seçə bilərsiniz"},noResults:function(){return"Nəticə tapılmadı"},searching:function(){return"Axtarılır…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/bg",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Моля въведете с "+t+" по-малко символ";return t>1&&(n+="a"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Моля въведете още "+t+" символ";return t>1&&(n+="a"),n},loadingMore:function(){return"Зареждат се още…"},maximumSelected:function(e){var t="Можете да направите до "+e.maximum+" ";return e.maximum>1?t+="избора":t+="избор",t},noResults:function(){return"Няма намерени съвпадения"},searching:function(){return"Търсене…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/bs",[],function(){function e(e,t,n,r){return e%10==1&&e%100!=11?t:e%10>=2&&e%10<=4&&(e%100<12||e%100>14)?n:r}return{errorLoading:function(){return"Preuzimanje nije uspijelo."},inputTooLong:function(t){var n=t.input.length-t.maximum,r="Obrišite "+n+" simbol";return r+=e(n,"","a","a"),r},inputTooShort:function(t){var n=t.minimum-t.input.length,r="Ukucajte bar još "+n+" simbol";return r+=e(n,"","a","a"),r},loadingMore:function(){return"Preuzimanje još rezultata…"},maximumSelected:function(t){var n="Možete izabrati samo "+t.maximum+" stavk";return n+=e(t.maximum,"u","e","i"),n},noResults:function(){return"Ništa nije pronađeno"},searching:function(){return"Pretraga…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ca",[],function(){return{errorLoading:function(){return"La càrrega ha fallat"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Si us plau, elimina "+t+" car";return t==1?n+="àcter":n+="àcters",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Si us plau, introdueix "+t+" car";return t==1?n+="àcter":n+="àcters",n},loadingMore:function(){return"Carregant més resultats…"},maximumSelected:function(e){var t="Només es pot seleccionar "+e.maximum+" element";return e.maximum!=1&&(t+="s"),t},noResults:function(){return"No s'han trobat resultats"},searching:function(){return"Cercant…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/cs",[],function(){function e(e,t){switch(e){case 2:return t?"dva":"dvě";case 3:return"tři";case 4:return"čtyři"}return""}return{errorLoading:function(){return"Výsledky nemohly být načteny."},inputTooLong:function(t){var n=t.input.length-t.maximum;return n==1?"Prosím, zadejte o jeden znak méně.":n<=4?"Prosím, zadejte o "+e(n,!0)+" znaky méně.":"Prosím, zadejte o "+n+" znaků méně."},inputTooShort:function(t){var n=t.minimum-t.input.length;return n==1?"Prosím, zadejte ještě jeden znak.":n<=4?"Prosím, zadejte ještě další "+e(n,!0)+" znaky.":"Prosím, zadejte ještě dalších "+n+" znaků."},loadingMore:function(){return"Načítají se další výsledky…"},maximumSelected:function(t){var n=t.maximum;return n==1?"Můžete zvolit jen jednu položku.":n<=4?"Můžete zvolit maximálně "+e(n,!1)+" položky.":"Můžete zvolit maximálně "+n+" položek."},noResults:function(){return"Nenalezeny žádné položky."},searching:function(){return"Vyhledávání…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/da",[],function(){return{errorLoading:function(){return"Resultaterne kunne ikke indlæses."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Angiv venligst "+t+" tegn mindre"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Angiv venligst "+t+" tegn mere"},loadingMore:function(){return"Indlæser flere resultater…"},maximumSelected:function(e){var t="Du kan kun vælge "+e.maximum+" emne";return e.maximum!=1&&(t+="r"),t},noResults:function(){return"Ingen resultater fundet"},searching:function(){return"Søger…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/de",[],function(){return{errorLoading:function(){return"Die Ergebnisse konnten nicht geladen werden."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Bitte "+t+" Zeichen weniger eingeben"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Bitte "+t+" Zeichen mehr eingeben"},loadingMore:function(){return"Lade mehr Ergebnisse…"},maximumSelected:function(e){var t="Sie können nur "+e.maximum+" Eintr";return e.maximum===1?t+="ag":t+="äge",t+=" auswählen",t},noResults:function(){return"Keine Übereinstimmungen gefunden"},searching:function(){return"Suche…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/dsb",[],function(){var e=["znamuško","znamušce","znamuška","znamuškow"],t=["zapisk","zapiska","zapiski","zapiskow"],n=function(t,n){if(t===1)return n[0];if(t===2)return n[1];if(t>2&&t<=4)return n[2];if(t>=5)return n[3]};return{errorLoading:function(){return"Wuslědki njejsu se dali zacytaś."},inputTooLong:function(t){var r=t.input.length-t.maximum;return"Pšosym lašuj "+r+" "+n(r,e)},inputTooShort:function(t){var r=t.minimum-t.input.length;return"Pšosym zapódaj nanejmjenjej "+r+" "+n(r,e)},loadingMore:function(){return"Dalšne wuslědki se zacytaju…"},maximumSelected:function(e){return"Móžoš jano "+e.maximum+" "+n(e.maximum,t)+"wubraś."},noResults:function(){return"Žedne wuslědki namakane"},searching:function(){return"Pyta se…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/el",[],function(){return{errorLoading:function(){return"Τα αποτελέσματα δεν μπόρεσαν να φορτώσουν."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Παρακαλώ διαγράψτε "+t+" χαρακτήρ";return t==1&&(n+="α"),t!=1&&(n+="ες"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Παρακαλώ συμπληρώστε "+t+" ή περισσότερους χαρακτήρες";return n},loadingMore:function(){return"Φόρτωση περισσότερων αποτελεσμάτων…"},maximumSelected:function(e){var t="Μπορείτε να επιλέξετε μόνο "+e.maximum+" επιλογ";return e.maximum==1&&(t+="ή"),e.maximum!=1&&(t+="ές"),t},noResults:function(){return"Δεν βρέθηκαν αποτελέσματα"},searching:function(){return"Αναζήτηση…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Please delete "+t+" character";return t!=1&&(n+="s"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Please enter "+t+" or more characters";return n},loadingMore:function(){return"Loading more results…"},maximumSelected:function(e){var t="You can only select "+e.maximum+" item";return e.maximum!=1&&(t+="s"),t},noResults:function(){return"No results found"},searching:function(){return"Searching…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/es",[],function(){return{errorLoading:function(){return"No se pudieron cargar los resultados"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Por favor, elimine "+t+" car";return t==1?n+="ácter":n+="acteres",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Por favor, introduzca "+t+" car";return t==1?n+="ácter":n+="acteres",n},loadingMore:function(){return"Cargando más resultados…"},maximumSelected:function(e){var t="Sólo puede seleccionar "+e.maximum+" elemento";return e.maximum!=1&&(t+="s"),t},noResults:function(){return"No se encontraron resultados"},searching:function(){return"Buscando…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/et",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Sisesta "+t+" täht";return t!=1&&(n+="e"),n+=" vähem",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Sisesta "+t+" täht";return t!=1&&(n+="e"),n+=" rohkem",n},loadingMore:function(){return"Laen tulemusi…"},maximumSelected:function(e){var t="Saad vaid "+e.maximum+" tulemus";return e.maximum==1?t+="e":t+="t",t+=" valida",t},noResults:function(){return"Tulemused puuduvad"},searching:function(){return"Otsin…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/eu",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Idatzi ";return t==1?n+="karaktere bat":n+=t+" karaktere",n+=" gutxiago",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Idatzi ";return t==1?n+="karaktere bat":n+=t+" karaktere",n+=" gehiago",n},loadingMore:function(){return"Emaitza gehiago kargatzen…"},maximumSelected:function(e){return e.maximum===1?"Elementu bakarra hauta dezakezu":e.maximum+" elementu hauta ditzakezu soilik"},noResults:function(){return"Ez da bat datorrenik aurkitu"},searching:function(){return"Bilatzen…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/fa",[],function(){return{errorLoading:function(){return"امکان بارگذاری نتایج وجود ندارد."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="لطفاً "+t+" کاراکتر را حذف نمایید";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="لطفاً تعداد "+t+" کاراکتر یا بیشتر وارد نمایید";return n},loadingMore:function(){return"در حال بارگذاری نتایج بیشتر..."},maximumSelected:function(e){var t="شما تنها می‌توانید "+e.maximum+" آیتم را انتخاب نمایید";return t},noResults:function(){return"هیچ نتیجه‌ای یافت نشد"},searching:function(){return"در حال جستجو..."}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/fi",[],function(){return{errorLoading:function(){return"Tuloksia ei saatu ladattua."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Ole hyvä ja anna "+t+" merkkiä vähemmän"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Ole hyvä ja anna "+t+" merkkiä lisää"},loadingMore:function(){return"Ladataan lisää tuloksia…"},maximumSelected:function(e){return"Voit valita ainoastaan "+e.maximum+" kpl"},noResults:function(){return"Ei tuloksia"},searching:function(){return"Haetaan…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/fr",[],function(){return{errorLoading:function(){return"Les résultats ne peuvent pas être chargés."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Supprimez "+t+" caractère"+(t>1)?"s":""},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Saisissez au moins "+t+" caractère"+(t>1)?"s":""},loadingMore:function(){return"Chargement de résultats supplémentaires…"},maximumSelected:function(e){return"Vous pouvez seulement sélectionner "+e.maximum+" élément"+(e.maximum>1)?"s":""},noResults:function(){return"Aucun résultat trouvé"},searching:function(){return"Recherche en cours…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/gl",[],function(){return{errorLoading:function(){return"Non foi posíbel cargar os resultados."},inputTooLong:function(e){var t=e.input.length-e.maximum;return t===1?"Elimine un carácter":"Elimine "+t+" caracteres"},inputTooShort:function(e){var t=e.minimum-e.input.length;return t===1?"Engada un carácter":"Engada "+t+" caracteres"},loadingMore:function(){return"Cargando máis resultados…"},maximumSelected:function(e){return e.maximum===1?"Só pode seleccionar un elemento":"Só pode seleccionar "+e.maximum+" elementos"},noResults:function(){return"Non se atoparon resultados"},searching:function(){return"Buscando…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/he",[],function(){return{errorLoading:function(){return"שגיאה בטעינת התוצאות"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="נא למחוק ";return t===1?n+="תו אחד":n+=t+" תווים",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="נא להכניס ";return t===1?n+="תו אחד":n+=t+" תווים",n+=" או יותר",n},loadingMore:function(){return"טוען תוצאות נוספות…"},maximumSelected:function(e){var t="באפשרותך לבחור עד ";return e.maximum===1?t+="פריט אחד":t+=e.maximum+" פריטים",t},noResults:function(){return"לא נמצאו תוצאות"},searching:function(){return"מחפש…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/hi",[],function(){return{errorLoading:function(){return"परिणामों को लोड नहीं किया जा सका।"},inputTooLong:function(e){var t=e.input.length-e.maximum,n=t+" अक्षर को हटा दें";return t>1&&(n=t+" अक्षरों को हटा दें "),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="कृपया "+t+" या अधिक अक्षर दर्ज करें";return n},loadingMore:function(){return"अधिक परिणाम लोड हो रहे है..."},maximumSelected:function(e){var t="आप केवल "+e.maximum+" आइटम का चयन कर सकते हैं";return t},noResults:function(){return"कोई परिणाम नहीं मिला"},searching:function(){return"खोज रहा है..."}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/hr",[],function(){function e(e){var t=" "+e+" znak";return e%10<5&&e%10>0&&(e%100<5||e%100>19)?e%10>1&&(t+="a"):t+="ova",t}return{errorLoading:function(){return"Preuzimanje nije uspjelo."},inputTooLong:function(t){var n=t.input.length-t.maximum;return"Unesite "+e(n)},inputTooShort:function(t){var n=t.minimum-t.input.length;return"Unesite još "+e(n)},loadingMore:function(){return"Učitavanje rezultata…"},maximumSelected:function(e){return"Maksimalan broj odabranih stavki je "+e.maximum},noResults:function(){return"Nema rezultata"},searching:function(){return"Pretraga…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/hsb",[],function(){var e=["znamješko","znamješce","znamješka","znamješkow"],t=["zapisk","zapiskaj","zapiski","zapiskow"],n=function(t,n){if(t===1)return n[0];if(t===2)return n[1];if(t>2&&t<=4)return n[2];if(t>=5)return n[3]};return{errorLoading:function(){return"Wuslědki njedachu so začitać."},inputTooLong:function(t){var r=t.input.length-t.maximum;return"Prošu zhašej "+r+" "+n(r,e)},inputTooShort:function(t){var r=t.minimum-t.input.length;return"Prošu zapodaj znajmjeńša "+r+" "+n(r,e)},loadingMore:function(){return"Dalše wuslědki so začitaja…"},maximumSelected:function(e){return"Móžeš jenož "+e.maximum+" "+n(e.maximum,t)+"wubrać"},noResults:function(){return"Žane wuslědki namakane"},searching:function(){return"Pyta so…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/hu",[],function(){return{errorLoading:function(){return"Az eredmények betöltése nem sikerült."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Túl hosszú. "+t+" karakterrel több, mint kellene."},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Túl rövid. Még "+t+" karakter hiányzik."},loadingMore:function(){return"Töltés…"},maximumSelected:function(e){return"Csak "+e.maximum+" elemet lehet kiválasztani."},noResults:function(){return"Nincs találat."},searching:function(){return"Keresés…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/hy",[],function(){return{errorLoading:function(){return"Արդյունքները հնարավոր չէ բեռնել։"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Խնդրում ենք հեռացնել "+t+" նշան";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Խնդրում ենք մուտքագրել "+t+" կամ ավել նշաններ";return n},loadingMore:function(){return"Բեռնվում են նոր արդյունքներ․․․"},maximumSelected:function(e){var t="Դուք կարող եք ընտրել առավելագույնը "+e.maximum+" կետ";return t},noResults:function(){return"Արդյունքներ չեն գտնվել"},searching:function(){return"Որոնում․․․"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/id",[],function(){return{errorLoading:function(){return"Data tidak boleh diambil."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Hapuskan "+t+" huruf"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Masukkan "+t+" huruf lagi"},loadingMore:function(){return"Mengambil data…"},maximumSelected:function(e){return"Anda hanya dapat memilih "+e.maximum+" pilihan"},noResults:function(){return"Tidak ada data yang sesuai"},searching:function(){return"Mencari…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/is",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Vinsamlegast styttið texta um "+t+" staf";return t<=1?n:n+"i"},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Vinsamlegast skrifið "+t+" staf";return t>1&&(n+="i"),n+=" í viðbót",n},loadingMore:function(){return"Sæki fleiri niðurstöður…"},maximumSelected:function(e){return"Þú getur aðeins valið "+e.maximum+" atriði"},noResults:function(){return"Ekkert fannst"},searching:function(){return"Leita…"}}}),{define:e.define,require:e.require}})();

View File

@@ -0,0 +1,3 @@
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/it",[],function(){return{errorLoading:function(){return"I risultati non possono essere caricati."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Per favore cancella "+t+" caratter";return t!==1?n+="i":n+="e",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Per favore inserisci "+t+" o più caratteri";return n},loadingMore:function(){return"Caricando più risultati…"},maximumSelected:function(e){var t="Puoi selezionare solo "+e.maximum+" element";return e.maximum!==1?t+="i":t+="o",t},noResults:function(){return"Nessun risultato trovato"},searching:function(){return"Sto cercando…"}}}),{define:e.define,require:e.require}})();

Some files were not shown because too many files have changed in this diff Show More