diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 55a979eef..cceea27b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ For real-time discussion, you can join the #netbox Slack channel on [NetworkToCo ## Reporting Bugs -* First, ensure that you've installed the [latest stable version](https://github.com/netbox-community/netbox/releases) +* First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases) of NetBox. If you're running an older version, it's possible that the bug has already been fixed. @@ -28,27 +28,26 @@ up (+1). You might also want to add a comment describing how it's affecting your installation. This will allow us to prioritize bugs based on how many users are affected. -* If you haven't found an existing issue that describes your suspected bug, -please inquire about it on the mailing list. **Do not** file an issue until you -have received confirmation that it is in fact a bug. Invalid issues are very -distracting and slow the pace at which NetBox is developed. - * When submitting an issue, please be as descriptive as possible. Be sure to -include: +provide all information request in the issue template, including: * The environment in which NetBox is running - * The exact steps that can be taken to reproduce the issue (if applicable) + * The exact steps that can be taken to reproduce the issue + * Expected and observed behavior * Any error messages generated * Screenshots (if applicable) * Please avoid prepending any sort of tag (e.g. "[Bug]") to the issue title. -The issue will be reviewed by a moderator after submission and the appropriate +The issue will be reviewed by a maintainer after submission and the appropriate labels will be applied for categorization. * Keep in mind that we prioritize bugs based on their severity and how much work is required to resolve them. It may take some time for someone to address your issue. +* For more information on how bug reports are handled, please see our [issue +intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy). + ## Feature Requests * First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues) @@ -61,10 +60,10 @@ free to add a comment with any additional justification for the feature. (However, note that comments with no substance other than a "+1" will be deleted. Please use GitHub's reactions feature to indicate your support.) -* Due to an excessive backlog of feature requests, we are not currently -accepting any proposals which substantially extend NetBox's functionality -beyond its current feature set. This includes the introduction of any new views -or models which have not already been proposed in an existing feature request. +* Due to a large backlog of feature requests, we are not currently accepting +any proposals which substantially extend NetBox's functionality beyond its +current feature set. This includes the introduction of any new views or models +which have not already been proposed in an existing feature request. * Before filing a new feature request, consider raising your idea on the mailing list first. Feedback you receive there will help validate and shape the @@ -75,8 +74,8 @@ describe the functionality and data model(s) being proposed. The more effort you put into writing a feature request, the better its chance is of being implemented. Overly broad feature requests will be closed. -* When submitting a feature request on GitHub, be sure to include the -following: +* When submitting a feature request on GitHub, be sure to include all +information requested by the issue template, including: * A detailed description of the proposed functionality * A use case for the feature; who would use it and what value it would add @@ -89,6 +88,9 @@ following: title. The issue will be reviewed by a moderator after submission and the appropriate labels will be applied for categorization. +* For more information on how feature requests are handled, please see our +[issue intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy). + ## Submitting Pull Requests * Be sure to open an issue **before** starting work on a pull request, and @@ -103,7 +105,7 @@ any work that's already in progress. * When submitting a pull request, please be sure to work off of the `develop` branch, rather than `master`. The `develop` branch is used for ongoing -development, while `master` is used for tagging new stable releases. +development, while `master` is used for tagging stable releases. * All code submissions should meet the following criteria (CI will enforce these checks): @@ -122,27 +124,26 @@ reduce noise in the discussion. ## Issue Lifecycle -When a correctly formatted issue is submitted it is evaluated by a moderator -who may elect to immediately label the issue as accepted in addition to another -issue type label. In other cases, the issue may be labeled as "status: gathering feedback" -which will often be accompanied by a comment from a moderator asking for further dialog from the community. -If an issue is labeled as "status: revisions needed" a moderator has identified a problem with -the issue itself and is asking for the submitter himself to update the original post with -the requested information. If the original post is not updated in a reasonable amount of time, -the issue will be closed as invalid. +New issues are handled according to our [issue intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy). +Maintainers will assign label(s) and/or close new issues as the policy +dictates. This helps ensure a productive development environment and avoid +accumulating a large backlog of work. -The core maintainers group has chosen to make use of the GitHub Stale bot to aid in issue management. +The core maintainers group has chosen to make use of GitHub's [Stale bot](https://github.com/apps/stale) +to aid in issue management. * Issues will be marked as stale after 14 days of no activity. * Then after 7 more days of inactivity, the issue will be closed. -* Any issue bearing one of the following labels will be exempt from all Stale bot actions: +* Any issue bearing one of the following labels will be exempt from all Stale + bot actions: * `status: accepted` * `status: gathering feedback` * `status: blocked` -It is natural that some new issues get more attention than others. Often this is a metric of an issues's -overall usefulness to the project. In other cases in which issues merely get lost in the shuffle, -notifications from Stale bot can bring renewed attention to potentially meaningful issues. +It is natural that some new issues get more attention than others. Often this +is a metric of an issues's overall value to the project. In other cases in +which issues merely get lost in the shuffle, notifications from Stale bot can +bring renewed attention to potentially meaningful issues. ## Maintainer Guidance diff --git a/README.md b/README.md index 38961c286..478f37e5e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![NetBox](docs/netbox_logo.png "NetBox logo") +![NetBox](docs/netbox_logo.svg "NetBox logo") NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at diff --git a/docs/additional-features/custom-scripts.md b/docs/additional-features/custom-scripts.md index cdb49c82a..c4dffb4b9 100644 --- a/docs/additional-features/custom-scripts.md +++ b/docs/additional-features/custom-scripts.md @@ -71,6 +71,18 @@ The checkbox to commit database changes when executing a script is checked by de commit_default = False ``` +## Accessing Request Data + +Details of the current HTTP request (the one being made to execute the script) are available as the instance attribute `self.request`. This can be used to infer, for example, the user executing the script and the client IP address: + +```python +username = self.request.user.username +ip_address = self.request.META.get('HTTP_X_FORWARDED_FOR') or self.request.META.get('REMOTE_ADDR') +self.log_info("Running as user {} (IP: {})...".format(username, ip_address)) +``` + +For a complete list of available request parameters, please see the [Django documentation](https://docs.djangoproject.com/en/stable/ref/request-response/). + ## Reading Data from Files The Script class provides two convenience methods for reading data from files: diff --git a/docs/index.md b/docs/index.md index 84c39ccde..a68d5a6bf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -![NetBox](netbox_logo.png "NetBox logo") +![NetBox](netbox_logo.svg "NetBox logo") # What is NetBox? diff --git a/docs/installation/2-netbox.md b/docs/installation/2-netbox.md index 7bae23d77..6d2706eb0 100644 --- a/docs/installation/2-netbox.md +++ b/docs/installation/2-netbox.md @@ -14,7 +14,7 @@ This section of the documentation discusses installing and configuring the NetBo # yum install -y epel-release # yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config redis # easy_install-3.6 pip -# ln -s /usr/bin/python36 /usr/bin/python3 +# ln -s /usr/bin/python3.6 /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. diff --git a/docs/installation/4-ldap.md b/docs/installation/4-ldap.md index 32623439a..a41400808 100644 --- a/docs/installation/4-ldap.md +++ b/docs/installation/4-ldap.md @@ -80,6 +80,7 @@ AUTH_LDAP_USER_ATTR_MAP = { ``` # User Groups for Permissions + !!! info When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. You will also need to modify the import line to use `NestedGroupOfNamesType` instead of `GroupOfNamesType` . @@ -117,6 +118,9 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600 * `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. +!!! warning + Authentication will fail if the groups (the distinguished names) do not exist in the LDAP directory. + # 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/`. diff --git a/docs/netbox_logo.svg b/docs/netbox_logo.svg new file mode 100644 index 000000000..5321be100 --- /dev/null +++ b/docs/netbox_logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index afad93fde..a5338698c 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -1,3 +1,29 @@ +# v2.6.10 (2019-01-02) + +## Enhancements + +* [#2233](https://github.com/netbox-community/netbox/issues/2233) - Add ability to move inventory items between devices +* [#2892](https://github.com/netbox-community/netbox/issues/2892) - Extend admin UI to allow deleting old report results +* [#3062](https://github.com/netbox-community/netbox/issues/3062) - Add `assigned_to_interface` filter for IP addresses +* [#3461](https://github.com/netbox-community/netbox/issues/3461) - Fail gracefully on custom link rendering exception +* [#3705](https://github.com/netbox-community/netbox/issues/3705) - Provide request context when executing custom scripts +* [#3762](https://github.com/netbox-community/netbox/issues/3762) - Add date/time picker widgets +* [#3788](https://github.com/netbox-community/netbox/issues/3788) - Enable partial search for inventory items +* [#3812](https://github.com/netbox-community/netbox/issues/3812) - Optimize size of pages containing a dynamic selection field +* [#3827](https://github.com/netbox-community/netbox/issues/3827) - Allow filtering console/power/interface connections by device ID + +## Bug Fixes + +* [#3106](https://github.com/netbox-community/netbox/issues/3106) - Restrict queryset of chained fields when form validation fails +* [#3695](https://github.com/netbox-community/netbox/issues/3695) - Include A/Z termination sites for circuits in global search +* [#3712](https://github.com/netbox-community/netbox/issues/3712) - Scrolling to target (hash) did not account for the header size +* [#3780](https://github.com/netbox-community/netbox/issues/3780) - Fix AttributeError exception in API docs +* [#3809](https://github.com/netbox-community/netbox/issues/3809) - Filter platform by manufacturer when editing devices +* [#3811](https://github.com/netbox-community/netbox/issues/3811) - Fix filtering of racks by group on device list +* [#3822](https://github.com/netbox-community/netbox/issues/3822) - Fix exception when editing a device bay (regression from #3596) + +--- + # v2.6.9 (2019-12-16) ## Enhancements @@ -13,6 +39,8 @@ * [#3749](https://github.com/netbox-community/netbox/issues/3749) - Fix exception on password change page for local users * [#3757](https://github.com/netbox-community/netbox/issues/3757) - Fix unable to assign IP to interface +--- + # v2.6.8 (2019-12-10) ## Enhancements @@ -35,6 +63,8 @@ * [#3724](https://github.com/netbox-community/netbox/issues/3724) - Fix API filtering of interfaces by more than one device name * [#3725](https://github.com/netbox-community/netbox/issues/3725) - Enforce client validation for minimum service port number +--- + # v2.6.7 (2019-11-01) ## Enhancements diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index dfe4f46e4..8ff6a0718 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -7,7 +7,7 @@ from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, - FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple + DatePicker, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple ) from .constants import * from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -161,7 +161,6 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm): ] help_texts = { 'cid': "Unique circuit ID", - 'install_date': "Format: YYYY-MM-DD", 'commit_rate': "Committed rate", } widgets = { @@ -172,7 +171,7 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm): api_url="/api/circuits/circuit-types/" ), 'status': StaticSelect2(), - + 'install_date': DatePicker(), } diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index cea279ddd..433e08960 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -868,8 +868,8 @@ class InventoryItemFilter(DeviceComponentFilterSet): qs_filter = ( Q(name__icontains=value) | Q(part_id__icontains=value) | - Q(serial__iexact=value) | - Q(asset_tag__iexact=value) | + Q(serial__icontains=value) | + Q(asset_tag__icontains=value) | Q(description__icontains=value) ) return queryset.filter(qs_filter) @@ -935,7 +935,7 @@ class CableFilter(django_filters.FilterSet): device_id = MultiValueNumberFilter( method='filter_device' ) - device = MultiValueNumberFilter( + device = MultiValueCharFilter( method='filter_device', field_name='device__name' ) @@ -978,9 +978,12 @@ class ConsoleConnectionFilter(django_filters.FilterSet): method='filter_site', label='Site (slug)', ) - device = django_filters.CharFilter( + device_id = MultiValueNumberFilter( + method='filter_device' + ) + device = MultiValueCharFilter( method='filter_device', - label='Device', + field_name='device__name' ) class Meta: @@ -993,11 +996,11 @@ class ConsoleConnectionFilter(django_filters.FilterSet): return queryset.filter(connected_endpoint__device__site__slug=value) def filter_device(self, queryset, name, value): - if not value.strip(): + if not value: return queryset return queryset.filter( - Q(device__name__icontains=value) | - Q(connected_endpoint__device__name__icontains=value) + Q(**{'{}__in'.format(name): value}) | + Q(**{'connected_endpoint__{}__in'.format(name): value}) ) @@ -1006,9 +1009,12 @@ class PowerConnectionFilter(django_filters.FilterSet): method='filter_site', label='Site (slug)', ) - device = django_filters.CharFilter( + device_id = MultiValueNumberFilter( + method='filter_device' + ) + device = MultiValueCharFilter( method='filter_device', - label='Device', + field_name='device__name' ) class Meta: @@ -1021,11 +1027,11 @@ class PowerConnectionFilter(django_filters.FilterSet): return queryset.filter(_connected_poweroutlet__device__site__slug=value) def filter_device(self, queryset, name, value): - if not value.strip(): + if not value: return queryset return queryset.filter( - Q(device__name__icontains=value) | - Q(_connected_poweroutlet__device__name__icontains=value) + Q(**{'{}__in'.format(name): value}) | + Q(**{'_connected_poweroutlet__{}__in'.format(name): value}) ) @@ -1034,9 +1040,12 @@ class InterfaceConnectionFilter(django_filters.FilterSet): method='filter_site', label='Site (slug)', ) - device = django_filters.CharFilter( + device_id = MultiValueNumberFilter( + method='filter_device' + ) + device = MultiValueCharFilter( method='filter_device', - label='Device', + field_name='device__name' ) class Meta: @@ -1052,11 +1061,11 @@ class InterfaceConnectionFilter(django_filters.FilterSet): ) def filter_device(self, queryset, name, value): - if not value.strip(): + if not value: return queryset return queryset.filter( - Q(device__name__icontains=value) | - Q(_connected_interface__device__name__icontains=value) + Q(**{'{}__in'.format(name): value}) | + Q(**{'_connected_interface__{}__in'.format(name): value}) ) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 66c98c022..c34c750c5 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1339,7 +1339,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): widget=APISelect( api_url="/api/dcim/manufacturers/", filter_for={ - 'device_type': 'manufacturer_id' + 'device_type': 'manufacturer_id', + 'platform': 'manufacturer_id' } ) ) @@ -1408,7 +1409,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): ), 'status': StaticSelect2(), 'platform': APISelect( - api_url="/api/dcim/platforms/" + api_url="/api/dcim/platforms/", + additional_query_params={ + "manufacturer_id": "null" + } ), 'primary_ip4': StaticSelect2(), 'primary_ip6': StaticSelect2(), @@ -1725,7 +1729,7 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm): model = Device field_order = [ - 'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant', + 'q', 'region', 'site', 'group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant', 'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip', ] q = forms.CharField( @@ -1751,12 +1755,12 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt api_url="/api/dcim/sites/", value_field="slug", filter_for={ - 'rack_group_id': 'site', + 'group_id': 'site', 'rack_id': 'site', } ) ) - rack_group_id = FilterChoiceField( + group_id = FilterChoiceField( queryset=RackGroup.objects.prefetch_related( 'site' ), @@ -1764,7 +1768,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt widget=APISelectMultiple( api_url="/api/dcim/rack-groups/", filter_for={ - 'rack_id': 'rack_group_id', + 'rack_id': 'group_id', } ) ) @@ -3167,9 +3171,13 @@ class CableFilterForm(BootstrapMixin, forms.Form): required=False, widget=ColorSelect() ) - device = forms.CharField( + device_id = FilterChoiceField( + queryset=Device.objects.all(), required=False, - label='Device name' + label='Device', + widget=APISelectMultiple( + api_url='/api/dcim/devices/', + ) ) @@ -3234,38 +3242,59 @@ class DeviceBayBulkRenameForm(BulkRenameForm): # class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): - site = forms.ModelChoiceField( + site = FilterChoiceField( queryset=Site.objects.all(), - required=False, - to_field_name='slug' + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + ) ) - device = forms.CharField( + device_id = FilterChoiceField( + queryset=Device.objects.all(), required=False, - label='Device name' + label='Device', + widget=APISelectMultiple( + api_url='/api/dcim/devices/', + ) ) class PowerConnectionFilterForm(BootstrapMixin, forms.Form): - site = forms.ModelChoiceField( + site = FilterChoiceField( queryset=Site.objects.all(), - required=False, - to_field_name='slug' + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + ) ) - device = forms.CharField( + device_id = FilterChoiceField( + queryset=Device.objects.all(), required=False, - label='Device name' + label='Device', + widget=APISelectMultiple( + api_url='/api/dcim/devices/', + ) ) class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): - site = forms.ModelChoiceField( + site = FilterChoiceField( queryset=Site.objects.all(), - required=False, - to_field_name='slug' + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + ) ) - device = forms.CharField( + device_id = FilterChoiceField( + queryset=Device.objects.all(), required=False, - label='Device name' + label='Device', + widget=APISelectMultiple( + api_url='/api/dcim/devices/', + ) ) @@ -3281,9 +3310,12 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm): class Meta: model = InventoryItem fields = [ - 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags', + 'name', 'device', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags', ] widgets = { + 'device': APISelect( + api_url="/api/dcim/devices/" + ), 'manufacturer': APISelect( api_url="/api/dcim/manufacturers/" ) @@ -3319,9 +3351,19 @@ class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm): queryset=InventoryItem.objects.all(), widget=forms.MultipleHiddenInput() ) + device = forms.ModelChoiceField( + queryset=Device.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/devices/" + ) + ) manufacturer = forms.ModelChoiceField( queryset=Manufacturer.objects.all(), - required=False + required=False, + widget=APISelect( + api_url="/api/dcim/manufacturers/" + ) ) part_id = forms.CharField( max_length=50, @@ -3345,18 +3387,25 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): required=False, label='Search' ) - device = forms.CharField( + device_id = FilterChoiceField( + queryset=Device.objects.all(), required=False, - label='Device name' + label='Device', + widget=APISelect( + api_url='/api/dcim/devices/', + ) ) manufacturer = FilterChoiceField( queryset=Manufacturer.objects.all(), to_field_name='slug', - null_label='-- None --' + widget=APISelect( + api_url="/api/dcim/manufacturers/", + value_field="slug", + ) ) discovered = forms.NullBooleanField( required=False, - widget=forms.Select( + widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index db88901b6..8f95fa19a 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2597,7 +2597,7 @@ class DeviceBay(ComponentModel): # Check that the installed device is not already installed elsewhere if self.installed_device: current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first() - if current_bay: + if current_bay and current_bay != self: raise ValidationError({ 'installed_device': "Cannot install the specified device; device is already installed in {}".format( current_bay diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index f99848b1b..ee21b4f5d 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -3,7 +3,10 @@ from django.contrib import admin from netbox.admin import admin_site from utilities.forms import LaxURLField -from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, TopologyMap, Webhook +from .models import ( + CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, TopologyMap, Webhook, +) +from .reports import get_report def order_content_types(field): @@ -166,6 +169,36 @@ class ExportTemplateAdmin(admin.ModelAdmin): form = ExportTemplateForm +# +# Reports +# + +@admin.register(ReportResult, site=admin_site) +class ReportResultAdmin(admin.ModelAdmin): + list_display = [ + 'report', 'active', 'created', 'user', 'passing', + ] + fields = [ + 'report', 'user', 'passing', 'data', + ] + list_filter = [ + 'failed', + ] + readonly_fields = fields + + def has_add_permission(self, request): + return False + + def active(self, obj): + module, report_name = obj.report.split('.') + return True if get_report(module, report_name) else False + active.boolean = True + + def passing(self, obj): + return not obj.failed + passing.boolean = True + + # # Topology maps # diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 2a13e5ce1..e0c70efa3 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -124,6 +124,9 @@ class CustomFieldModelSerializer(ValidatedModelSerializer): else: + if not hasattr(self, 'initial_data'): + self.initial_data = {} + # Populate default values if fields and 'custom_fields' not in self.initial_data: self.initial_data['custom_fields'] = {} diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index efb92b2ce..4f7f57fff 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -10,8 +10,8 @@ from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, - CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2, - BOOLEAN_WITH_BLANK_CHOICES, + CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, LaxURLField, JSONField, + SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES, ) from .constants import * from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag @@ -52,12 +52,12 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F else: initial = None field = forms.NullBooleanField( - required=cf.required, initial=initial, widget=forms.Select(choices=choices) + required=cf.required, initial=initial, widget=StaticSelect2(choices=choices) ) # Date elif cf.type == CF_TYPE_DATE: - field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD") + field = forms.DateField(required=cf.required, initial=initial, widget=DatePicker()) # Select elif cf.type == CF_TYPE_SELECT: @@ -71,7 +71,9 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F default_choice = cf.choices.get(value=initial).pk except ObjectDoesNotExist: pass - field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice) + field = forms.TypedChoiceField( + choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2() + ) # URL elif cf.type == CF_TYPE_URL: @@ -388,16 +390,12 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form): time_after = forms.DateTimeField( label='After', required=False, - widget=forms.TextInput( - attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'} - ) + widget=DateTimePicker() ) time_before = forms.DateTimeField( label='Before', required=False, - widget=forms.TextInput( - attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'} - ) + widget=DateTimePicker() ) action = forms.ChoiceField( choices=add_blank_choice(OBJECTCHANGE_ACTION_CHOICES), diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 170035eb7..038576b63 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -12,12 +12,11 @@ from django.db.models import F, Q from django.http import HttpResponse from django.template import Template, Context from django.urls import reverse -from jinja2 import Environment from taggit.models import TagBase, GenericTaggedItemBase from dcim.constants import CONNECTION_STATUS_CONNECTED from utilities.fields import ColorField -from utilities.utils import deepmerge, foreground_color, model_names_to_filter_dict +from utilities.utils import deepmerge, foreground_color, model_names_to_filter_dict, render_jinja2 from .constants import * from .querysets import ConfigContextQuerySet @@ -502,8 +501,7 @@ class ExportTemplate(models.Model): output = template.render(Context(context)) elif self.template_language == TEMPLATE_LANGUAGE_JINJA2: - template = Environment().from_string(source=self.template_code) - output = template.render(**context) + output = render_jinja2(self.template_code, context) else: return None @@ -917,6 +915,13 @@ class ReportResult(models.Model): class Meta: ordering = ['report'] + def __str__(self): + return "{} {} at {}".format( + self.report, + "passed" if not self.failed else "failed", + self.created + ) + # # Change logging diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index 4e0934a6a..28238b008 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -235,6 +235,9 @@ class BaseScript: # Initiate the log self.log = [] + # Declare the placeholder for the current request + self.request = None + # Grab some info about the script self.filename = inspect.getfile(self.__class__) self.source = inspect.getsource(self.__class__) @@ -337,7 +340,7 @@ def is_variable(obj): return isinstance(obj, ScriptVariable) -def run_script(script, data, files, commit=True): +def run_script(script, data, request, commit=True): """ A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It exists outside of the Script class to ensure it cannot be overridden by a script author. @@ -347,9 +350,13 @@ def run_script(script, data, files, commit=True): end_time = None # Add files to form data + files = request.FILES for field_name, fileobj in files.items(): data[field_name] = fileobj + # Add the current request as a property of the script + script.request = request + try: with transaction.atomic(): start_time = time.time() diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py index ce6cc482a..8c927a0ae 100644 --- a/netbox/extras/templatetags/custom_links.py +++ b/netbox/extras/templatetags/custom_links.py @@ -3,9 +3,9 @@ from collections import OrderedDict from django import template from django.contrib.contenttypes.models import ContentType from django.utils.safestring import mark_safe -from jinja2 import Environment from extras.models import CustomLink +from utilities.utils import render_jinja2 register = template.Library() @@ -46,12 +46,17 @@ def custom_links(obj): # Add non-grouped links else: - text_rendered = Environment().from_string(source=cl.text).render(**context) - if text_rendered: - link_target = ' target="_blank"' if cl.new_window else '' - template_code += LINK_BUTTON.format( - cl.url, link_target, cl.button_class, text_rendered - ) + try: + text_rendered = render_jinja2(cl.text, context) + if text_rendered: + link_rendered = render_jinja2(cl.url, context) + link_target = ' target="_blank"' if cl.new_window else '' + template_code += LINK_BUTTON.format( + link_rendered, link_target, cl.button_class, text_rendered + ) + except Exception as e: + template_code += '' \ + ' {}\n'.format(e, cl.name) # Add grouped links to template for group, links in group_names.items(): @@ -59,11 +64,17 @@ def custom_links(obj): links_rendered = [] for cl in links: - text_rendered = Environment().from_string(source=cl.text).render(**context) - if text_rendered: - link_target = ' target="_blank"' if cl.new_window else '' + try: + text_rendered = render_jinja2(cl.text, context) + if text_rendered: + link_target = ' target="_blank"' if cl.new_window else '' + links_rendered.append( + GROUP_LINK.format(cl.url, link_target, cl.text) + ) + except Exception as e: links_rendered.append( - GROUP_LINK.format(cl.url, link_target, cl.text) + '
  • ' + ' {}
  • '.format(e, cl.name) ) if links_rendered: @@ -71,7 +82,4 @@ def custom_links(obj): links[0].button_class, group, ''.join(links_rendered) ) - # Render template - rendered = Environment().from_string(source=template_code).render(**context) - - return mark_safe(rendered) + return mark_safe(template_code) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 3fe36c9ef..d419f2d7c 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -1,8 +1,11 @@ +import datetime + from django.contrib.contenttypes.models import ContentType from django.urls import reverse +from django.utils import timezone from rest_framework import status -from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Region, Site +from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Rack, RackGroup, RackRole, Region, Site from extras.constants import GRAPH_TYPE_SITE from extras.models import ConfigContext, Graph, ExportTemplate, Tag from tenancy.models import Tenant, TenantGroup @@ -520,3 +523,68 @@ class ConfigContextTest(APITestCase): configcontext6.sites.add(site2) rendered_context = device.get_config_context() self.assertEqual(rendered_context['bar'], 456) + + +class CreatedUpdatedFilterTest(APITestCase): + + def setUp(self): + + super().setUp() + + self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1') + self.rackgroup1 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 1', slug='test-rack-group-1') + self.rackrole1 = RackRole.objects.create(name='Test Rack Role 1', slug='test-rack-role-1', color='ff0000') + self.rack1 = Rack.objects.create( + site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 1', u_height=42, + ) + self.rack2 = Rack.objects.create( + site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 2', u_height=42, + ) + + # change the created and last_updated of one + Rack.objects.filter(pk=self.rack2.pk).update( + last_updated=datetime.datetime(2001, 2, 3, 1, 2, 3, 4, tzinfo=timezone.utc), + created=datetime.datetime(2001, 2, 3) + ) + + def test_get_rack_created(self): + url = reverse('dcim-api:rack-list') + response = self.client.get('{}?created=2001-02-03'.format(url), **self.header) + + self.assertEqual(response.data['count'], 1) + self.assertEqual(response.data['results'][0]['id'], self.rack2.pk) + + def test_get_rack_created_gte(self): + url = reverse('dcim-api:rack-list') + response = self.client.get('{}?created__gte=2001-02-04'.format(url), **self.header) + + self.assertEqual(response.data['count'], 1) + self.assertEqual(response.data['results'][0]['id'], self.rack1.pk) + + def test_get_rack_created_lte(self): + url = reverse('dcim-api:rack-list') + response = self.client.get('{}?created__lte=2001-02-04'.format(url), **self.header) + + self.assertEqual(response.data['count'], 1) + self.assertEqual(response.data['results'][0]['id'], self.rack2.pk) + + def test_get_rack_last_updated(self): + url = reverse('dcim-api:rack-list') + response = self.client.get('{}?last_updated=2001-02-03%2001:02:03.000004'.format(url), **self.header) + + self.assertEqual(response.data['count'], 1) + self.assertEqual(response.data['results'][0]['id'], self.rack2.pk) + + def test_get_rack_last_updated_gte(self): + url = reverse('dcim-api:rack-list') + response = self.client.get('{}?last_updated__gte=2001-02-04%2001:02:03.000004'.format(url), **self.header) + + self.assertEqual(response.data['count'], 1) + self.assertEqual(response.data['results'][0]['id'], self.rack1.pk) + + def test_get_rack_last_updated_lte(self): + url = reverse('dcim-api:rack-list') + response = self.client.get('{}?last_updated__lte=2001-02-04%2001:02:03.000004'.format(url), **self.header) + + self.assertEqual(response.data['count'], 1) + self.assertEqual(response.data['results'][0]['id'], self.rack2.pk) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index c8dc2f374..eb17a65ab 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -413,7 +413,7 @@ class ScriptView(PermissionRequiredMixin, View): if form.is_valid(): commit = form.cleaned_data.pop('_commit') - output, execution_time = run_script(script, form.cleaned_data, request.FILES, commit) + output, execution_time = run_script(script, form.cleaned_data, request, commit) return render(request, 'extras/script.html', { 'module': module, diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index c54ba2f62..1f8fd2caf 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -309,6 +309,10 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt queryset=Interface.objects.all(), label='Interface (ID)', ) + assigned_to_interface = django_filters.BooleanFilter( + method='_assigned_to_interface', + label='Is assigned to an interface', + ) status = django_filters.MultipleChoiceFilter( choices=IPADDRESS_STATUS_CHOICES, null_value=None @@ -366,6 +370,9 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt except Device.DoesNotExist: return queryset.none() + def _assigned_to_interface(self, queryset, name, value): + return queryset.exclude(interface__isnull=value) + class VLANGroupFilter(NameSlugSearchFilterSet): site_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 68529a7f0..44056653b 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -9,8 +9,8 @@ from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, - CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, SlugField, - StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES + CSVChoiceField, DatePicker, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, + SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES ) from virtualization.models import VirtualMachine from .constants import * @@ -156,12 +156,12 @@ class AggregateForm(BootstrapMixin, CustomFieldForm): help_texts = { 'prefix': "IPv4 or IPv6 network", 'rir': "Regional Internet Registry responsible for this prefix", - 'date_added': "Format: YYYY-MM-DD", } widgets = { 'rir': APISelect( api_url="/api/ipam/rirs/" - ) + ), + 'date_added': DatePicker(), } @@ -205,6 +205,9 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd nullable_fields = [ 'date_added', 'description', ] + widgets = { + 'date_added': DatePicker(), + } class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm): @@ -935,7 +938,8 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form): class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = IPAddress field_order = [ - 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'tenant_group', 'tenant', + 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'assigned_to_interface', 'tenant_group', + 'tenant', ] q = forms.CharField( required=False, @@ -981,6 +985,13 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo required=False, widget=StaticSelect2Multiple() ) + assigned_to_interface = forms.NullBooleanField( + required=False, + label='Assigned to an interface', + widget=StaticSelect2( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) # diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 701af3c12..c2945a79c 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured # Environment setup # -VERSION = '2.6.9' +VERSION = '2.6.10' # Hostname HOSTNAME = platform.node() diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 5dee6cade..da5fec24e 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -1,6 +1,6 @@ from collections import OrderedDict -from django.db.models import Count, F +from django.db.models import Count, F, OuterRef, Subquery from django.shortcuts import render from django.views.generic import View from rest_framework.response import Response @@ -8,7 +8,7 @@ from rest_framework.reverse import reverse from rest_framework.views import APIView from circuits.filters import CircuitFilter, ProviderFilter -from circuits.models import Circuit, Provider +from circuits.models import Circuit, CircuitTermination, Provider from circuits.tables import CircuitTable, ProviderTable from dcim.filters import ( CableFilter, DeviceFilter, DeviceTypeFilter, PowerFeedFilter, RackFilter, RackGroupFilter, SiteFilter, @@ -49,9 +49,15 @@ SEARCH_TYPES = OrderedDict(( ('circuit', { 'permission': 'circuits.view_circuit', 'queryset': Circuit.objects.prefetch_related( - 'type', 'provider', 'tenant' - ).prefetch_related( - 'terminations__site' + 'type', 'provider', 'tenant', 'terminations__site' + ).annotate( + # Annotate A/Z terminations + a_side=Subquery( + CircuitTermination.objects.filter(circuit=OuterRef('pk')).filter(term_side='A').values('site__name')[:1] + ), + z_side=Subquery( + CircuitTermination.objects.filter(circuit=OuterRef('pk')).filter(term_side='Z').values('site__name')[:1] + ), ), 'filter': CircuitFilter, 'table': CircuitTable, diff --git a/netbox/project-static/flatpickr-4.6.3/flatpickr.min.js b/netbox/project-static/flatpickr-4.6.3/flatpickr.min.js new file mode 100644 index 000000000..c850b7cfd --- /dev/null +++ b/netbox/project-static/flatpickr-4.6.3/flatpickr.min.js @@ -0,0 +1,2 @@ +/* flatpickr v4.6.3,, @license MIT */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).flatpickr=t()}(this,function(){"use strict";var e=function(){return(e=Object.assign||function(e){for(var t,n=1,a=arguments.length;n",noCalendar:!1,now:new Date,onChange:[],onClose:[],onDayCreate:[],onDestroy:[],onKeyDown:[],onMonthChange:[],onOpen:[],onParseConfig:[],onReady:[],onValueUpdate:[],onYearChange:[],onPreCalendarPosition:[],plugins:[],position:"auto",positionElement:void 0,prevArrow:"",shorthandCurrentMonth:!1,showMonths:1,static:!1,time_24hr:!1,weekNumbers:!1,wrap:!1},a={weekdays:{shorthand:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],longhand:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},months:{shorthand:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],longhand:["January","February","March","April","May","June","July","August","September","October","November","December"]},daysInMonth:[31,28,31,30,31,30,31,31,30,31,30,31],firstDayOfWeek:0,ordinal:function(e){var t=e%100;if(t>3&&t<21)return"th";switch(t%10){case 1:return"st";case 2:return"nd";case 3:return"rd";default:return"th"}},rangeSeparator:" to ",weekAbbreviation:"Wk",scrollTitle:"Scroll to increment",toggleTitle:"Click to toggle",amPM:["AM","PM"],yearAriaLabel:"Year",hourAriaLabel:"Hour",minuteAriaLabel:"Minute",time_24hr:!1},i=function(e){return("0"+e).slice(-2)},o=function(e){return!0===e?1:0};function r(e,t,n){var a;return void 0===n&&(n=!1),function(){var i=this,o=arguments;null!==a&&clearTimeout(a),a=window.setTimeout(function(){a=null,n||e.apply(i,o)},t),n&&!a&&e.apply(i,o)}}var l=function(e){return e instanceof Array?e:[e]};function c(e,t,n){if(!0===n)return e.classList.add(t);e.classList.remove(t)}function d(e,t,n){var a=window.document.createElement(e);return t=t||"",n=n||"",a.className=t,void 0!==n&&(a.textContent=n),a}function s(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function u(e,t){var n=d("div","numInputWrapper"),a=d("input","numInput "+e),i=d("span","arrowUp"),o=d("span","arrowDown");if(-1===navigator.userAgent.indexOf("MSIE 9.0")?a.type="number":(a.type="text",a.pattern="\\d*"),void 0!==t)for(var r in t)a.setAttribute(r,t[r]);return n.appendChild(a),n.appendChild(i),n.appendChild(o),n}var f=function(){},m=function(e,t,n){return n.months[t?"shorthand":"longhand"][e]},g={D:f,F:function(e,t,n){e.setMonth(n.months.longhand.indexOf(t))},G:function(e,t){e.setHours(parseFloat(t))},H:function(e,t){e.setHours(parseFloat(t))},J:function(e,t){e.setDate(parseFloat(t))},K:function(e,t,n){e.setHours(e.getHours()%12+12*o(new RegExp(n.amPM[1],"i").test(t)))},M:function(e,t,n){e.setMonth(n.months.shorthand.indexOf(t))},S:function(e,t){e.setSeconds(parseFloat(t))},U:function(e,t){return new Date(1e3*parseFloat(t))},W:function(e,t,n){var a=parseInt(t),i=new Date(e.getFullYear(),0,2+7*(a-1),0,0,0,0);return i.setDate(i.getDate()-i.getDay()+n.firstDayOfWeek),i},Y:function(e,t){e.setFullYear(parseFloat(t))},Z:function(e,t){return new Date(t)},d:function(e,t){e.setDate(parseFloat(t))},h:function(e,t){e.setHours(parseFloat(t))},i:function(e,t){e.setMinutes(parseFloat(t))},j:function(e,t){e.setDate(parseFloat(t))},l:f,m:function(e,t){e.setMonth(parseFloat(t)-1)},n:function(e,t){e.setMonth(parseFloat(t)-1)},s:function(e,t){e.setSeconds(parseFloat(t))},u:function(e,t){return new Date(parseFloat(t))},w:f,y:function(e,t){e.setFullYear(2e3+parseFloat(t))}},p={D:"(\\w+)",F:"(\\w+)",G:"(\\d\\d|\\d)",H:"(\\d\\d|\\d)",J:"(\\d\\d|\\d)\\w+",K:"",M:"(\\w+)",S:"(\\d\\d|\\d)",U:"(.+)",W:"(\\d\\d|\\d)",Y:"(\\d{4})",Z:"(.+)",d:"(\\d\\d|\\d)",h:"(\\d\\d|\\d)",i:"(\\d\\d|\\d)",j:"(\\d\\d|\\d)",l:"(\\w+)",m:"(\\d\\d|\\d)",n:"(\\d\\d|\\d)",s:"(\\d\\d|\\d)",u:"(.+)",w:"(\\d\\d|\\d)",y:"(\\d{2})"},h={Z:function(e){return e.toISOString()},D:function(e,t,n){return t.weekdays.shorthand[h.w(e,t,n)]},F:function(e,t,n){return m(h.n(e,t,n)-1,!1,t)},G:function(e,t,n){return i(h.h(e,t,n))},H:function(e){return i(e.getHours())},J:function(e,t){return void 0!==t.ordinal?e.getDate()+t.ordinal(e.getDate()):e.getDate()},K:function(e,t){return t.amPM[o(e.getHours()>11)]},M:function(e,t){return m(e.getMonth(),!0,t)},S:function(e){return i(e.getSeconds())},U:function(e){return e.getTime()/1e3},W:function(e,t,n){return n.getWeek(e)},Y:function(e){return e.getFullYear()},d:function(e){return i(e.getDate())},h:function(e){return e.getHours()%12?e.getHours()%12:12},i:function(e){return i(e.getMinutes())},j:function(e){return e.getDate()},l:function(e,t){return t.weekdays.longhand[e.getDay()]},m:function(e){return i(e.getMonth()+1)},n:function(e){return e.getMonth()+1},s:function(e){return e.getSeconds()},u:function(e){return e.getTime()},w:function(e){return e.getDay()},y:function(e){return String(e.getFullYear()).substring(2)}},v=function(e){var t=e.config,i=void 0===t?n:t,o=e.l10n,r=void 0===o?a:o;return function(e,t,n){var a=n||r;return void 0!==i.formatDate?i.formatDate(e,t,a):t.split("").map(function(t,n,o){return h[t]&&"\\"!==o[n-1]?h[t](e,a,i):"\\"!==t?t:""}).join("")}},D=function(e){var t=e.config,i=void 0===t?n:t,o=e.l10n,r=void 0===o?a:o;return function(e,t,a,o){if(0===e||e){var l,c=o||r,d=e;if(e instanceof Date)l=new Date(e.getTime());else if("string"!=typeof e&&void 0!==e.toFixed)l=new Date(e);else if("string"==typeof e){var s=t||(i||n).dateFormat,u=String(e).trim();if("today"===u)l=new Date,a=!0;else if(/Z$/.test(u)||/GMT$/.test(u))l=new Date(e);else if(i&&i.parseDate)l=i.parseDate(e,s);else{l=i&&i.noCalendar?new Date((new Date).setHours(0,0,0,0)):new Date((new Date).getFullYear(),0,1,0,0,0,0);for(var f=void 0,m=[],h=0,v=0,D="";hMath.min(t,n)&&er&&(s=n===h.hourElement?s-r-o(!h.amPM):a,f&&j(void 0,1,h.hourElement)),h.amPM&&u&&(1===l?s+c===23:Math.abs(s-c)>l)&&(h.amPM.textContent=h.l10n.amPM[o(h.amPM.textContent===h.l10n.amPM[0])]),n.value=i(s)}}(e);var t=h._input.value;k(),we(),h._input.value!==t&&h._debouncedChange()}function k(){if(void 0!==h.hourElement&&void 0!==h.minuteElement){var e,t,n=(parseInt(h.hourElement.value.slice(-2),10)||0)%24,a=(parseInt(h.minuteElement.value,10)||0)%60,i=void 0!==h.secondElement?(parseInt(h.secondElement.value,10)||0)%60:0;void 0!==h.amPM&&(e=n,t=h.amPM.textContent,n=e%12+12*o(t===h.l10n.amPM[1]));var r=void 0!==h.config.minTime||h.config.minDate&&h.minDateHasTime&&h.latestSelectedDateObj&&0===w(h.latestSelectedDateObj,h.config.minDate,!0);if(void 0!==h.config.maxTime||h.config.maxDate&&h.maxDateHasTime&&h.latestSelectedDateObj&&0===w(h.latestSelectedDateObj,h.config.maxDate,!0)){var l=void 0!==h.config.maxTime?h.config.maxTime:h.config.maxDate;(n=Math.min(n,l.getHours()))===l.getHours()&&(a=Math.min(a,l.getMinutes())),a===l.getMinutes()&&(i=Math.min(i,l.getSeconds()))}if(r){var c=void 0!==h.config.minTime?h.config.minTime:h.config.minDate;(n=Math.max(n,c.getHours()))===c.getHours()&&(a=Math.max(a,c.getMinutes())),a===c.getMinutes()&&(i=Math.max(i,c.getSeconds()))}O(n,a,i)}}function I(e){var t=e||h.latestSelectedDateObj;t&&O(t.getHours(),t.getMinutes(),t.getSeconds())}function S(){var e=h.config.defaultHour,t=h.config.defaultMinute,n=h.config.defaultSeconds;if(void 0!==h.config.minDate){var a=h.config.minDate.getHours(),i=h.config.minDate.getMinutes();(e=Math.max(e,a))===a&&(t=Math.max(i,t)),e===a&&t===i&&(n=h.config.minDate.getSeconds())}if(void 0!==h.config.maxDate){var o=h.config.maxDate.getHours(),r=h.config.maxDate.getMinutes();(e=Math.min(e,o))===o&&(t=Math.min(r,t)),e===o&&t===r&&(n=h.config.maxDate.getSeconds())}O(e,t,n)}function O(e,t,n){void 0!==h.latestSelectedDateObj&&h.latestSelectedDateObj.setHours(e%24,t,n||0,0),h.hourElement&&h.minuteElement&&!h.isMobile&&(h.hourElement.value=i(h.config.time_24hr?e:(12+e)%12+12*o(e%12==0)),h.minuteElement.value=i(t),void 0!==h.amPM&&(h.amPM.textContent=h.l10n.amPM[o(e>=12)]),void 0!==h.secondElement&&(h.secondElement.value=i(n)))}function _(e){var t=parseInt(e.target.value)+(e.delta||0);(t/1e3>1||"Enter"===e.key&&!/[^\d]/.test(t.toString()))&&Q(t)}function F(e,t,n,a){return t instanceof Array?t.forEach(function(t){return F(e,t,n,a)}):e instanceof Array?e.forEach(function(e){return F(e,t,n,a)}):(e.addEventListener(t,n,a),void h._handlers.push({element:e,event:t,handler:n,options:a}))}function N(e){return function(t){1===t.which&&e(t)}}function Y(){ge("onChange")}function A(e,t){var n=void 0!==e?h.parseDate(e):h.latestSelectedDateObj||(h.config.minDate&&h.config.minDate>h.now?h.config.minDate:h.config.maxDate&&h.config.maxDate=0&&w(e,h.selectedDates[1])<=0}(t)&&!he(t)&&o.classList.add("inRange"),h.weekNumbers&&1===h.config.showMonths&&"prevMonthDay"!==e&&n%7==1&&h.weekNumbers.insertAdjacentHTML("beforeend",""+h.config.getWeek(t)+""),ge("onDayCreate",o),o}function L(e){e.focus(),"range"===h.config.mode&&ne(e)}function W(e){for(var t=e>0?0:h.config.showMonths-1,n=e>0?h.config.showMonths:-1,a=t;a!=n;a+=e)for(var i=h.daysContainer.children[a],o=e>0?0:i.children.length-1,r=e>0?i.children.length:-1,l=o;l!=r;l+=e){var c=i.children[l];if(-1===c.className.indexOf("hidden")&&X(c.dateObj))return c}}function R(e,t){var n=ee(document.activeElement||document.body),a=void 0!==e?e:n?document.activeElement:void 0!==h.selectedDateElem&&ee(h.selectedDateElem)?h.selectedDateElem:void 0!==h.todayDateElem&&ee(h.todayDateElem)?h.todayDateElem:W(t>0?1:-1);return void 0===a?h._input.focus():n?void function(e,t){for(var n=-1===e.className.indexOf("Month")?e.dateObj.getMonth():h.currentMonth,a=t>0?h.config.showMonths:-1,i=t>0?1:-1,o=n-h.currentMonth;o!=a;o+=i)for(var r=h.daysContainer.children[o],l=n-h.currentMonth===o?e.$i+t:t<0?r.children.length-1:0,c=r.children.length,d=l;d>=0&&d0?c:-1);d+=i){var s=r.children[d];if(-1===s.className.indexOf("hidden")&&X(s.dateObj)&&Math.abs(e.$i-d)>=Math.abs(t))return L(s)}h.changeMonth(i),R(W(i),0)}(a,t):L(a)}function B(e,t){for(var n=(new Date(e,t,1).getDay()-h.l10n.firstDayOfWeek+7)%7,a=h.utils.getDaysInMonth((t-1+12)%12),i=h.utils.getDaysInMonth(t),o=window.document.createDocumentFragment(),r=h.config.showMonths>1,l=r?"prevMonthDay hidden":"prevMonthDay",c=r?"nextMonthDay hidden":"nextMonthDay",s=a+1-n,u=0;s<=a;s++,u++)o.appendChild(H(l,new Date(e,t-1,s),s,u));for(s=1;s<=i;s++,u++)o.appendChild(H("",new Date(e,t,s),s,u));for(var f=i+1;f<=42-n&&(1===h.config.showMonths||u%7!=0);f++,u++)o.appendChild(H(c,new Date(e,t+1,f%i),f,u));var m=d("div","dayContainer");return m.appendChild(o),m}function J(){if(void 0!==h.daysContainer){s(h.daysContainer),h.weekNumbers&&s(h.weekNumbers);for(var e=document.createDocumentFragment(),t=0;t1||"dropdown"!==h.config.monthSelectorType)){var e=function(e){return!(void 0!==h.config.minDate&&h.currentYear===h.config.minDate.getFullYear()&&eh.config.maxDate.getMonth())};h.monthsDropdownContainer.tabIndex=-1,h.monthsDropdownContainer.innerHTML="";for(var t=0;t<12;t++)if(e(t)){var n=d("option","flatpickr-monthDropdown-month");n.value=new Date(h.currentYear,t).getMonth().toString(),n.textContent=m(t,h.config.shorthandCurrentMonth,h.l10n),n.tabIndex=-1,h.currentMonth===t&&(n.selected=!0),h.monthsDropdownContainer.appendChild(n)}}}function U(){var e,t=d("div","flatpickr-month"),n=window.document.createDocumentFragment();h.config.showMonths>1||"static"===h.config.monthSelectorType?e=d("span","cur-month"):(h.monthsDropdownContainer=d("select","flatpickr-monthDropdown-months"),F(h.monthsDropdownContainer,"change",function(e){var t=e.target,n=parseInt(t.value,10);h.changeMonth(n-h.currentMonth),ge("onMonthChange")}),K(),e=h.monthsDropdownContainer);var a=u("cur-year",{tabindex:"-1"}),i=a.getElementsByTagName("input")[0];i.setAttribute("aria-label",h.l10n.yearAriaLabel),h.config.minDate&&i.setAttribute("min",h.config.minDate.getFullYear().toString()),h.config.maxDate&&(i.setAttribute("max",h.config.maxDate.getFullYear().toString()),i.disabled=!!h.config.minDate&&h.config.minDate.getFullYear()===h.config.maxDate.getFullYear());var o=d("div","flatpickr-current-month");return o.appendChild(e),o.appendChild(a),n.appendChild(o),t.appendChild(n),{container:t,yearElement:i,monthElement:e}}function q(){s(h.monthNav),h.monthNav.appendChild(h.prevMonthNav),h.config.showMonths&&(h.yearElements=[],h.monthElements=[]);for(var e=h.config.showMonths;e--;){var t=U();h.yearElements.push(t.yearElement),h.monthElements.push(t.monthElement),h.monthNav.appendChild(t.container)}h.monthNav.appendChild(h.nextMonthNav)}function $(){h.weekdayContainer?s(h.weekdayContainer):h.weekdayContainer=d("div","flatpickr-weekdays");for(var e=h.config.showMonths;e--;){var t=d("div","flatpickr-weekdaycontainer");h.weekdayContainer.appendChild(t)}return z(),h.weekdayContainer}function z(){if(h.weekdayContainer){var e=h.l10n.firstDayOfWeek,t=h.l10n.weekdays.shorthand.slice();e>0&&e\n "+t.join("")+"\n \n "}}function G(e,t){void 0===t&&(t=!0);var n=t?e:e-h.currentMonth;n<0&&!0===h._hidePrevMonthArrow||n>0&&!0===h._hideNextMonthArrow||(h.currentMonth+=n,(h.currentMonth<0||h.currentMonth>11)&&(h.currentYear+=h.currentMonth>11?1:-1,h.currentMonth=(h.currentMonth+12)%12,ge("onYearChange"),K()),J(),ge("onMonthChange"),ve())}function V(e){return!(!h.config.appendTo||!h.config.appendTo.contains(e))||h.calendarContainer.contains(e)}function Z(e){if(h.isOpen&&!h.config.inline){var t="function"==typeof(r=e).composedPath?r.composedPath()[0]:r.target,n=V(t),a=t===h.input||t===h.altInput||h.element.contains(t)||e.path&&e.path.indexOf&&(~e.path.indexOf(h.input)||~e.path.indexOf(h.altInput)),i="blur"===e.type?a&&e.relatedTarget&&!V(e.relatedTarget):!a&&!n&&!V(e.relatedTarget),o=!h.config.ignoredFocusElements.some(function(e){return e.contains(t)});i&&o&&(void 0!==h.timeContainer&&void 0!==h.minuteElement&&void 0!==h.hourElement&&T(),h.close(),"range"===h.config.mode&&1===h.selectedDates.length&&(h.clear(!1),h.redraw()))}var r}function Q(e){if(!(!e||h.config.minDate&&eh.config.maxDate.getFullYear())){var t=e,n=h.currentYear!==t;h.currentYear=t||h.currentYear,h.config.maxDate&&h.currentYear===h.config.maxDate.getFullYear()?h.currentMonth=Math.min(h.config.maxDate.getMonth(),h.currentMonth):h.config.minDate&&h.currentYear===h.config.minDate.getFullYear()&&(h.currentMonth=Math.max(h.config.minDate.getMonth(),h.currentMonth)),n&&(h.redraw(),ge("onYearChange"),K())}}function X(e,t){void 0===t&&(t=!0);var n=h.parseDate(e,void 0,t);if(h.config.minDate&&n&&w(n,h.config.minDate,void 0!==t?t:!h.minDateHasTime)<0||h.config.maxDate&&n&&w(n,h.config.maxDate,void 0!==t?t:!h.maxDateHasTime)>0)return!1;if(0===h.config.enable.length&&0===h.config.disable.length)return!0;if(void 0===n)return!1;for(var a=h.config.enable.length>0,i=a?h.config.enable:h.config.disable,o=0,r=void 0;o=r.from.getTime()&&n.getTime()<=r.to.getTime())return a}return!a}function ee(e){return void 0!==h.daysContainer&&(-1===e.className.indexOf("hidden")&&h.daysContainer.contains(e))}function te(e){var t=e.target===h._input,n=h.config.allowInput,a=h.isOpen&&(!n||!t),i=h.config.inline&&t&&!n;if(13===e.keyCode&&t){if(n)return h.setDate(h._input.value,!0,e.target===h.altInput?h.config.altFormat:h.config.dateFormat),e.target.blur();h.open()}else if(V(e.target)||a||i){var o=!!h.timeContainer&&h.timeContainer.contains(e.target);switch(e.keyCode){case 13:o?(e.preventDefault(),T(),de()):se(e);break;case 27:e.preventDefault(),de();break;case 8:case 46:t&&!h.config.allowInput&&(e.preventDefault(),h.clear());break;case 37:case 39:if(o||t)h.hourElement&&h.hourElement.focus();else if(e.preventDefault(),void 0!==h.daysContainer&&(!1===n||document.activeElement&&ee(document.activeElement))){var r=39===e.keyCode?1:-1;e.ctrlKey?(e.stopPropagation(),G(r),R(W(1),0)):R(void 0,r)}break;case 38:case 40:e.preventDefault();var l=40===e.keyCode?1:-1;h.daysContainer&&void 0!==e.target.$i||e.target===h.input||e.target===h.altInput?e.ctrlKey?(e.stopPropagation(),Q(h.currentYear-l),R(W(1),0)):o||R(void 0,7*l):e.target===h.currentYearElement?Q(h.currentYear-l):h.config.enableTime&&(!o&&h.hourElement&&h.hourElement.focus(),T(e),h._debouncedChange());break;case 9:if(o){var c=[h.hourElement,h.minuteElement,h.secondElement,h.amPM].concat(h.pluginElements).filter(function(e){return e}),d=c.indexOf(e.target);if(-1!==d){var s=c[d+(e.shiftKey?-1:1)];e.preventDefault(),(s||h._input).focus()}}else!h.config.noCalendar&&h.daysContainer&&h.daysContainer.contains(e.target)&&e.shiftKey&&(e.preventDefault(),h._input.focus())}}if(void 0!==h.amPM&&e.target===h.amPM)switch(e.key){case h.l10n.amPM[0].charAt(0):case h.l10n.amPM[0].charAt(0).toLowerCase():h.amPM.textContent=h.l10n.amPM[0],k(),we();break;case h.l10n.amPM[1].charAt(0):case h.l10n.amPM[1].charAt(0).toLowerCase():h.amPM.textContent=h.l10n.amPM[1],k(),we()}(t||V(e.target))&&ge("onKeyDown",e)}function ne(e){if(1===h.selectedDates.length&&(!e||e.classList.contains("flatpickr-day")&&!e.classList.contains("flatpickr-disabled"))){for(var t=e?e.dateObj.getTime():h.days.firstElementChild.dateObj.getTime(),n=h.parseDate(h.selectedDates[0],void 0,!0).getTime(),a=Math.min(t,h.selectedDates[0].getTime()),i=Math.max(t,h.selectedDates[0].getTime()),o=!1,r=0,l=0,c=a;ca&&cr)?r=c:c>n&&(!l||c0&&d0&&d>l;return u?(c.classList.add("notAllowed"),["inRange","startRange","endRange"].forEach(function(e){c.classList.remove(e)}),"continue"):o&&!u?"continue":(["startRange","inRange","endRange","notAllowed"].forEach(function(e){c.classList.remove(e)}),void(void 0!==e&&(e.classList.add(t<=h.selectedDates[0].getTime()?"startRange":"endRange"),nt&&d===n&&c.classList.add("endRange"),d>=r&&(0===l||d<=l)&&b(d,n,t)&&c.classList.add("inRange"))))},f=0,m=s.children.length;f0||n.getMinutes()>0||n.getSeconds()>0),h.selectedDates&&(h.selectedDates=h.selectedDates.filter(function(e){return X(e)}),h.selectedDates.length||"min"!==e||I(n),we()),h.daysContainer&&(ce(),void 0!==n?h.currentYearElement[e]=n.getFullYear().toString():h.currentYearElement.removeAttribute(e),h.currentYearElement.disabled=!!a&&void 0!==n&&a.getFullYear()===n.getFullYear())}}function re(){"object"!=typeof h.config.locale&&void 0===E.l10ns[h.config.locale]&&h.config.errorHandler(new Error("flatpickr: invalid locale "+h.config.locale)),h.l10n=e({},E.l10ns.default,"object"==typeof h.config.locale?h.config.locale:"default"!==h.config.locale?E.l10ns[h.config.locale]:void 0),p.K="("+h.l10n.amPM[0]+"|"+h.l10n.amPM[1]+"|"+h.l10n.amPM[0].toLowerCase()+"|"+h.l10n.amPM[1].toLowerCase()+")",void 0===e({},g,JSON.parse(JSON.stringify(f.dataset||{}))).time_24hr&&void 0===E.defaultConfig.time_24hr&&(h.config.time_24hr=h.l10n.time_24hr),h.formatDate=v(h),h.parseDate=D({config:h.config,l10n:h.l10n})}function le(e){if(void 0!==h.calendarContainer){ge("onPreCalendarPosition");var t=e||h._positionElement,n=Array.prototype.reduce.call(h.calendarContainer.children,function(e,t){return e+t.offsetHeight},0),a=h.calendarContainer.offsetWidth,i=h.config.position.split(" "),o=i[0],r=i.length>1?i[1]:null,l=t.getBoundingClientRect(),d=window.innerHeight-l.bottom,s="above"===o||"below"!==o&&dn,u=window.pageYOffset+l.top+(s?-n-2:t.offsetHeight+2);if(c(h.calendarContainer,"arrowTop",!s),c(h.calendarContainer,"arrowBottom",s),!h.config.inline){var f=window.pageXOffset+l.left-(null!=r&&"center"===r?(a-l.width)/2:0),m=window.document.body.offsetWidth-(window.pageXOffset+l.right),g=f+a>window.document.body.offsetWidth,p=m+a>window.document.body.offsetWidth;if(c(h.calendarContainer,"rightMost",g),!h.config.static)if(h.calendarContainer.style.top=u+"px",g)if(p){var v=document.styleSheets[0];if(void 0===v)return;var D=window.document.body.offsetWidth,w=Math.max(0,D/2-a/2),b=v.cssRules.length,C="{left:"+l.left+"px;right:auto;}";c(h.calendarContainer,"rightMost",!1),c(h.calendarContainer,"centerMost",!0),v.insertRule(".flatpickr-calendar.centerMost:before,.flatpickr-calendar.centerMost:after"+C,b),h.calendarContainer.style.left=w+"px",h.calendarContainer.style.right="auto"}else h.calendarContainer.style.left="auto",h.calendarContainer.style.right=m+"px";else h.calendarContainer.style.left=f+"px",h.calendarContainer.style.right="auto"}}}function ce(){h.config.noCalendar||h.isMobile||(ve(),J())}function de(){h._input.focus(),-1!==window.navigator.userAgent.indexOf("MSIE")||void 0!==navigator.msMaxTouchPoints?setTimeout(h.close,0):h.close()}function se(e){e.preventDefault(),e.stopPropagation();var t=function e(t,n){return n(t)?t:t.parentNode?e(t.parentNode,n):void 0}(e.target,function(e){return e.classList&&e.classList.contains("flatpickr-day")&&!e.classList.contains("flatpickr-disabled")&&!e.classList.contains("notAllowed")});if(void 0!==t){var n=t,a=h.latestSelectedDateObj=new Date(n.dateObj.getTime()),i=(a.getMonth()h.currentMonth+h.config.showMonths-1)&&"range"!==h.config.mode;if(h.selectedDateElem=n,"single"===h.config.mode)h.selectedDates=[a];else if("multiple"===h.config.mode){var o=he(a);o?h.selectedDates.splice(parseInt(o),1):h.selectedDates.push(a)}else"range"===h.config.mode&&(2===h.selectedDates.length&&h.clear(!1,!1),h.latestSelectedDateObj=a,h.selectedDates.push(a),0!==w(a,h.selectedDates[0],!0)&&h.selectedDates.sort(function(e,t){return e.getTime()-t.getTime()}));if(k(),i){var r=h.currentYear!==a.getFullYear();h.currentYear=a.getFullYear(),h.currentMonth=a.getMonth(),r&&(ge("onYearChange"),K()),ge("onMonthChange")}if(ve(),J(),we(),h.config.enableTime&&setTimeout(function(){return h.showTimeInput=!0},50),i||"range"===h.config.mode||1!==h.config.showMonths?void 0!==h.selectedDateElem&&void 0===h.hourElement&&h.selectedDateElem&&h.selectedDateElem.focus():L(n),void 0!==h.hourElement&&void 0!==h.hourElement&&h.hourElement.focus(),h.config.closeOnSelect){var l="single"===h.config.mode&&!h.config.enableTime,c="range"===h.config.mode&&2===h.selectedDates.length&&!h.config.enableTime;(l||c)&&de()}Y()}}h.parseDate=D({config:h.config,l10n:h.l10n}),h._handlers=[],h.pluginElements=[],h.loadedPlugins=[],h._bind=F,h._setHoursFromDate=I,h._positionCalendar=le,h.changeMonth=G,h.changeYear=Q,h.clear=function(e,t){void 0===e&&(e=!0);void 0===t&&(t=!0);h.input.value="",void 0!==h.altInput&&(h.altInput.value="");void 0!==h.mobileInput&&(h.mobileInput.value="");h.selectedDates=[],h.latestSelectedDateObj=void 0,!0===t&&(h.currentYear=h._initialDate.getFullYear(),h.currentMonth=h._initialDate.getMonth());h.showTimeInput=!1,!0===h.config.enableTime&&S();h.redraw(),e&&ge("onChange")},h.close=function(){h.isOpen=!1,h.isMobile||(void 0!==h.calendarContainer&&h.calendarContainer.classList.remove("open"),void 0!==h._input&&h._input.classList.remove("active"));ge("onClose")},h._createElement=d,h.destroy=function(){void 0!==h.config&&ge("onDestroy");for(var e=h._handlers.length;e--;){var t=h._handlers[e];t.element.removeEventListener(t.event,t.handler,t.options)}if(h._handlers=[],h.mobileInput)h.mobileInput.parentNode&&h.mobileInput.parentNode.removeChild(h.mobileInput),h.mobileInput=void 0;else if(h.calendarContainer&&h.calendarContainer.parentNode)if(h.config.static&&h.calendarContainer.parentNode){var n=h.calendarContainer.parentNode;if(n.lastChild&&n.removeChild(n.lastChild),n.parentNode){for(;n.firstChild;)n.parentNode.insertBefore(n.firstChild,n);n.parentNode.removeChild(n)}}else h.calendarContainer.parentNode.removeChild(h.calendarContainer);h.altInput&&(h.input.type="text",h.altInput.parentNode&&h.altInput.parentNode.removeChild(h.altInput),delete h.altInput);h.input&&(h.input.type=h.input._type,h.input.classList.remove("flatpickr-input"),h.input.removeAttribute("readonly"),h.input.value="");["_showTimeInput","latestSelectedDateObj","_hideNextMonthArrow","_hidePrevMonthArrow","__hideNextMonthArrow","__hidePrevMonthArrow","isMobile","isOpen","selectedDateElem","minDateHasTime","maxDateHasTime","days","daysContainer","_input","_positionElement","innerContainer","rContainer","monthNav","todayDateElem","calendarContainer","weekdayContainer","prevMonthNav","nextMonthNav","monthsDropdownContainer","currentMonthElement","currentYearElement","navigationCurrentMonth","selectedDateElem","config"].forEach(function(e){try{delete h[e]}catch(e){}})},h.isEnabled=X,h.jumpToDate=A,h.open=function(e,t){void 0===t&&(t=h._positionElement);if(!0===h.isMobile)return e&&(e.preventDefault(),e.target&&e.target.blur()),void 0!==h.mobileInput&&(h.mobileInput.focus(),h.mobileInput.click()),void ge("onOpen");if(h._input.disabled||h.config.inline)return;var n=h.isOpen;h.isOpen=!0,n||(h.calendarContainer.classList.add("open"),h._input.classList.add("active"),ge("onOpen"),le(t));!0===h.config.enableTime&&!0===h.config.noCalendar&&(0===h.selectedDates.length&&ie(),!1!==h.config.allowInput||void 0!==e&&h.timeContainer.contains(e.relatedTarget)||setTimeout(function(){return h.hourElement.select()},50))},h.redraw=ce,h.set=function(e,n){if(null!==e&&"object"==typeof e)for(var a in Object.assign(h.config,e),e)void 0!==ue[a]&&ue[a].forEach(function(e){return e()});else h.config[e]=n,void 0!==ue[e]?ue[e].forEach(function(e){return e()}):t.indexOf(e)>-1&&(h.config[e]=l(n));h.redraw(),we(!1)},h.setDate=function(e,t,n){void 0===t&&(t=!1);void 0===n&&(n=h.config.dateFormat);if(0!==e&&!e||e instanceof Array&&0===e.length)return h.clear(t);fe(e,n),h.showTimeInput=h.selectedDates.length>0,h.latestSelectedDateObj=h.selectedDates[h.selectedDates.length-1],h.redraw(),A(),I(),0===h.selectedDates.length&&h.clear(!1);we(t),t&&ge("onChange")},h.toggle=function(e){if(!0===h.isOpen)return h.close();h.open(e)};var ue={locale:[re,z],showMonths:[q,x,$],minDate:[A],maxDate:[A]};function fe(e,t){var n=[];if(e instanceof Array)n=e.map(function(e){return h.parseDate(e,t)});else if(e instanceof Date||"number"==typeof e)n=[h.parseDate(e,t)];else if("string"==typeof e)switch(h.config.mode){case"single":case"time":n=[h.parseDate(e,t)];break;case"multiple":n=e.split(h.config.conjunction).map(function(e){return h.parseDate(e,t)});break;case"range":n=e.split(h.l10n.rangeSeparator).map(function(e){return h.parseDate(e,t)})}else h.config.errorHandler(new Error("Invalid date supplied: "+JSON.stringify(e)));h.selectedDates=n.filter(function(e){return e instanceof Date&&X(e,!1)}),"range"===h.config.mode&&h.selectedDates.sort(function(e,t){return e.getTime()-t.getTime()})}function me(e){return e.slice().map(function(e){return"string"==typeof e||"number"==typeof e||e instanceof Date?h.parseDate(e,void 0,!0):e&&"object"==typeof e&&e.from&&e.to?{from:h.parseDate(e.from,void 0),to:h.parseDate(e.to,void 0)}:e}).filter(function(e){return e})}function ge(e,t){if(void 0!==h.config){var n=h.config[e];if(void 0!==n&&n.length>0)for(var a=0;n[a]&&a1||"static"===h.config.monthSelectorType?h.monthElements[t].textContent=m(n.getMonth(),h.config.shorthandCurrentMonth,h.l10n)+" ":h.monthsDropdownContainer.value=n.getMonth().toString(),e.value=n.getFullYear().toString()}),h._hidePrevMonthArrow=void 0!==h.config.minDate&&(h.currentYear===h.config.minDate.getFullYear()?h.currentMonth<=h.config.minDate.getMonth():h.currentYearh.config.maxDate.getMonth():h.currentYear>h.config.maxDate.getFullYear()))}function De(e){return h.selectedDates.map(function(t){return h.formatDate(t,e)}).filter(function(e,t,n){return"range"!==h.config.mode||h.config.enableTime||n.indexOf(e)===t}).join("range"!==h.config.mode?h.config.conjunction:h.l10n.rangeSeparator)}function we(e){void 0===e&&(e=!0),void 0!==h.mobileInput&&h.mobileFormatStr&&(h.mobileInput.value=void 0!==h.latestSelectedDateObj?h.formatDate(h.latestSelectedDateObj,h.mobileFormatStr):""),h.input.value=De(h.config.dateFormat),void 0!==h.altInput&&(h.altInput.value=De(h.config.altFormat)),!1!==e&&ge("onValueUpdate")}function be(e){var t=h.prevMonthNav.contains(e.target),n=h.nextMonthNav.contains(e.target);t||n?G(t?-1:1):h.yearElements.indexOf(e.target)>=0?e.target.select():e.target.classList.contains("arrowUp")?h.changeYear(h.currentYear+1):e.target.classList.contains("arrowDown")&&h.changeYear(h.currentYear-1)}return function(){h.element=h.input=f,h.isOpen=!1,function(){var a=["wrap","weekNumbers","allowInput","clickOpens","time_24hr","enableTime","noCalendar","altInput","shorthandCurrentMonth","inline","static","enableSeconds","disableMobile"],i=e({},g,JSON.parse(JSON.stringify(f.dataset||{}))),o={};h.config.parseDate=i.parseDate,h.config.formatDate=i.formatDate,Object.defineProperty(h.config,"enable",{get:function(){return h.config._enable},set:function(e){h.config._enable=me(e)}}),Object.defineProperty(h.config,"disable",{get:function(){return h.config._disable},set:function(e){h.config._disable=me(e)}});var r="time"===i.mode;if(!i.dateFormat&&(i.enableTime||r)){var c=E.defaultConfig.dateFormat||n.dateFormat;o.dateFormat=i.noCalendar||r?"H:i"+(i.enableSeconds?":S":""):c+" H:i"+(i.enableSeconds?":S":"")}if(i.altInput&&(i.enableTime||r)&&!i.altFormat){var d=E.defaultConfig.altFormat||n.altFormat;o.altFormat=i.noCalendar||r?"h:i"+(i.enableSeconds?":S K":" K"):d+" h:i"+(i.enableSeconds?":S":"")+" K"}i.altInputClass||(h.config.altInputClass=h.input.className+" "+h.config.altInputClass),Object.defineProperty(h.config,"minDate",{get:function(){return h.config._minDate},set:oe("min")}),Object.defineProperty(h.config,"maxDate",{get:function(){return h.config._maxDate},set:oe("max")});var s=function(e){return function(t){h.config["min"===e?"_minTime":"_maxTime"]=h.parseDate(t,"H:i:S")}};Object.defineProperty(h.config,"minTime",{get:function(){return h.config._minTime},set:s("min")}),Object.defineProperty(h.config,"maxTime",{get:function(){return h.config._maxTime},set:s("max")}),"time"===i.mode&&(h.config.noCalendar=!0,h.config.enableTime=!0),Object.assign(h.config,o,i);for(var u=0;u-1?h.config[p]=l(m[p]).map(y).concat(h.config[p]):void 0===i[p]&&(h.config[p]=m[p])}ge("onParseConfig")}(),re(),h.input=h.config.wrap?f.querySelector("[data-input]"):f,h.input?(h.input._type=h.input.type,h.input.type="text",h.input.classList.add("flatpickr-input"),h._input=h.input,h.config.altInput&&(h.altInput=d(h.input.nodeName,h.config.altInputClass),h._input=h.altInput,h.altInput.placeholder=h.input.placeholder,h.altInput.disabled=h.input.disabled,h.altInput.required=h.input.required,h.altInput.tabIndex=h.input.tabIndex,h.altInput.type="text",h.input.setAttribute("type","hidden"),!h.config.static&&h.input.parentNode&&h.input.parentNode.insertBefore(h.altInput,h.input.nextSibling)),h.config.allowInput||h._input.setAttribute("readonly","readonly"),h._positionElement=h.config.positionElement||h._input):h.config.errorHandler(new Error("Invalid input element specified")),function(){h.selectedDates=[],h.now=h.parseDate(h.config.now)||new Date;var e=h.config.defaultDate||("INPUT"!==h.input.nodeName&&"TEXTAREA"!==h.input.nodeName||!h.input.placeholder||h.input.value!==h.input.placeholder?h.input.value:null);e&&fe(e,h.config.dateFormat),h._initialDate=h.selectedDates.length>0?h.selectedDates[0]:h.config.minDate&&h.config.minDate.getTime()>h.now.getTime()?h.config.minDate:h.config.maxDate&&h.config.maxDate.getTime()0&&(h.latestSelectedDateObj=h.selectedDates[0]),void 0!==h.config.minTime&&(h.config.minTime=h.parseDate(h.config.minTime,"H:i")),void 0!==h.config.maxTime&&(h.config.maxTime=h.parseDate(h.config.maxTime,"H:i")),h.minDateHasTime=!!h.config.minDate&&(h.config.minDate.getHours()>0||h.config.minDate.getMinutes()>0||h.config.minDate.getSeconds()>0),h.maxDateHasTime=!!h.config.maxDate&&(h.config.maxDate.getHours()>0||h.config.maxDate.getMinutes()>0||h.config.maxDate.getSeconds()>0),Object.defineProperty(h,"showTimeInput",{get:function(){return h._showTimeInput},set:function(e){h._showTimeInput=e,h.calendarContainer&&c(h.calendarContainer,"showTimeInput",e),h.isOpen&&le()}})}(),h.utils={getDaysInMonth:function(e,t){return void 0===e&&(e=h.currentMonth),void 0===t&&(t=h.currentYear),1===e&&(t%4==0&&t%100!=0||t%400==0)?29:h.l10n.daysInMonth[e]}},h.isMobile||function(){var e=window.document.createDocumentFragment();if(h.calendarContainer=d("div","flatpickr-calendar"),h.calendarContainer.tabIndex=-1,!h.config.noCalendar){if(e.appendChild((h.monthNav=d("div","flatpickr-months"),h.yearElements=[],h.monthElements=[],h.prevMonthNav=d("span","flatpickr-prev-month"),h.prevMonthNav.innerHTML=h.config.prevArrow,h.nextMonthNav=d("span","flatpickr-next-month"),h.nextMonthNav.innerHTML=h.config.nextArrow,q(),Object.defineProperty(h,"_hidePrevMonthArrow",{get:function(){return h.__hidePrevMonthArrow},set:function(e){h.__hidePrevMonthArrow!==e&&(c(h.prevMonthNav,"flatpickr-disabled",e),h.__hidePrevMonthArrow=e)}}),Object.defineProperty(h,"_hideNextMonthArrow",{get:function(){return h.__hideNextMonthArrow},set:function(e){h.__hideNextMonthArrow!==e&&(c(h.nextMonthNav,"flatpickr-disabled",e),h.__hideNextMonthArrow=e)}}),h.currentYearElement=h.yearElements[0],ve(),h.monthNav)),h.innerContainer=d("div","flatpickr-innerContainer"),h.config.weekNumbers){var t=function(){h.calendarContainer.classList.add("hasWeeks");var e=d("div","flatpickr-weekwrapper");e.appendChild(d("span","flatpickr-weekday",h.l10n.weekAbbreviation));var t=d("div","flatpickr-weeks");return e.appendChild(t),{weekWrapper:e,weekNumbers:t}}(),n=t.weekWrapper,a=t.weekNumbers;h.innerContainer.appendChild(n),h.weekNumbers=a,h.weekWrapper=n}h.rContainer=d("div","flatpickr-rContainer"),h.rContainer.appendChild($()),h.daysContainer||(h.daysContainer=d("div","flatpickr-days"),h.daysContainer.tabIndex=-1),J(),h.rContainer.appendChild(h.daysContainer),h.innerContainer.appendChild(h.rContainer),e.appendChild(h.innerContainer)}h.config.enableTime&&e.appendChild(function(){h.calendarContainer.classList.add("hasTime"),h.config.noCalendar&&h.calendarContainer.classList.add("noCalendar"),h.timeContainer=d("div","flatpickr-time"),h.timeContainer.tabIndex=-1;var e=d("span","flatpickr-time-separator",":"),t=u("flatpickr-hour",{"aria-label":h.l10n.hourAriaLabel});h.hourElement=t.getElementsByTagName("input")[0];var n=u("flatpickr-minute",{"aria-label":h.l10n.minuteAriaLabel});if(h.minuteElement=n.getElementsByTagName("input")[0],h.hourElement.tabIndex=h.minuteElement.tabIndex=-1,h.hourElement.value=i(h.latestSelectedDateObj?h.latestSelectedDateObj.getHours():h.config.time_24hr?h.config.defaultHour:function(e){switch(e%24){case 0:case 12:return 12;default:return e%12}}(h.config.defaultHour)),h.minuteElement.value=i(h.latestSelectedDateObj?h.latestSelectedDateObj.getMinutes():h.config.defaultMinute),h.hourElement.setAttribute("step",h.config.hourIncrement.toString()),h.minuteElement.setAttribute("step",h.config.minuteIncrement.toString()),h.hourElement.setAttribute("min",h.config.time_24hr?"0":"1"),h.hourElement.setAttribute("max",h.config.time_24hr?"23":"12"),h.minuteElement.setAttribute("min","0"),h.minuteElement.setAttribute("max","59"),h.timeContainer.appendChild(t),h.timeContainer.appendChild(e),h.timeContainer.appendChild(n),h.config.time_24hr&&h.timeContainer.classList.add("time24hr"),h.config.enableSeconds){h.timeContainer.classList.add("hasSeconds");var a=u("flatpickr-second");h.secondElement=a.getElementsByTagName("input")[0],h.secondElement.value=i(h.latestSelectedDateObj?h.latestSelectedDateObj.getSeconds():h.config.defaultSeconds),h.secondElement.setAttribute("step",h.minuteElement.getAttribute("step")),h.secondElement.setAttribute("min","0"),h.secondElement.setAttribute("max","59"),h.timeContainer.appendChild(d("span","flatpickr-time-separator",":")),h.timeContainer.appendChild(a)}return h.config.time_24hr||(h.amPM=d("span","flatpickr-am-pm",h.l10n.amPM[o((h.latestSelectedDateObj?h.hourElement.value:h.config.defaultHour)>11)]),h.amPM.title=h.l10n.toggleTitle,h.amPM.tabIndex=-1,h.timeContainer.appendChild(h.amPM)),h.timeContainer}()),c(h.calendarContainer,"rangeMode","range"===h.config.mode),c(h.calendarContainer,"animate",!0===h.config.animate),c(h.calendarContainer,"multiMonth",h.config.showMonths>1),h.calendarContainer.appendChild(e);var r=void 0!==h.config.appendTo&&void 0!==h.config.appendTo.nodeType;if((h.config.inline||h.config.static)&&(h.calendarContainer.classList.add(h.config.inline?"inline":"static"),h.config.inline&&(!r&&h.element.parentNode?h.element.parentNode.insertBefore(h.calendarContainer,h._input.nextSibling):void 0!==h.config.appendTo&&h.config.appendTo.appendChild(h.calendarContainer)),h.config.static)){var l=d("div","flatpickr-wrapper");h.element.parentNode&&h.element.parentNode.insertBefore(l,h.element),l.appendChild(h.element),h.altInput&&l.appendChild(h.altInput),l.appendChild(h.calendarContainer)}h.config.static||h.config.inline||(void 0!==h.config.appendTo?h.config.appendTo:window.document.body).appendChild(h.calendarContainer)}(),function(){if(h.config.wrap&&["open","close","toggle","clear"].forEach(function(e){Array.prototype.forEach.call(h.element.querySelectorAll("[data-"+e+"]"),function(t){return F(t,"click",h[e])})}),h.isMobile)!function(){var e=h.config.enableTime?h.config.noCalendar?"time":"datetime-local":"date";h.mobileInput=d("input",h.input.className+" flatpickr-mobile"),h.mobileInput.step=h.input.getAttribute("step")||"any",h.mobileInput.tabIndex=1,h.mobileInput.type=e,h.mobileInput.disabled=h.input.disabled,h.mobileInput.required=h.input.required,h.mobileInput.placeholder=h.input.placeholder,h.mobileFormatStr="datetime-local"===e?"Y-m-d\\TH:i:S":"date"===e?"Y-m-d":"H:i:S",h.selectedDates.length>0&&(h.mobileInput.defaultValue=h.mobileInput.value=h.formatDate(h.selectedDates[0],h.mobileFormatStr)),h.config.minDate&&(h.mobileInput.min=h.formatDate(h.config.minDate,"Y-m-d")),h.config.maxDate&&(h.mobileInput.max=h.formatDate(h.config.maxDate,"Y-m-d")),h.input.type="hidden",void 0!==h.altInput&&(h.altInput.type="hidden");try{h.input.parentNode&&h.input.parentNode.insertBefore(h.mobileInput,h.input.nextSibling)}catch(e){}F(h.mobileInput,"change",function(e){h.setDate(e.target.value,!1,h.mobileFormatStr),ge("onChange"),ge("onClose")})}();else{var e=r(ae,50);h._debouncedChange=r(Y,M),h.daysContainer&&!/iPhone|iPad|iPod/i.test(navigator.userAgent)&&F(h.daysContainer,"mouseover",function(e){"range"===h.config.mode&&ne(e.target)}),F(window.document.body,"keydown",te),h.config.inline||h.config.static||F(window,"resize",e),void 0!==window.ontouchstart?F(window.document,"touchstart",Z):F(window.document,"mousedown",N(Z)),F(window.document,"focus",Z,{capture:!0}),!0===h.config.clickOpens&&(F(h._input,"focus",h.open),F(h._input,"mousedown",N(h.open))),void 0!==h.daysContainer&&(F(h.monthNav,"mousedown",N(be)),F(h.monthNav,["keyup","increment"],_),F(h.daysContainer,"mousedown",N(se))),void 0!==h.timeContainer&&void 0!==h.minuteElement&&void 0!==h.hourElement&&(F(h.timeContainer,["increment"],T),F(h.timeContainer,"blur",T,{capture:!0}),F(h.timeContainer,"mousedown",N(P)),F([h.hourElement,h.minuteElement],["focus","click"],function(e){return e.target.select()}),void 0!==h.secondElement&&F(h.secondElement,"focus",function(){return h.secondElement&&h.secondElement.select()}),void 0!==h.amPM&&F(h.amPM,"mousedown",N(function(e){T(e),Y()})))}}(),(h.selectedDates.length||h.config.noCalendar)&&(h.config.enableTime&&I(h.config.noCalendar?h.latestSelectedDateObj||h.config.minDate:void 0),we(!1)),x(),h.showTimeInput=h.selectedDates.length>0||h.config.noCalendar;var a=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);!h.isMobile&&a&&le(),ge("onReady")}(),h}function x(e,t){for(var n=Array.prototype.slice.call(e).filter(function(e){return e instanceof HTMLElement}),a=[],i=0;i + + + + + + + + + + + + + + + + + + + diff --git a/netbox/project-static/img/netbox_logo.svg b/netbox/project-static/img/netbox_logo.svg new file mode 100644 index 000000000..5321be100 --- /dev/null +++ b/netbox/project-static/img/netbox_logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index ae3501cae..55f9afbd5 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -103,14 +103,16 @@ $(document).ready(function() { placeholder: "---------", theme: "bootstrap", templateResult: colorPickerClassCopy, - templateSelection: colorPickerClassCopy + templateSelection: colorPickerClassCopy, + width: "off" }); // Static choice selection $('.netbox-select2-static').select2({ allowClear: true, placeholder: "---------", - theme: "bootstrap" + theme: "bootstrap", + width: "off" }); // API backed selection @@ -120,6 +122,7 @@ $(document).ready(function() { allowClear: true, placeholder: "---------", theme: "bootstrap", + width: "off", ajax: { delay: 500, @@ -184,7 +187,15 @@ $(document).ready(function() { $.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; + if (param_name in parameters) { + if (Array.isArray(parameters[param_name])) { + parameters[param_name].push(attr.value) + } else { + parameters[param_name] = [parameters[param_name], attr.value] + } + } else { + parameters[param_name] = attr.value; + } } }); @@ -251,6 +262,24 @@ $(document).ready(function() { } }); + // Flatpickr selectors + $('.date-picker').flatpickr({ + allowInput: true + }); + $('.datetime-picker').flatpickr({ + allowInput: true, + enableSeconds: true, + enableTime: true, + time_24hr: true + }); + $('.time-picker').flatpickr({ + allowInput: true, + enableSeconds: true, + enableTime: true, + noCalendar: true, + time_24hr: true + }); + // API backed tags var tags = $('#id_tags'); if (tags.length > 0 && tags.val().length > 0){ @@ -273,7 +302,8 @@ $(document).ready(function() { multiple: true, allowClear: true, placeholder: "Tags", - + theme: "bootstrap", + width: "off", ajax: { delay: 250, url: netbox_api_path + "extras/tags/", @@ -353,4 +383,19 @@ $(document).ready(function() { }); $('select#id_mode').trigger('change'); } + + // Scroll up an offset equal to the first nav element if a hash is present + // Cannot use '#navbar' because it is not always visible, like in small windows + function headerOffsetScroll() { + if (window.location.hash) { + // Short wait needed to allow the page to scroll to the element + setTimeout(function() { + window.scrollBy(0, -$('nav').height()) + }, 10); + } + } + + // Account for the header height when hash-scrolling + window.addEventListener('load', headerOffsetScroll); + window.addEventListener('hashchange', headerOffsetScroll); }); diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 0dfd7c861..b1a713f4e 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -9,6 +9,7 @@ + @@ -69,6 +70,7 @@ +