diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..dfdb8b771 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index dc2b296ee..c79f65d53 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -680,13 +680,21 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm): class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm): - parent = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name', required=False, - error_messages={'invalid_choice': 'Parent device not found.'}) + parent = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + required=False, + error_messages={ + 'invalid_choice': 'Parent device not found.' + } + ) device_bay_name = forms.CharField(required=False) class Meta(BaseDeviceFromCSVForm.Meta): - fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', - 'parent', 'device_bay_name'] + fields = [ + 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'parent', + 'device_bay_name', + ] def clean(self): @@ -733,7 +741,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Device q = forms.CharField(required=False, label='Search') site = FilterChoiceField( - queryset=Site.objects.annotate(filter_count=Count('racks__devices')), + queryset=Site.objects.annotate(filter_count=Count('devices')), to_field_name='slug', ) rack_group_id = FilterChoiceField( @@ -1610,20 +1618,23 @@ class DeviceBayCreateForm(DeviceComponentForm): class PopulateDeviceBayForm(BootstrapMixin, forms.Form): - installed_device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Child Device', - help_text="Child devices must first be created within the rack occupied " - "by the parent device. Then they can be assigned to a bay.") + installed_device = forms.ModelChoiceField( + queryset=Device.objects.all(), + label='Child Device', + help_text="Child devices must first be created and assigned to the site/rack of the parent device." + ) def __init__(self, device_bay, *args, **kwargs): super(PopulateDeviceBayForm, self).__init__(*args, **kwargs) - children_queryset = Device.objects.filter(rack=device_bay.device.rack, - parent_bay__isnull=True, - device_type__u_height=0, - device_type__subdevice_role=SUBDEVICE_ROLE_CHILD)\ - .exclude(pk=device_bay.device.pk) - self.fields['installed_device'].queryset = children_queryset + self.fields['installed_device'].queryset = Device.objects.filter( + site=device_bay.device.site, + rack=device_bay.device.rack, + parent_bay__isnull=True, + device_type__u_height=0, + device_type__subdevice_role=SUBDEVICE_ROLE_CHILD + ).exclude(pk=device_bay.device.pk) # diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 03d5b2587..73678ae1c 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -975,25 +975,26 @@ class Device(CreatedUpdatedModel, CustomFieldModel): # Child devices cannot be assigned to a rack face/unit if self.device_type.is_child_device and self.face is not None: raise ValidationError({ - 'face': "Child device types cannot be assigned to a rack face. This is an attribute of the parent " - "device." + 'face': "Child device types cannot be assigned to a rack face. This is an attribute of the " + "parent device." }) if self.device_type.is_child_device and self.position: raise ValidationError({ - 'position': "Child device types cannot be assigned to a rack position. This is an attribute of the " - "parent device." + 'position': "Child device types cannot be assigned to a rack position. This is an attribute of " + "the parent device." }) # Validate rack space rack_face = self.face if not self.device_type.is_full_depth else None exclude_list = [self.pk] if self.pk else [] try: - available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face, - exclude=exclude_list) + available_units = self.rack.get_available_units( + u_height=self.device_type.u_height, rack_face=rack_face, exclude=exclude_list + ) if self.position and self.position not in available_units: raise ValidationError({ - 'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) {} " - "({}U).".format(self.position, self.device_type, self.device_type.u_height) + 'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) " + "{} ({}U).".format(self.position, self.device_type, self.device_type.u_height) }) except Rack.DoesNotExist: pass @@ -1034,8 +1035,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel): self.device_type.device_bay_templates.all()] ) - # Update Rack assignment for any child Devices - Device.objects.filter(parent_bay__device=self).update(rack=self.rack) + # Update Site and Rack assignment for any child Devices + Device.objects.filter(parent_bay__device=self).update(site=self.site, rack=self.rack) def to_csv(self): return csv_format([ @@ -1059,8 +1060,10 @@ class Device(CreatedUpdatedModel, CustomFieldModel): return self.name elif self.position: return u"{} ({} U{})".format(self.device_type, self.rack.name, self.position) - else: + elif self.rack: return u"{} ({})".format(self.device_type, self.rack.name) + else: + return u"{} ({})".format(self.device_type, self.site.name) @property def identifier(self): diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 73de16e53..cb307324e 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -763,9 +763,12 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView): default_return_url = 'dcim:device_list' def save_obj(self, obj): - # Inherent rack from parent device + + # Inherit site and rack from parent device + obj.site = obj.parent_bay.device.site obj.rack = obj.parent_bay.device.rack obj.save() + # Save the reverse relation device_bay = obj.parent_bay device_bay.installed_device = obj diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 51a5fed7f..66065e9e7 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.9.1' +VERSION = '1.9.2' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index bbfdee58d..9b1f81dad 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -23,7 +23,7 @@ _patterns = [ url(r'^ipam/', include('ipam.urls', namespace='ipam')), url(r'^secrets/', include('secrets.urls', namespace='secrets')), url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')), - url(r'^profile/', include('users.urls', namespace='users')), + url(r'^user/', include('users.urls', namespace='user')), # API url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')), diff --git a/netbox/secrets/decorators.py b/netbox/secrets/decorators.py index 41af204da..683805124 100644 --- a/netbox/secrets/decorators.py +++ b/netbox/secrets/decorators.py @@ -15,10 +15,10 @@ def userkey_required(): uk = UserKey.objects.get(user=request.user) except UserKey.DoesNotExist: messages.warning(request, u"This operation requires an active user key, but you don't have one.") - return redirect('users:userkey') + return redirect('user:userkey') if not uk.is_active(): messages.warning(request, u"This operation is not available. Your user key has not been activated.") - return redirect('users:userkey') + return redirect('user:userkey') return view(request, *args, **kwargs) return wrapped_view return _decorator diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index d1090a5fb..6f0dfced6 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -245,7 +245,7 @@ {% if request.user.is_staff %}
  • Admin
  • {% endif %} -
  • {{ request.user }}
  • +
  • {{ request.user }}
  • Log out
  • {% else %}
  • Log in
  • diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 081397774..d38f60cb3 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -43,8 +43,10 @@ {% if device.parent_bay %} {% with device.parent_bay.device as parent %} - U{{ parent.position }} / {{ parent.get_face_display }} - ({{ parent }} - {{ device.parent_bay.name }}) + {{ parent }} {{ device.parent_bay.name }} + {% if parent.position %} + (U{{ parent.position }} / {{ parent.get_face_display }}) + {% endif %} {% endwith %} {% elif device.rack and device.position %} U{{ device.position }} / {{ device.get_face_display }} diff --git a/netbox/templates/users/_user.html b/netbox/templates/users/_user.html index 01978a59e..a2e18daed 100644 --- a/netbox/templates/users/_user.html +++ b/netbox/templates/users/_user.html @@ -9,10 +9,10 @@
    diff --git a/netbox/templates/users/change_password.html b/netbox/templates/users/change_password.html index 80528c981..700bf682d 100644 --- a/netbox/templates/users/change_password.html +++ b/netbox/templates/users/change_password.html @@ -24,7 +24,7 @@
    - Cancel + Cancel
    {% endblock %} diff --git a/netbox/templates/users/userkey.html b/netbox/templates/users/userkey.html index 08c519c2d..df5e55be9 100644 --- a/netbox/templates/users/userkey.html +++ b/netbox/templates/users/userkey.html @@ -15,7 +15,7 @@

    Your public key is below.

    {{ userkey.public_key }}
    - + Edit user key @@ -24,7 +24,7 @@ {% else %}

    You don't have a user key on file.

    - + Create a User Key diff --git a/netbox/templates/users/userkey_edit.html b/netbox/templates/users/userkey_edit.html index 550ade4c2..45bac1938 100644 --- a/netbox/templates/users/userkey_edit.html +++ b/netbox/templates/users/userkey_edit.html @@ -23,7 +23,7 @@

    - Cancel + Cancel
    diff --git a/netbox/users/urls.py b/netbox/users/urls.py index d33d14beb..a9f7a1d7f 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -7,9 +7,9 @@ urlpatterns = [ # User profiles url(r'^profile/$', views.profile, name='profile'), - url(r'^profile/password/$', views.change_password, name='change_password'), - url(r'^profile/user-key/$', views.userkey, name='userkey'), - url(r'^profile/user-key/edit/$', views.userkey_edit, name='userkey_edit'), - url(r'^profile/recent-activity/$', views.recent_activity, name='recent_activity'), + url(r'^password/$', views.change_password, name='change_password'), + url(r'^user-key/$', views.userkey, name='userkey'), + url(r'^user-key/edit/$', views.userkey_edit, name='userkey_edit'), + url(r'^recent-activity/$', views.recent_activity, name='recent_activity'), ] diff --git a/netbox/users/views.py b/netbox/users/views.py index 95f5f3bfc..12a714696 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -69,7 +69,7 @@ def change_password(request): form.save() update_session_auth_hash(request, form.user) messages.success(request, u"Your password has been changed successfully.") - return redirect('users:profile') + return redirect('user:profile') else: form = PasswordChangeForm(user=request.user) @@ -109,7 +109,7 @@ def userkey_edit(request): uk.user = request.user uk.save() messages.success(request, u"Your user key has been saved.") - return redirect('users:userkey') + return redirect('user:userkey') else: form = UserKeyForm(instance=userkey) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 76ce1796c..dd6235f45 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -57,7 +57,7 @@ def parse_numeric_range(string, base=10): begin, end = dash_range.split('-') except ValueError: begin, end = dash_range, dash_range - begin, end = int(begin.strip()), int(end.strip(), base=base) + 1 + begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1 values.extend(range(begin, end)) return list(set(values)) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 4d6ec3332..f38d9a0ab 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -434,7 +434,7 @@ class BulkEditView(View): # Are we editing *all* objects in the queryset or just a selected subset? if request.POST.get('_all') and self.filter is not None: - pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk'))] + pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk')).qs] else: pk_list = [int(pk) for pk in request.POST.getlist('pk')] @@ -572,7 +572,7 @@ class BulkDeleteView(View): # Are we deleting *all* objects in the queryset or just a selected subset? if request.POST.get('_all') and self.filter is not None: - pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk'))] + pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk')).qs] else: pk_list = [int(pk) for pk in request.POST.getlist('pk')]