Coverage for ipam/forms/widgets.py: 35%

25 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1import json 

2 

3from django import forms 

4from django.forms.utils import flatatt 

5 

6from ipam.choices import ServiceProtocolChoices 

7 

8__all__ = ( 

9 'PortMappingWidget', 

10) 

11 

12 

13class PortMappingWidget(forms.Widget): 

14 """ 

15 Renders a dynamic set of (protocol, ports) rows. The rows are serialized to a JSON string held in a 

16 single hidden input (client-side JS keeps the hidden input in sync as rows are added/removed). Each 

17 row's ``ports`` value is a raw comma/range string (e.g. "80,443,8000-8010"); the server expands it. 

18 """ 

19 template_name = 'ipam/widgets/port_mappings.html' 

20 

21 # aria-* attributes which render_field_with_aria() sets per-field, and which must be copied onto the 

22 # row controls: the wrapping <div> isn't a form control, so assistive technology ignores them there. 

23 CONTROL_ATTRS = ('aria-describedby', 'aria-invalid') 

24 

25 def id_for_label(self, id_): 

26 # The field's <label for="..."> must point at a real form control, not the wrapping <div> which 

27 # carries the widget's id. Target the first row's protocol <select>; the client-side widget keeps 

28 # this id on whichever row is first as rows are added and removed. 

29 return f'{id_}_protocol_0' if id_ else '' 

30 

31 def get_context(self, name, value, attrs): 

32 attrs = attrs or {} 

33 rows = [] 

34 if value: 

35 try: 

36 rows = json.loads(value) 

37 except (TypeError, ValueError): 

38 rows = [] 

39 # Re-rendering an invalid bound form hands us back the raw POST value, which need not be the list 

40 # of {protocol, ports} objects the JS produces: a crafted payload of e.g. `5` or `{"a": 1}` parses 

41 # as valid JSON but would break the template's row loop. Discard anything of the wrong shape and 

42 # fall through to the blank row below. 

43 if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): 

44 rows = [] 

45 # Always render at least one (blank) row so the entry fields are visible on an empty form 

46 if not rows: 

47 rows = [{'protocol': '', 'ports': ''}] 

48 return { 

49 'widget': { 

50 'name': name, 

51 'value': value or '[]', 

52 'rows': rows, 

53 'attrs': attrs, 

54 'label_id': self.id_for_label(attrs.get('id')), 

55 'control_attrs': flatatt({ 

56 key: value for key, value in attrs.items() if key in self.CONTROL_ATTRS 

57 }), 

58 }, 

59 'protocol_choices': list(ServiceProtocolChoices), 

60 } 

61 

62 def value_from_datadict(self, data, files, name): 

63 return data.get(name)