def test_select_field(self): obj_type = ContentType.objects.get_for_model(Site) # Create a custom field cf = CustomField(type=CF_TYPE_SELECT, name='my_field', required=False) cf.save() cf.obj_type = [obj_type] cf.save() # Create some choices for the field CustomFieldChoice.objects.bulk_create([ CustomFieldChoice(field=cf, value='Option A'), CustomFieldChoice(field=cf, value='Option B'), CustomFieldChoice(field=cf, value='Option C'), ]) # Assign a value to the first Site site = Site.objects.first() cfv = CustomFieldValue(field=cf, obj_type=obj_type, obj_id=site.id) cfv.value = cf.choices.first() cfv.save() # Retrieve the stored value cfv = CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).first() self.assertEqual(str(cfv.value), 'Option A') # Delete the stored value cfv.value = None cfv.save() self.assertEqual(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).count(), 0) # Delete the custom field cf.delete()
def test_get_obj_with_custom_fields(self): CUSTOM_FIELD_VALUES = [ (self.cf_text, 'Test string'), (self.cf_integer, 1234), (self.cf_boolean, True), (self.cf_date, date(2016, 6, 23)), (self.cf_url, 'http://example.com/'), (self.cf_select, self.cf_select_choice1.pk), ] for field, value in CUSTOM_FIELD_VALUES: cfv = CustomFieldValue(field=field, obj=self.site) cfv.value = value cfv.save() url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) response = self.client.get(url, **self.header) self.assertEqual(response.data['name'], self.site.name) self.assertEqual(response.data['custom_fields'].get('magic_word'), CUSTOM_FIELD_VALUES[0][1]) self.assertEqual(response.data['custom_fields'].get('magic_number'), CUSTOM_FIELD_VALUES[1][1]) self.assertEqual(response.data['custom_fields'].get('is_magic'), CUSTOM_FIELD_VALUES[2][1]) self.assertEqual(response.data['custom_fields'].get('magic_date'), CUSTOM_FIELD_VALUES[3][1]) self.assertEqual(response.data['custom_fields'].get('magic_url'), CUSTOM_FIELD_VALUES[4][1]) self.assertEqual(response.data['custom_fields'].get('magic_choice'), { 'value': self.cf_select_choice1.pk, 'label': 'Foo' })
def test_select_field(self): obj_type = ContentType.objects.get_for_model(Site) # Create a custom field cf = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='my_field', required=False) cf.save() cf.obj_type.set([obj_type]) cf.save() # Create some choices for the field CustomFieldChoice.objects.bulk_create([ CustomFieldChoice(field=cf, value='Option A'), CustomFieldChoice(field=cf, value='Option B'), CustomFieldChoice(field=cf, value='Option C'), ]) # Assign a value to the first Site site = Site.objects.first() cfv = CustomFieldValue(field=cf, obj_type=obj_type, obj_id=site.id) cfv.value = cf.choices.first() cfv.save() # Retrieve the stored value cfv = CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).first() self.assertEqual(str(cfv.value), 'Option A') # Delete the stored value cfv.value = None cfv.save() self.assertEqual(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).count(), 0) # Delete the custom field cf.delete()
def test_get_obj_with_custom_fields(self): CUSTOM_FIELD_VALUES = [ (self.cf_text, 'Test string'), (self.cf_integer, 1234), (self.cf_boolean, True), (self.cf_date, date(2016, 6, 23)), (self.cf_url, 'http://example.com/'), (self.cf_select, self.cf_select_choice1.pk), ] for field, value in CUSTOM_FIELD_VALUES: cfv = CustomFieldValue(field=field, obj=self.site) cfv.value = value cfv.save() url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) response = self.client.get(url, **self.header) self.assertEqual(response.data['name'], self.site.name) self.assertEqual(response.data['custom_fields'].get('magic_word'), CUSTOM_FIELD_VALUES[0][1]) self.assertEqual(response.data['custom_fields'].get('magic_number'), CUSTOM_FIELD_VALUES[1][1]) self.assertEqual(response.data['custom_fields'].get('is_magic'), CUSTOM_FIELD_VALUES[2][1]) self.assertEqual(response.data['custom_fields'].get('magic_date'), CUSTOM_FIELD_VALUES[3][1]) self.assertEqual(response.data['custom_fields'].get('magic_url'), CUSTOM_FIELD_VALUES[4][1]) self.assertEqual(response.data['custom_fields'].get('magic_choice'), { 'value': self.cf_select_choice1.pk, 'label': 'Foo' })
def save_coords(self, request): results = {} if settings.PLUGINS_CONFIG["netbox_topology_views"][ "allow_coordinates_saving"]: try: cfCoords = CustomField.objects.get(name='coordinates') except CustomField.DoesNotExist: results["status"] = "coords custom field not created" return Response(status=500) obj_type = ContentType.objects.get_for_model(Device) device_id = None x_coord = None y_coord = None if "node_id" in request.data: if request.data["node_id"]: device_id = request.data["node_id"] if "x" in request.data: if request.data["x"]: x_coord = request.data["x"] if "y" in request.data: if request.data["y"]: y_coord = request.data["y"] actual_device = Device.objects.get(id=device_id) try: cfvCoords = CustomFieldValue.objects.get( obj_type=obj_type, obj_id=actual_device.pk, field__name='coordinates') cfvCoords.value = "%s;%s" % (x_coord, y_coord) cfvCoords.save() results["status"] = "coords set" except CustomFieldValue.DoesNotExist: cfvCoords = CustomFieldValue(field=cfCoords, obj_type=obj_type, obj_id=actual_device.id) cfvCoords.value = "%s;%s" % (x_coord, y_coord) cfvCoords.save() results["status"] = "coords set for first time" return Response(results) else: results["status"] = "not allowed to save coords" return Response(results, status=500)
def test_simple_fields(self): DATA = ( {'field_type': CF_TYPE_TEXT, 'field_value': 'Foobar!', 'empty_value': ''}, {'field_type': CF_TYPE_INTEGER, 'field_value': 0, 'empty_value': None}, {'field_type': CF_TYPE_INTEGER, 'field_value': 42, 'empty_value': None}, {'field_type': CF_TYPE_BOOLEAN, 'field_value': True, 'empty_value': None}, {'field_type': CF_TYPE_BOOLEAN, 'field_value': False, 'empty_value': None}, {'field_type': CF_TYPE_DATE, 'field_value': date( 2016, 6, 23), 'empty_value': None}, {'field_type': CF_TYPE_URL, 'field_value': 'http://example.com/', 'empty_value': ''}, ) obj_type = ContentType.objects.get_for_model(Site) for data in DATA: # Create a custom field cf = CustomField(type=data['field_type'], name='my_field', required=False) cf.save() cf.obj_type = [obj_type] cf.save() # Assign a value to the first Site site = Site.objects.first() cfv = CustomFieldValue(field=cf, obj_type=obj_type, obj_id=site.id) cfv.value = data['field_value'] cfv.save() # Retrieve the stored value cfv = CustomFieldValue.objects.filter( obj_type=obj_type, obj_id=site.pk).first() self.assertEqual(cfv.value, data['field_value']) # Delete the stored value cfv.value = data['empty_value'] cfv.save() self.assertEqual(CustomFieldValue.objects.filter( obj_type=obj_type, obj_id=site.pk).count(), 0) # Delete the custom field cf.delete()
def test_simple_fields(self): DATA = ( {'field_type': CF_TYPE_TEXT, 'field_value': 'Foobar!', 'empty_value': ''}, {'field_type': CF_TYPE_INTEGER, 'field_value': 0, 'empty_value': None}, {'field_type': CF_TYPE_INTEGER, 'field_value': 42, 'empty_value': None}, {'field_type': CF_TYPE_BOOLEAN, 'field_value': True, 'empty_value': None}, {'field_type': CF_TYPE_BOOLEAN, 'field_value': False, 'empty_value': None}, {'field_type': CF_TYPE_DATE, 'field_value': date(2016, 6, 23), 'empty_value': None}, {'field_type': CF_TYPE_URL, 'field_value': 'http://example.com/', 'empty_value': ''}, ) obj_type = ContentType.objects.get_for_model(Site) for data in DATA: # Create a custom field cf = CustomField(type=data['field_type'], name='my_field', required=False) cf.save() cf.obj_type = [obj_type] cf.save() # Assign a value to the first Site site = Site.objects.first() cfv = CustomFieldValue(field=cf, obj_type=obj_type, obj_id=site.id) cfv.value = data['field_value'] cfv.save() # Retrieve the stored value cfv = CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).first() self.assertEqual(cfv.value, data['field_value']) # Delete the stored value cfv.value = data['empty_value'] cfv.save() self.assertEqual(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).count(), 0) # Delete the custom field cf.delete()
def post(self, request, **kwargs): logger = logging.getLogger('netbox.views.BulkEditView') model = self.queryset.model # If we are editing *all* objects in the queryset, replace the PK list with all matched objects. if request.POST.get('_all') and self.filterset is not None: pk_list = [ obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs ] else: pk_list = request.POST.getlist('pk') if '_apply' in request.POST: form = self.form(model, request.POST) if form.is_valid(): logger.debug("Form validation was successful") custom_fields = form.custom_fields if hasattr( form, 'custom_fields') else [] standard_fields = [ field for field in form.fields if field not in custom_fields + ['pk'] ] nullified_fields = request.POST.getlist('_nullify') try: with transaction.atomic(): updated_count = 0 for obj in model.objects.filter( pk__in=form.cleaned_data['pk']): # Update standard fields. If a field is listed in _nullify, delete its value. for name in standard_fields: try: model_field = model._meta.get_field(name) except FieldDoesNotExist: # This form field is used to modify a field rather than set its value directly model_field = None # Handle nullification if name in form.nullable_fields and name in nullified_fields: if isinstance(model_field, ManyToManyField): getattr(obj, name).set([]) else: setattr( obj, name, None if model_field.null else '') # ManyToManyFields elif isinstance(model_field, ManyToManyField): getattr(obj, name).set(form.cleaned_data[name]) # Normal fields elif form.cleaned_data[name] not in (None, ''): setattr(obj, name, form.cleaned_data[name]) obj.full_clean() obj.save() logger.debug(f"Saved {obj} (PK: {obj.pk})") # Update custom fields obj_type = ContentType.objects.get_for_model(model) for name in custom_fields: field = form.fields[name].model if name in form.nullable_fields and name in nullified_fields: CustomFieldValue.objects.filter( field=field, obj_type=obj_type, obj_id=obj.pk).delete() elif form.cleaned_data[name] not in [None, '']: try: cfv = CustomFieldValue.objects.get( field=field, obj_type=obj_type, obj_id=obj.pk) except CustomFieldValue.DoesNotExist: cfv = CustomFieldValue( field=field, obj_type=obj_type, obj_id=obj.pk) cfv.value = form.cleaned_data[name] cfv.save() logger.debug( f"Saved custom fields for {obj} (PK: {obj.pk})" ) # Add/remove tags if form.cleaned_data.get('add_tags', None): obj.tags.add(*form.cleaned_data['add_tags']) if form.cleaned_data.get('remove_tags', None): obj.tags.remove( *form.cleaned_data['remove_tags']) updated_count += 1 if updated_count: msg = 'Updated {} {}'.format( updated_count, model._meta.verbose_name_plural) logger.info(msg) messages.success(self.request, msg) return redirect(self.get_return_url(request)) except ValidationError as e: messages.error(self.request, "{} failed validation: {}".format(obj, e)) else: logger.debug("Form validation failed") else: # Include the PK list as initial data for the form initial_data = {'pk': pk_list} # Check for other contextual data needed for the form. We avoid passing all of request.GET because the # filter values will conflict with the bulk edit form fields. # TODO: Find a better way to accomplish this if 'device' in request.GET: initial_data['device'] = request.GET.get('device') elif 'device_type' in request.GET: initial_data['device_type'] = request.GET.get('device_type') form = self.form(model, initial=initial_data) # Retrieve objects being edited table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False) if not table.rows: messages.warning( request, "No {} were selected.".format(model._meta.verbose_name_plural)) return redirect(self.get_return_url(request)) return render( request, self.template_name, { 'form': form, 'table': table, 'obj_type_plural': model._meta.verbose_name_plural, 'return_url': self.get_return_url(request), })
def post(self, request, **kwargs): model = self.queryset.model # Create a mutable copy of the POST data post_data = request.POST.copy() # If we are editing *all* objects in the queryset, replace the PK list with all matched objects. if post_data.get('_all') and self.filterset is not None: post_data['pk'] = [ obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs ] if '_apply' in request.POST: form = self.form(model, request.POST) if form.is_valid(): custom_fields = form.custom_fields if hasattr( form, 'custom_fields') else [] standard_fields = [ field for field in form.fields if field not in custom_fields + ['pk'] ] nullified_fields = request.POST.getlist('_nullify') try: with transaction.atomic(): updated_count = 0 for obj in model.objects.filter( pk__in=form.cleaned_data['pk']): # Update standard fields. If a field is listed in _nullify, delete its value. for name in standard_fields: try: model_field = model._meta.get_field(name) except FieldDoesNotExist: # This form field is used to modify a field rather than set its value directly model_field = None # Handle nullification if name in form.nullable_fields and name in nullified_fields: if isinstance(model_field, ManyToManyField): getattr(obj, name).set([]) else: setattr( obj, name, None if model_field.null else '') # ManyToManyFields elif isinstance(model_field, ManyToManyField): getattr(obj, name).set(form.cleaned_data[name]) # Normal fields elif form.cleaned_data[name] not in (None, ''): setattr(obj, name, form.cleaned_data[name]) obj.full_clean() obj.save() # Update custom fields obj_type = ContentType.objects.get_for_model(model) for name in custom_fields: field = form.fields[name].model if name in form.nullable_fields and name in nullified_fields: CustomFieldValue.objects.filter( field=field, obj_type=obj_type, obj_id=obj.pk).delete() elif form.cleaned_data[name] not in [None, '']: try: cfv = CustomFieldValue.objects.get( field=field, obj_type=obj_type, obj_id=obj.pk) except CustomFieldValue.DoesNotExist: cfv = CustomFieldValue( field=field, obj_type=obj_type, obj_id=obj.pk) cfv.value = form.cleaned_data[name] cfv.save() # Add/remove tags if form.cleaned_data.get('add_tags', None): obj.tags.add(*form.cleaned_data['add_tags']) if form.cleaned_data.get('remove_tags', None): obj.tags.remove( *form.cleaned_data['remove_tags']) updated_count += 1 if updated_count: msg = 'Updated {} {}'.format( updated_count, model._meta.verbose_name_plural) messages.success(self.request, msg) return redirect(self.get_return_url(request)) except ValidationError as e: messages.error(self.request, "{} failed validation: {}".format(obj, e)) else: # Pass the PK list as initial data to avoid binding the form initial_data = querydict_to_dict(post_data) form = self.form(model, initial=initial_data) # Retrieve objects being edited table = self.table( self.queryset.filter(pk__in=post_data.getlist('pk')), orderable=False) if not table.rows: messages.warning( request, "No {} were selected.".format(model._meta.verbose_name_plural)) return redirect(self.get_return_url(request)) return render( request, self.template_name, { 'form': form, 'table': table, 'obj_type_plural': model._meta.verbose_name_plural, 'return_url': self.get_return_url(request), })
def post(self, request, **kwargs): # Attempt to derive parent object if a parent class has been given if self.parent_cls: parent_obj = get_object_or_404(self.parent_cls, **kwargs) else: parent_obj = None # Determine URL to redirect users upon modification of objects posted_return_url = request.POST.get('return_url') if posted_return_url and is_safe_url(url=posted_return_url, host=request.get_host()): return_url = posted_return_url elif parent_obj: return_url = parent_obj.get_absolute_url() else: return_url = reverse(self.default_return_url) # 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')).qs ] else: pk_list = [int(pk) for pk in request.POST.getlist('pk')] if '_apply' in request.POST: form = self.form(self.cls, parent_obj, request.POST) if form.is_valid(): custom_fields = form.custom_fields if hasattr( form, 'custom_fields') else [] standard_fields = [ field for field in form.fields if field not in custom_fields and field != 'pk' ] nullified_fields = request.POST.getlist('_nullify') try: with transaction.atomic(): updated_count = 0 for obj in self.cls.objects.filter(pk__in=pk_list): # Update standard fields. If a field is listed in _nullify, delete its value. for name in standard_fields: if name in form.nullable_fields and name in nullified_fields: setattr( obj, name, '' if isinstance( form.fields[name], CharField) else None) elif form.cleaned_data[name] not in (None, ''): setattr(obj, name, form.cleaned_data[name]) obj.full_clean() obj.save() # Update custom fields obj_type = ContentType.objects.get_for_model( self.cls) for name in custom_fields: field = form.fields[name].model if name in form.nullable_fields and name in nullified_fields: CustomFieldValue.objects.filter( field=field, obj_type=obj_type, obj_id=obj.pk).delete() elif form.cleaned_data[name] not in [None, '']: try: cfv = CustomFieldValue.objects.get( field=field, obj_type=obj_type, obj_id=obj.pk) except CustomFieldValue.DoesNotExist: cfv = CustomFieldValue( field=field, obj_type=obj_type, obj_id=obj.pk) cfv.value = form.cleaned_data[name] cfv.save() updated_count += 1 if updated_count: msg = 'Updated {} {}'.format( updated_count, self.cls._meta.verbose_name_plural) messages.success(self.request, msg) UserAction.objects.log_bulk_edit( request.user, ContentType.objects.get_for_model(self.cls), msg) return redirect(return_url) except ValidationError as e: messages.error(self.request, "{} failed validation: {}".format(obj, e)) else: initial_data = request.POST.copy() initial_data['pk'] = pk_list form = self.form(self.cls, parent_obj, initial=initial_data) # Retrieve objects being edited queryset = self.queryset or self.cls.objects.all() table = self.table(queryset.filter(pk__in=pk_list), orderable=False) if not table.rows: messages.warning( request, "No {} were selected.".format( self.cls._meta.verbose_name_plural)) return redirect(return_url) return render( request, self.template_name, { 'form': form, 'table': table, 'obj_type_plural': self.cls._meta.verbose_name_plural, 'return_url': return_url, })
def setUpTestData(cls): content_type = ContentType.objects.get_for_model(Site) # Text custom field cls.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo') cls.cf_text.save() cls.cf_text.obj_type.set([content_type]) # Integer custom field cls.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='number_field', default=123) cls.cf_integer.save() cls.cf_integer.obj_type.set([content_type]) # Boolean custom field cls.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False) cls.cf_boolean.save() cls.cf_boolean.obj_type.set([content_type]) # Date custom field cls.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01') cls.cf_date.save() cls.cf_date.obj_type.set([content_type]) # URL custom field cls.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1') cls.cf_url.save() cls.cf_url.obj_type.set([content_type]) # Select custom field cls.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='choice_field') cls.cf_select.save() cls.cf_select.obj_type.set([content_type]) cls.cf_select_choice1 = CustomFieldChoice(field=cls.cf_select, value='Foo') cls.cf_select_choice1.save() cls.cf_select_choice2 = CustomFieldChoice(field=cls.cf_select, value='Bar') cls.cf_select_choice2.save() cls.cf_select_choice3 = CustomFieldChoice(field=cls.cf_select, value='Baz') cls.cf_select_choice3.save() cls.cf_select.default = cls.cf_select_choice1.value cls.cf_select.save() # Create some sites cls.sites = ( Site(name='Site 1', slug='site-1'), Site(name='Site 2', slug='site-2'), ) Site.objects.bulk_create(cls.sites) # Assign custom field values for site 2 site2_cfvs = { cls.cf_text: 'bar', cls.cf_integer: 456, cls.cf_boolean: True, cls.cf_date: '2020-01-02', cls.cf_url: 'http://example.com/2', cls.cf_select: cls.cf_select_choice2.pk, } for field, value in site2_cfvs.items(): cfv = CustomFieldValue(field=field, obj=cls.sites[1]) cfv.value = value cfv.save()
def post(self, request, **kwargs): model = self.queryset.model # Attempt to derive parent object if a parent class has been given if self.parent_model: parent_obj = get_object_or_404(self.parent_model, **kwargs) else: parent_obj = None # Are we editing *all* objects in the queryset or just a selected subset? if request.POST.get('_all') and self.filterset is not None: pk_list = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs] else: pk_list = [int(pk) for pk in request.POST.getlist('pk')] if '_apply' in request.POST: form = self.form(model, parent_obj, request.POST) if form.is_valid(): custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else [] standard_fields = [field for field in form.fields if field not in custom_fields and field != 'pk'] nullified_fields = request.POST.getlist('_nullify') try: with transaction.atomic(): updated_count = 0 for obj in model.objects.filter(pk__in=pk_list): # Update standard fields. If a field is listed in _nullify, delete its value. for name in standard_fields: if name in form.nullable_fields and name in nullified_fields and isinstance(form.cleaned_data[name], QuerySet): getattr(obj, name).set([]) elif name in form.nullable_fields and name in nullified_fields: setattr(obj, name, '' if isinstance(form.fields[name], CharField) else None) elif isinstance(form.cleaned_data[name], QuerySet) and form.cleaned_data[name]: getattr(obj, name).set(form.cleaned_data[name]) elif form.cleaned_data[name] not in (None, '') and not isinstance(form.cleaned_data[name], QuerySet): setattr(obj, name, form.cleaned_data[name]) obj.full_clean() obj.save() # Update custom fields obj_type = ContentType.objects.get_for_model(model) for name in custom_fields: field = form.fields[name].model if name in form.nullable_fields and name in nullified_fields: CustomFieldValue.objects.filter( field=field, obj_type=obj_type, obj_id=obj.pk ).delete() elif form.cleaned_data[name] not in [None, '']: try: cfv = CustomFieldValue.objects.get( field=field, obj_type=obj_type, obj_id=obj.pk ) except CustomFieldValue.DoesNotExist: cfv = CustomFieldValue( field=field, obj_type=obj_type, obj_id=obj.pk ) cfv.value = form.cleaned_data[name] cfv.save() # Add/remove tags if form.cleaned_data.get('add_tags', None): obj.tags.add(*form.cleaned_data['add_tags']) if form.cleaned_data.get('remove_tags', None): obj.tags.remove(*form.cleaned_data['remove_tags']) updated_count += 1 if updated_count: msg = 'Updated {} {}'.format(updated_count, model._meta.verbose_name_plural) messages.success(self.request, msg) return redirect(self.get_return_url(request)) except ValidationError as e: messages.error(self.request, "{} failed validation: {}".format(obj, e)) else: initial_data = request.POST.copy() initial_data['pk'] = pk_list form = self.form(model, parent_obj, initial=initial_data) # Retrieve objects being edited table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False) if not table.rows: messages.warning(request, "No {} were selected.".format(model._meta.verbose_name_plural)) return redirect(self.get_return_url(request)) return render(request, self.template_name, { 'form': form, 'table': table, 'obj_type_plural': model._meta.verbose_name_plural, 'return_url': self.get_return_url(request), })
def post(self, request, **kwargs): # Attempt to derive parent object if a parent class has been given if self.parent_cls: parent_obj = get_object_or_404(self.parent_cls, **kwargs) else: parent_obj = None # Determine URL to redirect users upon modification of objects posted_return_url = request.POST.get('return_url') if posted_return_url and is_safe_url(url=posted_return_url, host=request.get_host()): return_url = posted_return_url elif parent_obj: return_url = parent_obj.get_absolute_url() else: return_url = reverse(self.default_return_url) # 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')).qs] else: pk_list = [int(pk) for pk in request.POST.getlist('pk')] if '_apply' in request.POST: form = self.form(self.cls, parent_obj, request.POST) if form.is_valid(): custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else [] standard_fields = [field for field in form.fields if field not in custom_fields and field != 'pk'] nullified_fields = request.POST.getlist('_nullify') try: with transaction.atomic(): updated_count = 0 for obj in self.cls.objects.filter(pk__in=pk_list): # Update standard fields. If a field is listed in _nullify, delete its value. for name in standard_fields: if name in form.nullable_fields and name in nullified_fields: setattr(obj, name, '' if isinstance(form.fields[name], CharField) else None) elif form.cleaned_data[name] not in (None, ''): setattr(obj, name, form.cleaned_data[name]) obj.full_clean() obj.save() # Update custom fields obj_type = ContentType.objects.get_for_model(self.cls) for name in custom_fields: field = form.fields[name].model if name in form.nullable_fields and name in nullified_fields: CustomFieldValue.objects.filter( field=field, obj_type=obj_type, obj_id=obj.pk ).delete() elif form.cleaned_data[name] not in [None, '']: try: cfv = CustomFieldValue.objects.get( field=field, obj_type=obj_type, obj_id=obj.pk ) except CustomFieldValue.DoesNotExist: cfv = CustomFieldValue( field=field, obj_type=obj_type, obj_id=obj.pk ) cfv.value = form.cleaned_data[name] cfv.save() updated_count += 1 if updated_count: msg = 'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural) messages.success(self.request, msg) UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg) return redirect(return_url) except ValidationError as e: messages.error(self.request, "{} failed validation: {}".format(obj, e)) else: initial_data = request.POST.copy() initial_data['pk'] = pk_list form = self.form(self.cls, parent_obj, initial=initial_data) # Retrieve objects being edited queryset = self.queryset or self.cls.objects.all() table = self.table(queryset.filter(pk__in=pk_list), orderable=False) if not table.rows: messages.warning(request, "No {} were selected.".format(self.cls._meta.verbose_name_plural)) return redirect(return_url) return render(request, self.template_name, { 'form': form, 'table': table, 'obj_type_plural': self.cls._meta.verbose_name_plural, 'return_url': return_url, })