Coverage for .tox/coverage/lib/python3.13/site-packages/wuttaweb/forms/widgets.py: 100%

254 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-24 16:36 -0500

1# -*- coding: utf-8; -*- 

2################################################################################ 

3# 

4# wuttaweb -- Web App for Wutta Framework 

5# Copyright © 2024-2026 Lance Edgar 

6# 

7# This file is part of Wutta Framework. 

8# 

9# Wutta Framework is free software: you can redistribute it and/or modify it 

10# under the terms of the GNU General Public License as published by the Free 

11# Software Foundation, either version 3 of the License, or (at your option) any 

12# later version. 

13# 

14# Wutta Framework is distributed in the hope that it will be useful, but 

15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 

17# more details. 

18# 

19# You should have received a copy of the GNU General Public License along with 

20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>. 

21# 

22################################################################################ 

23""" 

24Form widgets 

25 

26This module defines some custom widgets for use with WuttaWeb. 

27 

28However for convenience it also makes other Deform widgets available 

29in the namespace: 

30 

31* :class:`deform:deform.widget.Widget` (base class) 

32* :class:`deform:deform.widget.TextInputWidget` 

33* :class:`deform:deform.widget.TextAreaWidget` 

34* :class:`deform:deform.widget.PasswordWidget` 

35* :class:`deform:deform.widget.CheckedPasswordWidget` 

36* :class:`deform:deform.widget.CheckboxWidget` 

37* :class:`deform:deform.widget.SelectWidget` 

38* :class:`deform:deform.widget.CheckboxChoiceWidget` 

39* :class:`deform:deform.widget.DateInputWidget` 

40* :class:`deform:deform.widget.DateTimeInputWidget` 

41* :class:`deform:deform.widget.MoneyInputWidget` 

42""" 

43 

44import datetime 

45import decimal 

46import os 

47 

48import colander 

49import humanize 

50from deform.widget import ( # pylint: disable=unused-import 

51 Widget, 

52 TextInputWidget, 

53 TextAreaWidget, 

54 PasswordWidget, 

55 CheckedPasswordWidget, 

56 CheckboxWidget, 

57 SelectWidget, 

58 CheckboxChoiceWidget, 

59 DateInputWidget, 

60 DateTimeInputWidget, 

61 MoneyInputWidget, 

62) 

63from webhelpers2.html import HTML, tags 

64 

65from wuttjamaican.conf import parse_list 

66 

67from wuttaweb.diffs import WebDiff 

68 

69 

70class ObjectRefWidget(SelectWidget): 

71 """ 

72 Widget for use with model "object reference" fields, e.g. foreign 

73 key UUID => TargetModel instance. 

74 

75 While you may create instances of this widget directly, it 

76 normally happens automatically when schema nodes of the 

77 :class:`~wuttaweb.forms.schema.ObjectRef` (sub)type are part of 

78 the form schema; via 

79 :meth:`~wuttaweb.forms.schema.ObjectRef.widget_maker()`. 

80 

81 In readonly mode, this renders a ``<span>`` tag around the 

82 :attr:`model_instance` (converted to string). 

83 

84 Otherwise it renders a select (dropdown) element allowing user to 

85 choose from available records. 

86 

87 This is a subclass of :class:`deform:deform.widget.SelectWidget` 

88 and uses these Deform templates: 

89 

90 * ``select`` 

91 * ``readonly/objectref`` 

92 

93 .. attribute:: model_instance 

94 

95 Reference to the model record instance, i.e. the "far side" of 

96 the foreign key relationship. 

97 

98 .. note:: 

99 

100 You do not need to provide the ``model_instance`` when 

101 constructing the widget. Rather, it is set automatically 

102 when the :class:`~wuttaweb.forms.schema.ObjectRef` type 

103 instance (associated with the node) is serialized. 

104 """ 

105 

106 readonly_template = "readonly/objectref" 

107 

108 def __init__(self, request, *args, **kwargs): 

109 url = kwargs.pop("url", None) 

110 super().__init__(*args, **kwargs) 

111 self.request = request 

112 self.url = url 

113 

114 def get_template_values( # pylint: disable=empty-docstring 

115 self, field, cstruct, kw 

116 ): 

117 """ """ 

118 values = super().get_template_values(field, cstruct, kw) 

119 

120 # add url, only if rendering readonly 

121 readonly = kw.get("readonly", self.readonly) 

122 if readonly: 

123 if ( 

124 "url" not in values 

125 and self.url 

126 and getattr(field.schema, "model_instance", None) 

127 ): 

128 values["url"] = self.url(field.schema.model_instance) 

129 

130 return values 

131 

132 

133class NotesWidget(TextAreaWidget): 

134 """ 

135 Widget for use with "notes" fields. 

136 

137 In readonly mode, this shows the notes with a background to make 

138 them stand out a bit more. 

139 

140 Otherwise it effectively shows a ``<textarea>`` input element. 

141 

142 This is a subclass of :class:`deform:deform.widget.TextAreaWidget` 

143 and uses these Deform templates: 

144 

145 * ``textarea`` 

146 * ``readonly/notes`` 

147 """ 

148 

149 readonly_template = "readonly/notes" 

150 

151 

152class CopyableTextWidget(Widget): # pylint: disable=abstract-method 

153 """ 

154 A readonly text widget which adds a "copy" icon/link just after 

155 the text. 

156 """ 

157 

158 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

159 """ """ 

160 if not cstruct: 

161 return colander.null 

162 

163 return HTML.tag("wutta-copyable-text", **{"text": cstruct}) 

164 

165 def deserialize(self, field, pstruct): # pylint: disable=empty-docstring 

166 """ """ 

167 raise NotImplementedError 

168 

169 

170class ExternalLinkWidget(TextInputWidget): 

171 """ 

172 Widget for use with "external link" URL fields. In readonly mode, 

173 displays a hyperlink with ``_blank`` window target. 

174 

175 This is a subclass of 

176 :class:`deform:deform.widget.TextInputWidget` and uses these 

177 Deform templates: 

178 

179 * ``textinput`` 

180 * ``readonly/external_link`` 

181 """ 

182 

183 readonly_template = "readonly/external_link" 

184 

185 

186class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget): 

187 """ 

188 Custom widget for :class:`python:set` fields. 

189 

190 This is a subclass of 

191 :class:`deform:deform.widget.CheckboxChoiceWidget`. 

192 

193 :param request: Current :term:`request` object. 

194 

195 It uses these Deform templates: 

196 

197 * ``checkbox_choice`` 

198 * ``readonly/checkbox_choice`` 

199 """ 

200 

201 def __init__(self, request, *args, **kwargs): 

202 super().__init__(*args, **kwargs) 

203 self.request = request 

204 self.config = self.request.wutta_config 

205 self.app = self.config.get_app() 

206 

207 

208class WuttaCheckedPasswordWidget(PasswordWidget): 

209 """ 

210 Custom widget for password+confirmation field. 

211 

212 This widget is used only for Vue 3 + Oruga, but is *not* used for 

213 Vue 2 + Buefy. 

214 

215 This is a subclass of :class:`deform:deform.widget.PasswordWidget` 

216 and uses these Deform templates: 

217 

218 * ``wutta_checked_password`` 

219 """ 

220 

221 template = "wutta_checked_password" 

222 

223 

224class WuttaDateWidget(DateInputWidget): 

225 """ 

226 Custom widget for :class:`python:datetime.date` fields. 

227 

228 The main purpose of this widget is to leverage 

229 :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_date()` 

230 for the readonly display. 

231 

232 It is automatically used for SQLAlchemy mapped classes where the 

233 field maps to a :class:`sqlalchemy:sqlalchemy.types.Date` column. 

234 For other (non-mapped) date fields, or mapped datetime fields for 

235 which a date widget is preferred, use 

236 :meth:`~wuttaweb.forms.base.Form.set_widget()`. 

237 

238 This is a subclass of 

239 :class:`deform:deform.widget.DateInputWidget` and uses these 

240 Deform templates: 

241 

242 * ``dateinput`` 

243 """ 

244 

245 def __init__(self, request, *args, **kwargs): 

246 super().__init__(*args, **kwargs) 

247 self.request = request 

248 self.config = self.request.wutta_config 

249 self.app = self.config.get_app() 

250 

251 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

252 """ """ 

253 readonly = kw.get("readonly", self.readonly) 

254 if readonly and cstruct: 

255 try: 

256 dt = datetime.date.fromisoformat(cstruct) 

257 except ValueError: 

258 dt = datetime.datetime.fromisoformat(cstruct) 

259 return self.app.render_date(dt) 

260 

261 return super().serialize(field, cstruct, **kw) 

262 

263 

264class WuttaDateTimeWidget(DateTimeInputWidget): 

265 """ 

266 Custom widget for :class:`python:datetime.datetime` fields. 

267 

268 The main purpose of this widget is to leverage 

269 :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_datetime()` 

270 for the readonly display. 

271 

272 It is automatically used for SQLAlchemy mapped classes where the 

273 field maps to a :class:`sqlalchemy:sqlalchemy.types.DateTime` 

274 column. For other (non-mapped) datetime fields, you may have to 

275 use it explicitly via 

276 :meth:`~wuttaweb.forms.base.Form.set_widget()`. 

277 

278 This is a subclass of 

279 :class:`deform:deform.widget.DateTimeInputWidget` and uses these 

280 Deform templates: 

281 

282 * ``datetimeinput`` 

283 """ 

284 

285 def __init__(self, request, *args, **kwargs): 

286 super().__init__(*args, **kwargs) 

287 self.request = request 

288 self.config = self.request.wutta_config 

289 self.app = self.config.get_app() 

290 

291 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

292 """ """ 

293 readonly = kw.get("readonly", self.readonly) 

294 if readonly: 

295 if not cstruct: 

296 return "" 

297 dt = datetime.datetime.fromisoformat(cstruct) 

298 return self.app.render_datetime(dt, html=True) 

299 

300 return super().serialize(field, cstruct, **kw) 

301 

302 

303class WuttaMoneyInputWidget(MoneyInputWidget): 

304 """ 

305 Custom widget for "money" fields. This is used by default for 

306 :class:`~wuttaweb.forms.schema.WuttaMoney` type nodes. 

307 

308 The main purpose of this widget is to leverage 

309 :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_currency()` 

310 for the readonly display. 

311 

312 This is a subclass of 

313 :class:`deform:deform.widget.MoneyInputWidget` and uses these 

314 Deform templates: 

315 

316 * ``moneyinput`` 

317 

318 :param request: Current :term:`request` object. 

319 

320 :param scale: If this kwarg is specified, it will be passed along 

321 to ``render_currency()`` call. 

322 """ 

323 

324 def __init__(self, request, *args, **kwargs): 

325 self.scale = kwargs.pop("scale", 2) 

326 super().__init__(*args, **kwargs) 

327 self.request = request 

328 self.config = self.request.wutta_config 

329 self.app = self.config.get_app() 

330 

331 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

332 """ """ 

333 readonly = kw.get("readonly", self.readonly) 

334 if readonly: 

335 if cstruct in (colander.null, None): 

336 return HTML.tag("span") 

337 cstruct = decimal.Decimal(cstruct) 

338 text = self.app.render_currency(cstruct, scale=self.scale) 

339 return HTML.tag("span", c=[text]) 

340 

341 return super().serialize(field, cstruct, **kw) 

342 

343 

344class WuttaQuantityInputWidget(TextInputWidget): 

345 """ 

346 Custom widget for "quantity" fields. This is used by default for 

347 :class:`~wuttaweb.forms.schema.WuttaQuantity` type nodes. 

348 

349 The main purpose of this widget is to leverage 

350 :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_quantity()` 

351 for the readonly display. 

352 

353 This is a subclass of 

354 :class:`deform:deform.widget.TextInputWidget` and uses these 

355 Deform templates: 

356 

357 * ``textinput`` 

358 

359 :param request: Current :term:`request` object. 

360 """ 

361 

362 def __init__(self, request, *args, **kwargs): 

363 super().__init__(*args, **kwargs) 

364 self.request = request 

365 self.config = self.request.wutta_config 

366 self.app = self.config.get_app() 

367 

368 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

369 """ """ 

370 readonly = kw.get("readonly", self.readonly) 

371 if readonly: 

372 if cstruct in (colander.null, None): 

373 return HTML.tag("span") 

374 

375 cstruct = decimal.Decimal(cstruct) 

376 text = self.app.render_quantity(cstruct) 

377 return HTML.tag("span", c=[text]) 

378 

379 return super().serialize(field, cstruct, **kw) 

380 

381 

382class FileDownloadWidget(Widget): # pylint: disable=abstract-method 

383 """ 

384 Widget for use with :class:`~wuttaweb.forms.schema.FileDownload` 

385 fields. 

386 

387 This only supports readonly, and shows a hyperlink to download the 

388 file. Link text is the filename plus file size. 

389 

390 This is a subclass of :class:`deform:deform.widget.Widget` and 

391 uses these Deform templates: 

392 

393 * ``readonly/filedownload`` 

394 

395 :param request: Current :term:`request` object. 

396 

397 :param url: Optional URL for hyperlink. If not specified, file 

398 name/size is shown with no hyperlink. 

399 """ 

400 

401 readonly_template = "readonly/filedownload" 

402 

403 # pylint: disable=duplicate-code 

404 def __init__(self, request, *args, **kwargs): 

405 self.url = kwargs.pop("url", None) 

406 super().__init__(*args, **kwargs) 

407 self.request = request 

408 self.config = self.request.wutta_config 

409 self.app = self.config.get_app() 

410 

411 # pylint: enable=duplicate-code 

412 

413 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

414 """ """ 

415 # nb. readonly is the only way this rolls 

416 kw["readonly"] = True 

417 template = self.readonly_template 

418 

419 path = cstruct or None 

420 if path: 

421 kw.setdefault("filename", os.path.basename(path)) 

422 kw.setdefault("filesize", self.readable_size(path)) 

423 if self.url: 

424 kw.setdefault("url", self.url) 

425 

426 else: 

427 kw.setdefault("filename", None) 

428 kw.setdefault("filesize", None) 

429 

430 kw.setdefault("url", None) 

431 values = self.get_template_values(field, cstruct, kw) 

432 return field.renderer(template, **values) 

433 

434 def readable_size(self, path): # pylint: disable=empty-docstring 

435 """ """ 

436 try: 

437 size = os.path.getsize(path) 

438 except os.error: 

439 size = 0 

440 return humanize.naturalsize(size) 

441 

442 

443class GridWidget(Widget): # pylint: disable=abstract-method 

444 """ 

445 Widget for fields whose data is represented by a :term:`grid`. 

446 

447 This is a subclass of :class:`deform:deform.widget.Widget` but 

448 does not use any Deform templates. 

449 

450 This widget only supports "readonly" mode, is not editable. It is 

451 merely a convenience around the grid itself, which does the heavy 

452 lifting. 

453 

454 Instead of creating this widget directly you probably should call 

455 :meth:`~wuttaweb.forms.base.Form.set_grid()` on your form. 

456 

457 :param request: Current :term:`request` object. 

458 

459 :param grid: :class:`~wuttaweb.grids.base.Grid` instance, used to 

460 display the field data. 

461 """ 

462 

463 def __init__(self, request, grid, *args, **kwargs): 

464 super().__init__(*args, **kwargs) 

465 self.request = request 

466 self.grid = grid 

467 

468 def serialize(self, field, cstruct, **kw): 

469 """ 

470 This widget simply calls 

471 :meth:`~wuttaweb.grids.base.Grid.render_table_element()` on 

472 the ``grid`` to serialize. 

473 """ 

474 readonly = kw.get("readonly", self.readonly) 

475 if not readonly: 

476 raise NotImplementedError("edit not allowed for this widget") 

477 

478 return self.grid.render_table_element() 

479 

480 

481class DiffWidget(Widget): # pylint: disable=abstract-method 

482 """ 

483 Widget for fields whose data is represented by a diff table. 

484 

485 This is a subclass of :class:`deform:deform.widget.Widget` and 

486 uses this Deform template: 

487 

488 * ``readonly/diff`` 

489 

490 This widget only supports "readonly" mode, is not editable. It is 

491 mostly a convenience around the ``<wutta-diff>`` Vue.js component, 

492 and ultimately the :class:`~wuttaweb.diffs.WebDiff` class. 

493 

494 :param request: Current :term:`request` object. 

495 

496 :param diff: Optional :class:`~wuttaweb.diffs.WebDiff` (or 

497 subclass) instance. 

498 

499 :param \\*args: Will be passed along to the parent class 

500 constructor if you provide the ``diff`` object, otherwise to 

501 the ``WebDiff`` constructor. 

502 

503 :param \\**kwargs: Will be passed along to the parent class 

504 constructor if you provide the ``diff`` object, otherwise to 

505 the ``WebDiff`` constructor. 

506 """ 

507 

508 readonly_template = "readonly/diff" 

509 

510 def __init__(self, request, *args, **kwargs): 

511 self.diff = kwargs.pop("diff", None) 

512 if self.diff: 

513 super().__init__(*args, **kwargs) 

514 else: 

515 super().__init__() 

516 

517 self.request = request 

518 self.config = self.request.wutta_config 

519 

520 self.show_filter = kwargs.pop("show_filter", False) 

521 self.filtered_by_default = kwargs.pop("filtered_by_default", False) 

522 

523 if not self.diff: 

524 

525 if self.show_filter: 

526 

527 def extra_row_attrs( 

528 field, is_diff, attrs 

529 ): # pylint: disable=unused-argument 

530 if not is_diff: 

531 attrs["v-show"] = "!slotProps.filtered" 

532 return attrs 

533 

534 kwargs["extra_row_attrs"] = extra_row_attrs 

535 

536 self.diff = WebDiff(self.config, *args, **kwargs) 

537 

538 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

539 """ """ 

540 readonly = kw.get("readonly", self.readonly) 

541 if not readonly: 

542 raise NotImplementedError("edit not allowed for this widget") 

543 

544 kw.update( 

545 { 

546 "diff": self.diff.render_html(), 

547 "show_filter": self.show_filter, 

548 "filtered_by_default": self.filtered_by_default, 

549 } 

550 ) 

551 

552 template = self.readonly_template 

553 values = self.get_template_values(field, cstruct, kw) 

554 return field.renderer(template, **values) 

555 

556 

557class RoleRefsWidget(WuttaCheckboxChoiceWidget): 

558 """ 

559 Widget for use with User 

560 :attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field. 

561 This is the default widget for the 

562 :class:`~wuttaweb.forms.schema.RoleRefs` type. 

563 

564 This is a subclass of :class:`WuttaCheckboxChoiceWidget`. 

565 """ 

566 

567 readonly_template = "readonly/rolerefs" 

568 session = None 

569 

570 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

571 """ """ 

572 model = self.app.model 

573 

574 # special logic when field is editable 

575 readonly = kw.get("readonly", self.readonly) 

576 if not readonly: 

577 

578 # but does not apply if current user is root 

579 if not self.request.is_root: 

580 auth = self.app.get_auth_handler() 

581 admin = auth.get_role_administrator(self.session) 

582 

583 # prune admin role from values list; it should not be 

584 # one of the options since current user is not admin 

585 values = kw.get("values", self.values) 

586 values = [val for val in values if val[0] != admin.uuid] 

587 kw["values"] = values 

588 

589 else: # readonly 

590 

591 # roles 

592 roles = [] 

593 if cstruct: 

594 for uuid in cstruct: 

595 role = self.session.get(model.Role, uuid) 

596 if role: 

597 roles.append(role) 

598 kw["roles"] = sorted(roles, key=lambda r: r.name) 

599 

600 # url 

601 def url(role): 

602 return self.request.route_url("roles.view", uuid=role.uuid) 

603 

604 kw["url"] = url 

605 

606 # default logic from here 

607 return super().serialize(field, cstruct, **kw) 

608 

609 

610class PermissionsWidget(WuttaCheckboxChoiceWidget): 

611 """ 

612 Widget for use with Role 

613 :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.permissions` 

614 field. 

615 

616 This is a subclass of :class:`WuttaCheckboxChoiceWidget`. It uses 

617 these Deform templates: 

618 

619 * ``permissions`` 

620 * ``readonly/permissions`` 

621 """ 

622 

623 template = "permissions" 

624 readonly_template = "readonly/permissions" 

625 permissions = None 

626 

627 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

628 """ """ 

629 kw.setdefault("permissions", self.permissions) 

630 

631 if "values" not in kw: 

632 values = [] 

633 for group in self.permissions.values(): 

634 for pkey, perm in group["perms"].items(): 

635 values.append((pkey, perm["label"])) 

636 kw["values"] = values 

637 

638 return super().serialize(field, cstruct, **kw) 

639 

640 

641class EmailSettingRefWidget(TextInputWidget): 

642 """ 

643 Widget for an :term:`email setting` reference field. 

644 

645 This is mainly used for the readonly form when viewing a Problem 

646 Check definition. The email setting key is shown as a hyperlink 

647 to the corresponding Email Setting view. 

648 

649 This is a subclass of 

650 :class:`deform:deform.widget.TextInputWidget`. It uses these 

651 Deform templates (technically only the latter is intended for 

652 use): 

653 

654 * ``textinput`` 

655 * ``readonly/textinput`` 

656 """ 

657 

658 def __init__(self, request, *args, **kwargs): 

659 super().__init__(*args, **kwargs) 

660 self.request = request 

661 

662 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

663 """ """ 

664 if cstruct: 

665 cstruct = tags.link_to( 

666 cstruct, self.request.route_url("email_settings.view", key=cstruct) 

667 ) 

668 return super().serialize(field, cstruct, **kw) 

669 

670 

671class EmailRecipientsWidget(TextAreaWidget): 

672 """ 

673 Widget for :term:`email setting` recipient fields (``To``, ``Cc``, 

674 ``Bcc``). 

675 

676 This is a subclass of 

677 :class:`deform:deform.widget.TextAreaWidget`. It uses these 

678 Deform templates: 

679 

680 * ``textarea`` 

681 * ``readonly/email_recips`` 

682 

683 See also the :class:`~wuttaweb.forms.schema.EmailRecipients` 

684 schema type, which uses this widget. 

685 """ 

686 

687 readonly_template = "readonly/email_recips" 

688 

689 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

690 """ """ 

691 readonly = kw.get("readonly", self.readonly) 

692 if readonly: 

693 kw["recips"] = parse_list(cstruct or "") 

694 

695 return super().serialize(field, cstruct, **kw) 

696 

697 def deserialize(self, field, pstruct): # pylint: disable=empty-docstring 

698 """ """ 

699 if pstruct is colander.null: 

700 return colander.null 

701 

702 values = [value for value in parse_list(pstruct) if value] 

703 return ", ".join(values) 

704 

705 

706class BatchIdWidget(Widget): # pylint: disable=abstract-method 

707 """ 

708 Widget for use with the 

709 :attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.id` 

710 field of a :term:`batch` model. 

711 

712 This widget is "always" read-only and renders the Batch ID as 

713 zero-padded 8-char string 

714 """ 

715 

716 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

717 """ """ 

718 if cstruct is colander.null: 

719 return colander.null 

720 

721 batch_id = int(cstruct) 

722 return f"{batch_id:08d}" 

723 

724 

725class AlembicRevisionWidget(Widget): # pylint: disable=missing-class-docstring 

726 """ 

727 Widget to show an Alembic revision identifier, with link to view 

728 the revision. 

729 """ 

730 

731 def __init__(self, request, *args, **kwargs): 

732 super().__init__(*args, **kwargs) 

733 self.request = request 

734 

735 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

736 """ """ 

737 if not cstruct: 

738 return colander.null 

739 

740 return tags.link_to( 

741 cstruct, self.request.route_url("alembic.migrations.view", revision=cstruct) 

742 ) 

743 

744 def deserialize(self, field, pstruct): # pylint: disable=empty-docstring 

745 """ """ 

746 raise NotImplementedError 

747 

748 

749class AlembicRevisionsWidget(Widget): 

750 """ 

751 Widget to show list of Alembic revision identifiers, with links to 

752 view each revision. 

753 """ 

754 

755 def __init__(self, request, *args, **kwargs): 

756 super().__init__(*args, **kwargs) 

757 self.request = request 

758 self.config = self.request.wutta_config 

759 

760 def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring 

761 """ """ 

762 if not cstruct: 

763 return colander.null 

764 

765 revisions = [] 

766 for rev in self.config.parse_list(cstruct): 

767 revisions.append( 

768 tags.link_to( 

769 rev, self.request.route_url("alembic.migrations.view", revision=rev) 

770 ) 

771 ) 

772 

773 return ", ".join(revisions) 

774 

775 def deserialize(self, field, pstruct): # pylint: disable=empty-docstring 

776 """ """ 

777 raise NotImplementedError