Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# Imports 

2 

3from django.core.exceptions import FieldDoesNotExist 

4from django.db import models 

5from django.urls import path, re_path, reverse, reverse_lazy, NoReverseMatch 

6 

7# Exports 

8 

9__all__ = ( 

10 "Pattern", 

11 "ModelPattern", 

12) 

13 

14# Classes 

15 

16 

17class Pattern(object): 

18 """A generic pattern for representing a view.""" 

19 

20 def __init__(self, name, namespace=None): 

21 self.name = name 

22 self.namespace = namespace 

23 

24 def __repr__(self): 

25 return "<%s %s:%s>" % (self.__class__.__name__, self.namespace, self.name) 

26 

27 def get_name(self, namespace=None): 

28 """Get the name of the view with the ``namespace`` if one has been provided. 

29 

30 :param namespace: The namespace in which the view operates. 

31 :type namespace: str 

32 

33 :rtype: str 

34 

35 """ 

36 _namespace = None 

37 if namespace or self.namespace: 

38 _namespace = namespace or self.namespace 

39 

40 if _namespace is not None: 

41 return "%s_%s" % (_namespace, self.name) 

42 

43 return self.name 

44 

45 def reverse(self, *args, **kwargs): 

46 return reverse(self.get_name(), args=args, kwargs=kwargs) 

47 

48 def reverse_lazy(self, *args, **kwargs): 

49 return reverse_lazy(self.get_name(), args=args, kwargs=kwargs) 

50 

51 

52class ModelPattern(object): 

53 """A helper class for encapsulating data for model views to derive the necessary components for automatic URL 

54 definition. 

55 

56 Primarily used by the UI package to automatically generate URLs for the user interface. However, programmatic use is 

57 also possible: 

58 

59 .. code-block:: python 

60 

61 update = ModelPattern( 

62 Project, 

63 ProjectUpdate, 

64 lookup_field="uuid" 

65 ) 

66 

67 """ 

68 

69 def __init__(self, model, view, list_path="", lookup_field=None, lookup_key=None, name=None, namespace=None, 

70 prefix=None, regex=None, verb=None): 

71 """Initialize a view pattern. 

72 

73 :param model: The model class. This is used only during instantiation. 

74 :type model: class 

75 

76 :param view: The view class. 

77 :type view: class 

78 

79 :param list_path: The part of the path that represents the model's list view. This defaults to an empty string, 

80 but may be given if something else will live at the "index" of the model's views. For example, 

81 ``list_path="index"`` or ``list_path="list"``. 

82 :type list_path: str 

83 

84 :param lookup_field: The name of the field used to uniquely identify a record. This is required for any view 

85 that resolves to a specific record. Default: ``uuid``. 

86 :type lookup_field: str 

87 

88 :param name: If given, this is used as the pattern name of the view instead of composing the name using pattern 

89 attributes. 

90 :type name: str 

91 

92 :param namespace: The namespace in which the view operates. 

93 :type namespace: str 

94 

95 :param prefix: The prefix of the URL. For example, the model's ``app_label``. 

96 :type prefix: str 

97 

98 :param regex: A regular expression used to identify the view in URLs. This is optional. 

99 :type regex: str 

100 

101 :param verb: The name (verb) of the view. For example: "create", "list", "detail", etc. 

102 :type verb: str 

103 

104 .. important:: 

105 ``lookup_field`` should only be provided for views that actually require identifying a specific record. 

106 

107 """ 

108 self.list_path = list_path 

109 self.lookup_field = lookup_field 

110 self.lookup_key = lookup_key or lookup_field 

111 self.namespace = namespace 

112 self.prefix = prefix 

113 self.view = view 

114 self.verb = verb 

115 # noinspection PyProtectedMember 

116 self._app_label = model._meta.app_label 

117 self._name = name 

118 self._regex = regex 

119 # noinspection PyProtectedMember 

120 self._model_name = model._meta.model_name 

121 

122 def __repr__(self): 

123 return "<%s %s>" % (self.__class__.__name__, self.get_name()) 

124 

125 def get_name(self, namespace=None): 

126 """Get the name of the view. 

127 

128 :param namespace: The namespace in which the view operates. 

129 :type namespace: str 

130 

131 :rtype: str 

132 

133 """ 

134 # if self._name is not None: 

135 # name = self._name 

136 # else: 

137 # name = self.get_verb() 

138 

139 # The only certain way of providing unique pattern name is to include the app label and model name before the 

140 # name of the action. 

141 if self._name: 

142 pattern_name = "%s_%s" % (self._app_label, self._name) 

143 else: 

144 pattern_name = "%s_%s_%s" % (self._app_label, self._model_name, self.get_verb()) 

145 

146 # Handle namespace. 

147 _namespace = None 

148 if namespace or self.namespace: 

149 _namespace = namespace or self.namespace 

150 

151 if _namespace is not None: 

152 pattern_name = "%s_%s" % (_namespace, pattern_name) 

153 

154 return pattern_name 

155 

156 def get_path(self): 

157 """Get the URL path for the view. 

158 

159 :rtype: str 

160 

161 """ 

162 name = self.get_name() 

163 

164 if "_create" in name: 

165 value = "create/" 

166 elif "_list" in name: 

167 value = self.list_path 

168 elif self.lookup_field is not None: 

169 if self.lookup_field == "pk": 

170 converter = "<int:%s>" % self.lookup_key 

171 else: 

172 converter = "<%s:%s>" % (self.lookup_field, self.lookup_key) 

173 

174 value = "%s/%s/" % (self.get_verb().replace("_", "/"), converter) 

175 else: 

176 value = self.get_verb().replace("_", "/") + "/" 

177 

178 value = "%s/%s" % (self._model_name, value) 

179 

180 if self.prefix is not None: 

181 value = "%s/%s" % (self.prefix, value) 

182 

183 return value 

184 

185 def get_regex(self): 

186 """Get the regular expression for the view. 

187 

188 :rtype: str | None 

189 

190 """ 

191 if self._regex is None: 

192 return None 

193 

194 regex = self._regex 

195 if self.prefix: 

196 regex = "%s/%s" % (self.prefix, regex) 

197 

198 return regex 

199 

200 def get_url(self): 

201 """Get the URL object for the view. 

202 

203 :rtype: RegexPattern | RoutePattern 

204 

205 """ 

206 if self._regex is not None: 

207 return re_path(self.get_regex(), self.view, name=self.get_name()) 

208 

209 return path(self.get_path(), self.view, name=self.get_name()) 

210 

211 def get_verb(self): 

212 """Get the action (verb) that the view represents. 

213 

214 :rtype: str 

215 

216 """ 

217 if self.verb is not None: 

218 return self.verb 

219 

220 return self.view.__name__.replace("_view", "") 

221 

222 @property 

223 def name(self): 

224 """Alias for ``get_name()``.""" 

225 return self.get_name() 

226 

227 @property 

228 def path(self): 

229 """Alias for ``get_path()``.""" 

230 return self.get_path() 

231 

232 @property 

233 def regex(self): 

234 """Alias for ``get_regex()``.""" 

235 return self.get_regex() 

236 

237 def reverse(self, namespace=None, record=None): 

238 """Use Django's ``reverse()`` to obtain the URL. 

239 

240 :param namespace: The namespace in which the view operates. 

241 :type namespace: str 

242 

243 :param record: The model instance. 

244 

245 :rtype: str | None 

246 

247 """ 

248 args = None 

249 if record is not None: 

250 args = [self._get_identifier(record)] 

251 

252 try: 

253 return reverse(self.get_name(namespace=namespace), args=args) 

254 except NoReverseMatch: 

255 return None 

256 

257 # TODO: Determine whether reverse_lazy() is actually useful. 

258 def reverse_lazy(self, namespace=None, record=None): 

259 """Use Django's ``reverse_lazy()`` to obtain the URL. 

260 

261 :param namespace: The namespace in which the view operates. 

262 :type namespace: str 

263 

264 :param record: The model instance. 

265 

266 :rtype: str 

267 

268 """ 

269 args = None 

270 if record is not None: 

271 args = [self._get_identifier(record)] 

272 

273 return reverse_lazy(self.get_name(namespace=namespace), args=args) 

274 

275 # noinspection PyMethodMayBeStatic 

276 def _field_exists(self, field_name, record, unique=False): 

277 """Determine if the given field name is defined on the model. 

278 

279 :param record: The model instance may be used to determine which field to use. 

280 

281 :param field_name: The name of the field to check. 

282 :type field_name: str 

283 

284 :param unique: Also test whether the field is unique. 

285 :type unique: bool 

286 

287 :rtype: bool 

288 

289 """ 

290 try: 

291 # noinspection PyProtectedMember 

292 field = record._meta.get_field(field_name) 

293 if unique: 

294 

295 if isinstance(field, models.UUIDField): 

296 return True 

297 

298 return field.unique 

299 

300 return True 

301 except FieldDoesNotExist: 

302 return False 

303 

304 def _get_identifier(self, record): 

305 """Get the value of the given record's lookup field. 

306 

307 :param record: The model instance. 

308 

309 :rtype: int | str 

310 

311 """ 

312 lookup_field = self._get_lookup_field(record) 

313 return getattr(record, lookup_field) 

314 

315 def _get_lookup_field(self, record): 

316 """Get (guess) the name of the field that uniquely identifies the record. 

317 

318 :param record: The model instance may be used to determine which field to use. 

319 

320 :rtype: str 

321 

322 """ 

323 if self.lookup_field is not None: 

324 return self.lookup_field 

325 

326 if self._field_exists("unique_id", record, unique=True): 

327 return "unique_id" 

328 

329 if self._field_exists("uuid", record, unique=True): 

330 return "uuid" 

331 

332 if self._field_exists("slug", record, unique=True): 

333 return "slug" 

334 

335 return "pk"