diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 19985132c..ed9cf318f 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -20,7 +20,7 @@ from extras.api.customfields import CustomFieldModelSerializer from ipam.models import IPAddress, VLAN from tenancy.api.serializers import NestedTenantSerializer from users.api.serializers import NestedUserSerializer -from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer +from utilities.api import ChoiceFieldSerializer, TimeZoneField, ValidatedModelSerializer from virtualization.models import Cluster @@ -58,13 +58,14 @@ class WritableRegionSerializer(ValidatedModelSerializer): class SiteSerializer(CustomFieldModelSerializer): region = NestedRegionSerializer() tenant = NestedTenantSerializer() + time_zone = TimeZoneField(required=False) class Meta: model = Site fields = [ - 'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', - 'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes', - 'count_vlans', 'count_racks', 'count_devices', 'count_circuits', + 'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'physical_address', + 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', + 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits', ] @@ -77,12 +78,13 @@ class NestedSiteSerializer(serializers.ModelSerializer): class WritableSiteSerializer(CustomFieldModelSerializer): + time_zone = TimeZoneField(required=False) class Meta: model = Site fields = [ - 'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', - 'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', + 'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'physical_address', + 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', ] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 1a7c3837b..2bca68bd3 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -7,6 +7,7 @@ from django.contrib.auth.models import User from django.contrib.postgres.forms.array import SimpleArrayField from django.db.models import Count, Q from mptt.forms import TreeNodeChoiceField +from timezone_field import TimeZoneFormField from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from ipam.models import IPAddress, VLAN, VLANGroup @@ -96,7 +97,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): model = Site fields = [ 'name', 'slug', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'physical_address', - 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', + 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'time_zone', 'comments', ] widgets = { 'physical_address': SmallTextarea(attrs={'rows': 3}), @@ -135,7 +136,7 @@ class SiteCSVForm(forms.ModelForm): model = Site fields = [ 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', - 'contact_name', 'contact_phone', 'contact_email', 'comments', + 'contact_name', 'contact_phone', 'contact_email', 'time_zone', 'comments', ] help_texts = { 'name': 'Site name', @@ -149,9 +150,10 @@ class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN') + time_zone = TimeZoneFormField(required=False) class Meta: - nullable_fields = ['region', 'tenant', 'asn'] + nullable_fields = ['region', 'tenant', 'asn', 'time_zone'] class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): diff --git a/netbox/dcim/migrations/0054_site_time_zone.py b/netbox/dcim/migrations/0054_site_time_zone.py new file mode 100644 index 000000000..f599cb155 --- /dev/null +++ b/netbox/dcim/migrations/0054_site_time_zone.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-12-19 21:53 +from __future__ import unicode_literals + +from django.db import migrations +import timezone_field.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0053_platform_manufacturer'), + ] + + operations = [ + migrations.AddField( + model_name='site', + name='time_zone', + field=timezone_field.fields.TimeZoneField(blank=True), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index b35d0b078..8fb58a081 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -14,6 +14,7 @@ from django.db.models import Count, Q, ObjectDoesNotExist from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from mptt.models import MPTTModel, TreeForeignKey +from timezone_field import TimeZoneField from circuits.models import Circuit from extras.models import CustomFieldModel, CustomFieldValue, ImageAttachment @@ -86,6 +87,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): tenant = models.ForeignKey(Tenant, related_name='sites', blank=True, null=True, on_delete=models.PROTECT) facility = models.CharField(max_length=50, blank=True) asn = ASNField(blank=True, null=True, verbose_name='ASN') + time_zone = TimeZoneField(blank=True) physical_address = models.CharField(max_length=200, blank=True) shipping_address = models.CharField(max_length=200, blank=True) contact_name = models.CharField(max_length=50, blank=True) @@ -98,7 +100,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel): objects = SiteManager() csv_headers = [ - 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email', + 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'contact_name', 'contact_phone', + 'contact_email', ] class Meta: @@ -118,6 +121,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): self.tenant.name if self.tenant else None, self.facility, self.asn, + self.time_zone, self.contact_name, self.contact_phone, self.contact_email, diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index ac1aba492..7a3f6cbec 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -134,6 +134,7 @@ INSTALLED_APPS = ( 'mptt', 'rest_framework', 'rest_framework_swagger', + 'timezone_field', 'circuits', 'dcim', 'ipam', diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index efc98c3d0..c61f007df 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -1,5 +1,6 @@ {% extends '_base.html' %} {% load static from staticfiles %} +{% load tz %} {% load helpers %} {% block content %} @@ -105,6 +106,17 @@ {% endif %} + + Time Zone + + {% if site.time_zone %} + {{ site.time_zone }} (UTC {{ site.time_zone|tzoffset }})
+ Site time: {% timezone site.time_zone %}{% now "SHORT_DATETIME_FORMAT" %}{% endtimezone %} + {% else %} + N/A + {% endif %} + +
diff --git a/netbox/templates/dcim/site_edit.html b/netbox/templates/dcim/site_edit.html index a1c13075a..21b78f229 100644 --- a/netbox/templates/dcim/site_edit.html +++ b/netbox/templates/dcim/site_edit.html @@ -10,6 +10,7 @@ {% render_field form.region %} {% render_field form.facility %} {% render_field form.asn %} + {% render_field form.time_zone %}
diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 4f5ce4471..9dccdcc9d 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from collections import OrderedDict +import pytz from django.conf import settings from django.contrib.contenttypes.models import ContentType @@ -97,6 +98,23 @@ class ContentTypeFieldSerializer(Field): raise ValidationError("Invalid content type") +class TimeZoneField(Field): + """ + Represent a pytz time zone. + """ + + def to_representation(self, obj): + return obj.zone if obj else None + + def to_internal_value(self, data): + if not data: + return "" + try: + return pytz.timezone(str(data)) + except pytz.exceptions.UnknownTimeZoneError: + raise ValidationError('Invalid time zone "{}"'.format(data)) + + # # Viewsets # diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 2af936885..2dd726195 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -1,5 +1,8 @@ from __future__ import unicode_literals +import datetime +import pytz + from django import template from django.utils.safestring import mark_safe from markdown import markdown @@ -117,6 +120,14 @@ def example_choices(field, arg=3): return ', '.join(examples) or 'None' +@register.filter() +def tzoffset(value): + """ + Returns the hour offset of a given time zone using the current time. + """ + return datetime.datetime.now(value).strftime('%z') + + # # Tags # diff --git a/requirements.txt b/requirements.txt index 303d2ad47..a65988307 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ django-filter>=1.1.0 django-mptt==0.8.7 django-rest-swagger>=2.1.0 django-tables2>=1.10.0 +django-timezone-field>=2.0 djangorestframework>=3.6.4 graphviz>=0.6 Markdown>=2.6.7