Coverage for audoma/links.py: 82%

80 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-08-08 06:12 +0000

1""" 

2This module is responsible for creating x-choices links. 

3Such links may be used to generate choices enums for fields. 

4 

5If some field in the serializer has this attribute, it 

6will be used to generate choices for this field. 

7In other words choices should be limited to values 

8available under passed x-choices link. 

9""" 

10 

11import re 

12from dataclasses import dataclass 

13from typing import ( 

14 Any, 

15 Dict, 

16 Type, 

17 Union, 

18) 

19 

20from drf_spectacular.plumbing import force_instance 

21from rest_framework.serializers import BaseSerializer 

22 

23from django.urls import ( 

24 NoReverseMatch, 

25 URLResolver, 

26) 

27from django.urls.resolvers import get_resolver 

28 

29 

30def get_endpoint_pattern(endpoint_name: str, urlconf=None) -> str: 

31 """ 

32 This methods retrieves url pattern of the endpoint by given endpoint_name. 

33 

34 Args: 

35 * endpoint_name: name of the endpoint 

36 * urlconf: urlconf to use 

37 

38 Returns: url pattern of the endpoint 

39 """ 

40 resolver = get_resolver(urlconf) 

41 patterns = resolver.url_patterns 

42 new_patterns = [] 

43 resolvers = [] 

44 

45 for pattern in patterns: 

46 if isinstance(pattern, URLResolver): 

47 resolvers.append(pattern) 

48 continue 

49 new_patterns.append(pattern) 

50 

51 while resolvers: 

52 resolver = resolvers.pop() 

53 patterns = resolver.url_patterns 

54 for pattern in patterns: 

55 if isinstance(pattern, URLResolver): 

56 resolvers.append(pattern) 

57 continue 

58 new_patterns.append(pattern) 

59 

60 possibilities = list(filter(lambda p: p.name == endpoint_name, new_patterns)) 

61 

62 for p in possibilities: 

63 if "format" not in p.pattern.regex.pattern: 

64 return str(p.pattern) 

65 

66 raise NoReverseMatch(f"There is no pattern with name {endpoint_name}") 

67 

68 

69@dataclass 

70class ChoicesOptionsLink: 

71 """ 

72 Helper dataclass, which holds data abot x-choices link. 

73 """ 

74 

75 field_name: str 

76 viewname: str 

77 value_field: str 

78 display_field: str 

79 serializer_class: Type[BaseSerializer] 

80 

81 description: str = "" 

82 

83 def _format_param_field(self, field: str) -> str: 

84 """ 

85 Helper method which formats field name to be a JSON pointer. 

86 If the passed field name is already a JSON pointer, it is returned unchagned. 

87 

88 Args: 

89 * field: field name 

90 

91 Returns: formatted field name 

92 """ 

93 if "$" in field: 

94 # than we presume that there has been given full pointer 

95 return field 

96 

97 field = f"$response.body#results/*/{field}" 

98 

99 return field 

100 

101 @property 

102 def formatted_value_field(self) -> str: 

103 return self._format_param_field(self.value_field) 

104 

105 @property 

106 def formatted_display_field(self) -> str: 

107 return self._format_param_field(self.display_field) 

108 

109 def get_url_pattern(self) -> str: 

110 """ 

111 Returns formatted url pattern of the linked view. 

112 """ 

113 pattern = ( 

114 get_endpoint_pattern(self.viewname) 

115 .replace("$", "") 

116 .replace("^", "/") 

117 .replace("/", "~1") 

118 ) 

119 

120 params = re.search(r"<.*>", pattern) 

121 if params: 

122 for param in params: 

123 param = param.string.replace("<", "{").replace(">", "}") 

124 

125 regexes = re.search(r"\(.*\)", pattern) 

126 for x, regex in enumerate(regexes): 

127 pattern = pattern.replace(regex.string, params[x]) 

128 

129 pattern = f"#/paths/{pattern}" 

130 

131 return pattern 

132 

133 

134class ChoicesOptionsLinkSchemaGenerator: 

135 def _process_link( 

136 self, link: Union[ChoicesOptionsLink, Dict[str, Any]] 

137 ) -> ChoicesOptionsLink: 

138 if isinstance(link, dict): 

139 link = ChoicesOptionsLink(**link) 

140 

141 if not isinstance(link, ChoicesOptionsLink): 

142 raise TypeError( 

143 f"This is not possible to create ChoicesOptionsLink \ 

144 from object of type {type(link)}" 

145 ) 

146 

147 serializer = force_instance(link.serializer_class) 

148 # serializer must own defined field 

149 if not serializer.fields.get(link.field_name, None): 

150 raise AttributeError( 

151 f"Serializer class: {link.serializer_class} does not have field: {link.field_name}" 

152 ) 

153 return link 

154 

155 def _create_link_title(self, link: Union[ChoicesOptionsLink, dict]) -> str: 

156 partials = link.viewname.replace("-", " ").replace("_", " ").split(" ") 

157 partials = [p.capitalize() for p in partials] 

158 return " ".join(partials).title() 

159 

160 def generate_schema(self, link: Union[ChoicesOptionsLink, Dict[str, Any]]) -> dict: 

161 """ 

162 Generates x-choices link schema. 

163 Args: 

164 * link: ChoicesOptionsLink instance or dict with link data 

165 

166 Returns: x-choices link schema 

167 """ 

168 

169 if not link: 

170 return 

171 

172 link = self._process_link(link) 

173 

174 schema = { 

175 "operationRef": link.get_url_pattern(), 

176 # "parameters": "", 

177 "value": link.formatted_value_field, 

178 "display": link.formatted_display_field, 

179 } 

180 return schema