Coverage for /Users/davegaeddert/Development/dropseed/plain/plain-staff/plain/staff/views/base.py: 55%

211 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-16 22:04 -0500

1from typing import TYPE_CHECKING 

2 

3from plain import models 

4from plain.auth.views import AuthViewMixin 

5from plain.htmx.views import HTMXViewMixin 

6from plain.http import Response, ResponseRedirect 

7from plain.paginator import Paginator 

8from plain.staff.dates import DatetimeRange, DatetimeRangeAliases 

9from plain.urls import reverse 

10from plain.utils import timezone 

11from plain.utils.text import slugify 

12from plain.views import ( 

13 CreateView, 

14 DeleteView, 

15 DetailView, 

16 TemplateView, 

17 UpdateView, 

18) 

19 

20from .registry import registry 

21 

22if TYPE_CHECKING: 

23 from ..cards import Card 

24 

25 

26URL_NAMESPACE = "staff" 

27 

28 

29class StaffView(AuthViewMixin, TemplateView): 

30 staff_required = True 

31 

32 title: str 

33 slug: str = "" 

34 path: str = "" 

35 description: str = "" 

36 

37 # Leave empty to hide from nav 

38 # 

39 # An explicit disabling of showing this url/page in the nav 

40 # which importantly effects the (future) recent pages list 

41 # so you can also use this for pages that can never be bookmarked 

42 nav_section = "App" 

43 

44 links: dict[str] = {} 

45 

46 parent_view_class: "StaffView" = None 

47 

48 template_name = "staff/page.html" 

49 cards: list["Card"] = [] 

50 

51 default_datetime_range = DatetimeRangeAliases.LAST_365_DAYS 

52 

53 def get_template_context(self): 

54 context = super().get_template_context() 

55 context["title"] = self.get_title() 

56 context["slug"] = self.get_slug() 

57 context["description"] = self.get_description() 

58 context["links"] = self.get_links() 

59 context["parent_view_classes"] = self.get_parent_view_classes() 

60 context["admin_registry"] = registry 

61 context["cards"] = self.get_cards() 

62 context["render_card"] = self.render_card 

63 context["from_datetime"] = self.datetime_range.start 

64 context["to_datetime"] = self.datetime_range.end 

65 context["time_zone"] = timezone.get_current_timezone_name() 

66 return context 

67 

68 def get_response(self): 

69 default_range = DatetimeRangeAliases.to_range(self.default_datetime_range) 

70 from_datetime = self.request.GET.get("from", default_range.start) 

71 to_datetime = self.request.GET.get("to", default_range.end) 

72 self.datetime_range = DatetimeRange(from_datetime, to_datetime) 

73 return super().get_response() 

74 

75 @classmethod 

76 def view_name(cls) -> str: 

77 return f"view_{cls.get_slug()}" 

78 

79 @classmethod 

80 def get_title(cls) -> str: 

81 return cls.title 

82 

83 @classmethod 

84 def get_slug(cls) -> str: 

85 return cls.slug or slugify(cls.get_title()) 

86 

87 @classmethod 

88 def get_description(cls) -> str: 

89 return cls.description 

90 

91 @classmethod 

92 def get_path(cls) -> str: 

93 return cls.path or cls.get_slug() 

94 

95 @classmethod 

96 def get_parent_view_classes(cls) -> list["StaffView"]: 

97 parents = [] 

98 parent = cls.parent_view_class 

99 while parent: 

100 parents.append(parent) 

101 parent = parent.parent_view_class 

102 return parents 

103 

104 @classmethod 

105 def get_nav_section(cls) -> bool: 

106 if not cls.nav_section: 

107 return "" 

108 

109 if cls.parent_view_class: 

110 # Don't show child views by default 

111 return "" 

112 

113 return cls.nav_section 

114 

115 @classmethod 

116 def get_absolute_url(cls) -> str: 

117 return reverse(f"{URL_NAMESPACE}:" + cls.view_name()) 

118 

119 def get_links(self) -> dict[str]: 

120 return self.links.copy() 

121 

122 def get_cards(self): 

123 return self.cards.copy() 

124 

125 def render_card(self, card: "Card"): 

126 """Render card as a subview""" 

127 # response = card.as_view()(self.request) 

128 # response.render() 

129 # content = response.content.decode() 

130 return card().render(self, self.request, self.datetime_range) 

131 

132 

133class StaffListView(HTMXViewMixin, StaffView): 

134 template_name = "staff/list.html" 

135 fields: list[str] 

136 actions: list[str] = [] 

137 filters: list[str] = [] 

138 page_size = 100 

139 show_search = False 

140 allow_global_search = False 

141 

142 def get_template_context(self): 

143 context = super().get_template_context() 

144 

145 # Make this available on self for usage in get_objects and other methods 

146 self.filter = self.request.GET.get("filter", "") 

147 

148 # Make this available to get_filters and stuff 

149 self.objects = self.get_objects() 

150 

151 page_size = self.request.GET.get("page_size", self.page_size) 

152 paginator = Paginator(self.objects, page_size) 

153 self._page = paginator.get_page(self.request.GET.get("page", 1)) 

154 

155 context["paginator"] = paginator 

156 context["page"] = self._page 

157 context["objects"] = self._page # alias 

158 context["fields"] = self.get_fields() 

159 context["actions"] = self.get_actions() 

160 context["filters"] = self.get_filters() 

161 

162 context["active_filter"] = self.filter 

163 

164 # Implement search yourself in get_objects 

165 context["search_query"] = self.request.GET.get("search", "") 

166 context["show_search"] = self.show_search 

167 

168 context["table_style"] = getattr(self, "_table_style", "default") 

169 

170 context["get_object_pk"] = self.get_object_pk 

171 context["get_field_value"] = self.get_field_value 

172 context["get_field_value_template"] = self.get_field_value_template 

173 

174 context["get_create_url"] = self.get_create_url 

175 context["get_object_links"] = self.get_object_links 

176 

177 return context 

178 

179 def get(self) -> Response: 

180 if self.is_htmx_request: 

181 hx_from_this_page = self.request.path in self.request.headers.get( 

182 "HX-Current-Url", "" 

183 ) 

184 if not hx_from_this_page: 

185 self._table_style = "simple" 

186 else: 

187 hx_from_this_page = False 

188 

189 response = super().get() 

190 

191 if self.is_htmx_request and not hx_from_this_page and not self._page: 

192 # Don't render anything 

193 return Response(status=204) 

194 

195 return response 

196 

197 def post(self) -> Response: 

198 # won't be "key" anymore, just list 

199 action_name = self.request.POST.get("action_name") 

200 actions = self.get_actions() 

201 if action_name and action_name in actions: 

202 target_pks = self.request.POST["action_pks"].split(",") 

203 response = self.perform_action(action_name, target_pks) 

204 if response: 

205 return response 

206 else: 

207 # message in session first 

208 return ResponseRedirect(".") 

209 

210 raise ValueError("Invalid action") 

211 

212 def perform_action(self, action: str, target_pks: list) -> Response | None: 

213 raise NotImplementedError 

214 

215 def get_objects(self) -> list: 

216 return [] 

217 

218 def get_fields(self) -> list: 

219 return ( 

220 self.fields.copy() 

221 ) # Avoid mutating the class attribute if using append etc 

222 

223 def get_actions(self) -> dict[str]: 

224 return self.actions.copy() # Avoid mutating the class attribute itself 

225 

226 def get_filters(self) -> list[str]: 

227 return self.filters.copy() # Avoid mutating the class attribute itself 

228 

229 def get_field_value(self, obj, field: str): 

230 # Try basic dict lookup first 

231 if field in obj: 

232 return obj[field] 

233 

234 # Try dot notation 

235 if "." in field: 

236 field, subfield = field.split(".", 1) 

237 return self.get_field_value(obj[field], subfield) 

238 

239 # Try regular object attribute 

240 return getattr(obj, field) 

241 

242 def get_object_pk(self, obj): 

243 try: 

244 return self.get_field_value(obj, "pk") 

245 except AttributeError: 

246 return self.get_field_value(obj, "id") 

247 

248 def get_field_value_template(self, obj, field: str, value): 

249 type_str = type(value).__name__.lower() 

250 return [ 

251 f"staff/values/{type_str}.html", # Create a template per-type 

252 f"staff/values/{field}.html", # Or for specific field names 

253 "staff/values/default.html", 

254 ] 

255 

256 def get_create_url(self) -> str | None: 

257 return None 

258 

259 def get_detail_url(self, obj) -> str | None: 

260 return None 

261 

262 def get_update_url(self, obj) -> str | None: 

263 return None 

264 

265 def get_object_links(self, obj) -> dict[str]: 

266 links = {} 

267 if self.get_detail_url(obj): 

268 links["Detail"] = self.get_detail_url(obj) 

269 if self.get_update_url(obj): 

270 links["Update"] = self.get_update_url(obj) 

271 return links 

272 

273 

274class StaffDetailView(StaffView, DetailView): 

275 template_name = None 

276 nav_section = "" 

277 

278 def get_template_context(self): 

279 context = super().get_template_context() 

280 context["get_field_value"] = self.get_field_value 

281 return context 

282 

283 def get_template_names(self) -> list[str]: 

284 # TODO move these to model views 

285 if not self.template_name and isinstance(self.object, models.Model): 

286 object_meta = self.object._meta 

287 return [ 

288 f"staff/{object_meta.package_label}/{object_meta.model_name}{self.template_name_suffix}.html" 

289 ] 

290 

291 return super().get_template_names() 

292 

293 def get_field_value(self, obj, field: str): 

294 return getattr(obj, field) 

295 

296 def get_update_url(self, obj) -> str | None: 

297 return None 

298 

299 

300class StaffUpdateView(StaffView, UpdateView): 

301 template_name = None 

302 nav_section = "" 

303 

304 def get_template_names(self) -> list[str]: 

305 if not self.template_name and isinstance(self.object, models.Model): 

306 object_meta = self.object._meta 

307 return [ 

308 f"staff/{object_meta.package_label}/{object_meta.model_name}{self.template_name_suffix}.html" 

309 ] 

310 

311 return super().get_template_names() 

312 

313 def get_detail_url(self, obj) -> str | None: 

314 return None 

315 

316 

317class StaffCreateView(StaffView, CreateView): 

318 template_name = None 

319 

320 def get_template_names(self) -> list[str]: 

321 if not self.template_name and isinstance(self.object, models.Model): 

322 object_meta = self.object._meta 

323 return [ 

324 f"staff/{object_meta.package_label}/{object_meta.model_name}{self.template_name_suffix}.html" 

325 ] 

326 

327 return super().get_template_names() 

328 

329 

330class StaffDeleteView(StaffView, DeleteView): 

331 template_name = "staff/confirm_delete.html" 

332 nav_section = ""