Coverage for portality/lib/formulaic.py: 49%

615 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-20 16:12 +0100

1# ~~Formulaic:Library~~ 

2 

3""" 

4EXAMPLE = { 

5 "contexts" : { 

6 "[context name]" : { 

7 "fieldsets" : [ 

8 "[fieldset name]" 

9 ], 

10 "asynchronous_warnings" : [ 

11 "[warning function reference]" 

12 ] 

13 } 

14 }, 

15 "fieldsets" : { 

16 "[fieldset name]" : { 

17 "label" : "[label]", 

18 "fields" : [ 

19 "[field name]" 

20 ] 

21 } 

22 }, 

23 "fields" : { 

24 "[field name]" : { 

25 "label" : "[label]", 

26 "input" : "[input type: radio|text|taglist|select|checkbox]", 

27 "multiple" : "[select multiple True|False]", 

28 "datatype" : "[coerce datatype]", 

29 "options" : [ # for radio, select and checkbox 

30 { 

31 "display" : "[display value]", 

32 "value" : "[stored value]", 

33 "exclusive" : "[exclusive: True|False]", # if you select this option you can't select others 

34 "subfields" : ["[field name]"] 

35 } 

36 ], 

37 "options_fn" : "function name to generate options", 

38 "default" : "[default value]", 

39 "disabled" : "[disabled: True|False OR a function reference string]", 

40 "conditional" : [ # conditions to AND together 

41 { 

42 "field" : "[field name]", 

43 "value" : "[field value]" 

44 } 

45 ], 

46 "help" : { 

47 "placeholder" : "[input field placeholder text]", 

48 "description" : "[description]", 

49 "tooltip" : "[tooltip/long description]", 

50 "doaj_criteria" : "[doaj compliance criteria]", 

51 "seal_criteria" : "[doaj compliance criteria]" 

52 }, 

53 "validate" : { 

54 "[validate function]", 

55 {"[validate function]" : {"arg" : "argval"}} 

56 }, 

57 "widgets" : { 

58 "[widget function]", 

59 {"[widget function]" : {"arg" : "argval"}} 

60 }, 

61 "postprocessing" : { 

62 "[processing function]", 

63 {"[processing function]" : {"arg" : "argval"}} 

64 }, 

65 "attr" : { 

66 "[html attribute name]" : "[html attribute value]" 

67 }, 

68 "contexts" : { 

69 "[context name]" : { 

70 "[field property]" : "[field property value]" 

71 } 

72 } 

73 } 

74 } 

75} 

76 

77CONTEXT_EXAMPLE = { 

78 "fieldsets" : [ 

79 { 

80 "name" : "[fieldset name]", 

81 "label" : "[label]", 

82 "fields" : [ 

83 { 

84 "name" : "[field name]", 

85 "[field property]" : "[field property value]" 

86 } 

87 ] 

88 } 

89 ], 

90 "asynchronous_warnings" : [ 

91 "[warning function reference]" 

92 ], 

93 "template" : "path/to/form/page/template.html", 

94 "crosswalks" : { 

95 "obj2form" : "crosswalk.obj2form", 

96 "form2obj" : "crosswalk.form2obj" 

97 }, 

98 "processor" : "module.path.to.processor" 

99} 

100""" 

101import csv 

102from copy import deepcopy 

103from wtforms import Form 

104from wtforms.fields.core import UnboundField, FieldList, FormField 

105 

106from portality.lib import plugin 

107from flask import render_template 

108import json 

109 

110 

111UI_CONFIG_FIELDS = [ 

112 "label", 

113 "input", 

114 "options", 

115 "help", 

116 "validate", 

117 "visible", 

118 "conditional", 

119 "widgets", 

120 "attr", 

121 "multiple", 

122 "repeatable", 

123 "datatype", 

124 "disabled", 

125 "name", 

126 "subfields", 

127 "subfield", 

128 "group" 

129] 

130 

131 

132class FormulaicException(Exception): 

133 def __init__(self, *args): 

134 try: 

135 self.message = args[0] 

136 except IndexError: 

137 self.message = '' 

138 super(FormulaicException, self).__init__(*args) 

139 

140 

141class Formulaic(object): 

142 def __init__(self, definition, wtforms_builders, function_map=None, javascript_functions=None): 

143 self._definition = definition 

144 self._wtforms_builders = wtforms_builders 

145 self._function_map = function_map 

146 self._javascript_functions = javascript_functions 

147 

148 def context(self, context_name): 

149 context_def = deepcopy(self._definition.get("contexts", {}).get(context_name)) 

150 if context_def is None: 

151 return None 

152 

153 fieldsets = context_def.get("fieldsets", []) 

154 

155 expanded_fieldsets = [] 

156 for fsn in fieldsets: 

157 fieldset_def = deepcopy(self._definition.get("fieldsets", {}).get(fsn)) 

158 if fieldset_def is None: 

159 raise FormulaicException("Unable to locate fieldset with name {x} in context {y}".format(x=fsn, y=context_name)) 

160 fieldset_def["name"] = fsn 

161 

162 expanded_fields = self._process_fields(context_name, fieldset_def.get("fields", [])) 

163 

164 fieldset_def["fields"] = expanded_fields 

165 expanded_fieldsets.append(fieldset_def) 

166 

167 context_def["fieldsets"] = expanded_fieldsets 

168 return FormulaicContext(context_name, context_def, self) 

169 

170 @property 

171 def wtforms_builders(self): 

172 return self._wtforms_builders 

173 

174 @property 

175 def function_map(self): 

176 return self._function_map 

177 

178 @property 

179 def javascript_functions(self): 

180 return self._javascript_functions 

181 

182 def choices_for(self, field_name, formulaic_context=None): 

183 field_def = self._definition.get("fields", {}).get(field_name) 

184 if field_def is None: 

185 return [] 

186 

187 return FormulaicField._options2choices(field_def, formulaic_context) 

188 

189 def _process_fields(self, context_name, field_names): 

190 field_defs = [] 

191 for fn in field_names: 

192 field_def = deepcopy(self._definition.get("fields", {}).get(fn)) 

193 field_def["name"] = fn 

194 if field_def is None: 

195 raise FormulaicException("Field '{x}' is referenced but not defined".format(x=fn)) 

196 

197 # filter for context 

198 context_overrides = field_def.get("contexts", {}).get(context_name) 

199 if context_overrides is not None: 

200 for k, v in context_overrides.items(): 

201 field_def[k] = v 

202 

203 # and remove the context overrides settings, so they don't bleed to contexts that don't require them 

204 if "contexts" in field_def: 

205 del field_def["contexts"] 

206 

207 field_defs.append(field_def) 

208 

209 return field_defs 

210 

211 @classmethod 

212 def run_options_fn(cls, field_def, formulaic_context): 

213 # opt_fn = options_function_map.get(field_def["options_fn"]) 

214 opt_fn = formulaic_context.function_map.get("options", {}).get(field_def.get("options_fn")) 

215 if opt_fn is None: 

216 raise FormulaicException( 

217 "No function mapping defined for function reference '{x}'".format(x=field_def["options_fn"])) 

218 if isinstance(opt_fn, str): 

219 opt_fn = plugin.load_function(opt_fn) 

220 return opt_fn(field_def, formulaic_context) 

221 

222 

223# ~~->$ FormulaicContext:Feature~~ 

224class FormulaicContext(object): 

225 def __init__(self, name, definition, parent: Formulaic): 

226 self._name = name 

227 self._definition = definition 

228 self._formulaic = parent 

229 self._wtform_class = None 

230 self._wtform_inst = None 

231 

232 self._wtform_class = self.wtform_class() 

233 self._wtform_inst = self.wtform() 

234 

235 @property 

236 def name(self): 

237 return self._name 

238 

239 @property 

240 def wtforms_builders(self): 

241 return self._formulaic.wtforms_builders 

242 

243 @property 

244 def function_map(self): 

245 return self._formulaic.function_map 

246 

247 @property 

248 def wtform_inst(self): 

249 return self._wtform_inst 

250 

251 @property 

252 def javascript_functions(self): 

253 return self._formulaic.javascript_functions 

254 

255 @property 

256 def ui_settings(self): 

257 ui = deepcopy(self._definition.get("fieldsets", [])) 

258 for fieldset in ui: 

259 for field in fieldset.get("fields", []): 

260 for fn in [k for k in field.keys()]: 

261 if fn not in UI_CONFIG_FIELDS: 

262 del field[fn] 

263 return ui 

264 

265 @property 

266 def default_field_template(self): 

267 return self._definition.get("templates", {}).get("default_field") 

268 

269 @property 

270 def default_group_template(self): 

271 return self._definition.get("templates", {}).get("default_group") 

272 

273 def list_fields_in_order(self): 

274 fieldlist = [] 

275 for fs in self.fieldsets(): 

276 for field in fs.fields(): 

277 fieldlist.append(field) 

278 if field.group_subfields(): 

279 for sf in field.group_subfields(): 

280 fieldlist.append(sf) 

281 return fieldlist 

282 

283 def make_wtform_class(self, fields): 

284 # ~~^-> WTForms:Library~~ 

285 class TempForm(Form): 

286 pass 

287 

288 for field in fields: 

289 self.bind_wtforms_field(TempForm, field) 

290 

291 return TempForm 

292 

293 def wtform_class(self): 

294 if self._definition is None: 

295 return 

296 

297 if self._wtform_class is not None: 

298 return self._wtform_class 

299 

300 # FIXME: we should just store a list of fields in the context, and reference them 

301 # from the fieldset, which would get rid of a lot of this double-layered looping 

302 fields = [] 

303 for fieldset in self._definition.get("fieldsets", []): 

304 for field in fieldset.get("fields", []): 

305 if "group" in field: 

306 continue 

307 #if "subfield" in field: 

308 # continue 

309 fields.append(field) 

310 

311 klazz = self.make_wtform_class(fields) 

312 self._wtform_class = klazz 

313 return self._wtform_class 

314 

315 def wtform(self, formdata=None, data=None): 

316 if self._definition is None: 

317 return 

318 

319 klazz = self.wtform_class() 

320 self._wtform_inst = klazz(formdata=formdata, data=data) 

321 

322 if formdata is not None or data is not None: 

323 for fs in self._definition.get("fieldsets", []): 

324 for f in fs.get("fields", []): 

325 if "options_fn" in f: 

326 opts = FormulaicField._options2choices(f, self) 

327 wtf = None 

328 if f.get("group") is not None: 

329 wtf = self._wtform_inst[f["group"]] 

330 if isinstance(wtf, FieldList): 

331 for entry in wtf: 

332 entry[f["name"]].choices = opts 

333 else: 

334 wtf[f["name"]].choices = opts 

335 else: 

336 wtf = self._wtform_inst[f["name"]] 

337 if isinstance(wtf, FieldList): 

338 for entry in wtf: 

339 entry.choices = opts 

340 else: 

341 wtf.choices = opts 

342 

343 return self._wtform_inst 

344 

345 def get(self, field_name, parent=None): 

346 if parent is None: 

347 parent = self 

348 for fs in self._definition.get("fieldsets", []): 

349 for f in fs.get("fields", []): 

350 if f.get("name") == field_name: 

351 return FormulaicField(f, parent) 

352 

353 def repeatable_fields(self, parent=None): 

354 if parent is None: 

355 parent = self 

356 

357 reps = [] 

358 for fs in self._definition.get("fieldsets", []): 

359 for f in fs.get("fields", []): 

360 if "repeatable" in f: 

361 reps.append(FormulaicField(f, parent)) 

362 

363 return reps 

364 

365 def disabled_fields(self, parent=None): 

366 if parent is None: 

367 parent = self 

368 

369 disableds = [] 

370 for fs in self._definition.get("fieldsets", []): 

371 for f in fs.get("fields", []): 

372 field = FormulaicField(f, parent) 

373 if field.is_disabled: 

374 disableds.append(field) 

375 

376 return disableds 

377 

378 def fieldset(self, fieldset_name): 

379 for fs in self._definition.get("fieldsets", []): 

380 if fs.get("name") == fieldset_name: 

381 return FormulaicFieldset(fs, self) 

382 return None 

383 

384 def fieldsets(self): 

385 return [FormulaicFieldset(fs, self) for fs in self._definition.get("fieldsets", [])] 

386 

387 def json(self): 

388 return json.dumps(self._definition) 

389 

390 def render_template(self, **kwargs): 

391 # ~~^-> Jinja2:Technology~~ 

392 template = self._definition.get("templates", {}).get("form") 

393 return render_template(template, formulaic_context=self, **kwargs) 

394 

395 def processor(self, formdata=None, source=None): 

396 # ~~^-> FormProcessor:Feature~~ 

397 klazz = self._definition.get("processor") 

398 if isinstance(klazz, str): 

399 klazz = plugin.load_class(klazz) 

400 return klazz(formdata=formdata, source=source, parent=self) 

401 

402 def obj2form(self, obj): 

403 # ~~^-> Crosswalk:Feature~~ 

404 xwalk_fn = self._definition.get("crosswalks", {}).get("obj2form") 

405 if xwalk_fn is None: 

406 return None 

407 if isinstance(xwalk_fn, str): 

408 xwalk_fn = plugin.load_function(xwalk_fn) 

409 data = xwalk_fn(obj) 

410 return self.wtform(data=data) 

411 

412 def form2obj(self): 

413 # ~~^-> Crosswalk:Feature~~ 

414 xwalk_fn = self._definition.get("crosswalks", {}).get("form2obj") 

415 if xwalk_fn is None: 

416 return None 

417 if isinstance(xwalk_fn, str): 

418 xwalk_fn = plugin.load_function(xwalk_fn) 

419 return xwalk_fn(self._wtform_inst) 

420 

421 def bind_wtforms_field(self, FormClass, field): 

422 field_name = field.get("name") 

423 if not hasattr(FormClass, field_name): 

424 field_definition = FormulaicField.make_wtforms_field(self, field) 

425 setattr(FormClass, field_name, field_definition) 

426 

427 def to_summary_csv(self, out_file): 

428 def _make_row(i, fs, parent, field, writer): 

429 options = "" 

430 if field.explicit_options: 

431 options = "; ".join([o.get("display") + " (" + o.get("value") + ")" for o in field.explicit_options]) 

432 elif field.options_fn_name: 

433 options = field.options_fn_name 

434 

435 label = "" 

436 if hasattr(field, "label"): 

437 label = field.label 

438 

439 name = "" 

440 if parent is not None and hasattr(parent, "name"): 

441 name = parent.name + "." 

442 if hasattr(field, "name"): 

443 name += field.name 

444 

445 writer.writerow([ 

446 i, 

447 name, 

448 label, 

449 field.input, 

450 options, 

451 field.is_disabled, 

452 fs.name 

453 ]) 

454 

455 with open(out_file, "w", encoding="utf-8") as f: 

456 writer = csv.writer(f) 

457 writer.writerow(["Form Position", "Field Name", "Label", "Input Type", "Options", "Disabled?", "Fieldset ID"]) 

458 i = 1 

459 for fs in self.fieldsets(): 

460 for field in fs.fields(): 

461 _make_row(i, fs, None, field, writer) 

462 i += 1 

463 if field.group_subfields(): 

464 for sf in field.group_subfields(): 

465 _make_row(i, fs, field, sf, writer) 

466 i += 1 

467 

468# ~~->$ FormulaicFieldset:Feature~~ 

469class FormulaicFieldset(object): 

470 def __init__(self, definition, parent): 

471 self._definition = definition 

472 self._formulaic_context = parent 

473 

474 @property 

475 def wtforms_builders(self): 

476 return self._formulaic_context.wtforms_builders 

477 

478 @property 

479 def function_map(self): 

480 return self._formulaic_context.function_map 

481 

482 @property 

483 def wtform_inst(self): 

484 return self._formulaic_context.wtform_inst 

485 

486 @property 

487 def default_field_template(self): 

488 return self._formulaic_context.default_field_template 

489 

490 @property 

491 def default_group_template(self): 

492 return self._formulaic_context.default_group_template 

493 

494 def fields(self): 

495 return [FormulaicField(f, self) for f in 

496 self._definition.get("fields", []) if not f.get("group")] 

497 

498 def field(self, field_name): 

499 for f in self._definition.get("fields", []): 

500 if f.get("name") == field_name: 

501 return FormulaicField(f, self) 

502 

503 def __getattr__(self, name): 

504 if hasattr(self.__class__, name): 

505 return object.__getattribute__(self, name) 

506 

507 if name in self._definition: 

508 return self._definition[name] 

509 

510 raise AttributeError('{name} is not set'.format(name=name)) 

511 

512# ~~->$ FormulaicField:Feature~~ 

513class FormulaicField(object): 

514 def __init__(self, definition, parent): 

515 self._definition = definition 

516 self._formulaic_fieldset = parent 

517 

518 def __contains__(self, item): 

519 return item in self._definition 

520 

521 def __getattr__(self, name): 

522 if hasattr(self.__class__, name): 

523 return object.__getattribute__(self, name) 

524 

525 if name in self._definition: 

526 return self._definition[name] 

527 

528 raise AttributeError('{name} is not set'.format(name=name)) 

529 

530 @property 

531 def parent_context(self): 

532 if isinstance(self._formulaic_fieldset, FormulaicContext): 

533 return self._formulaic_fieldset 

534 return self._formulaic_fieldset._formulaic_context 

535 

536 def get(self, attr, default=None): 

537 return self._definition.get(attr, default) 

538 

539 def help(self, key, default=None): 

540 return self._definition.get("help", {}).get(key, default) 

541 

542 @property 

543 def optional(self): 

544 return self._definition.get("optional", False) 

545 

546 @property 

547 def wtforms_builders(self): 

548 return self._formulaic_fieldset.wtforms_builders 

549 

550 @property 

551 def function_map(self): 

552 return self._formulaic_fieldset.function_map 

553 

554 @property 

555 def wtform_inst(self): 

556 if "group" in self._definition: 

557 group = self._definition["group"] 

558 group_field = self._formulaic_fieldset.field(group) 

559 return group_field.wtfield 

560 return self._formulaic_fieldset.wtform_inst 

561 

562 @property 

563 def wtfield(self): 

564 name = self._definition.get("name") 

565 if self.wtform_inst is None: 

566 return None 

567 if hasattr(self.wtform_inst, name): 

568 return getattr(self.wtform_inst, name) 

569 return None 

570 

571 @property 

572 def explicit_options(self): 

573 opts = self._definition.get("options", []) 

574 if isinstance(opts, list): 

575 return opts 

576 return [] 

577 

578 @property 

579 def options_fn_name(self): 

580 return self._definition.get("options_fn") 

581 

582 @property 

583 def is_disabled(self): 

584 differently_abled = self._definition.get("disabled", False) 

585 if isinstance(differently_abled, str): 

586 fn = self.function_map.get("disabled", {}).get(differently_abled) 

587 differently_abled = fn(self, self.parent_context) 

588 return differently_abled 

589 

590 @property 

591 def has_conditional(self): 

592 return len(self._definition.get("conditional", [])) > 0 

593 

594 @property 

595 def condition_field(self): 

596 condition = self._definition.get("conditional", []) 

597 return condition[0].get("field", None) if len(condition) > 0 else None 

598 

599 @property 

600 def condition_value(self): 

601 condition = self._definition.get("conditional", []) 

602 return condition[0].get("value", None) if len(condition) > 0 else None 

603 

604 @property 

605 def template(self): 

606 local = self._definition.get("template") 

607 if local is not None: 

608 return local 

609 

610 if self._definition.get("input") == "group": 

611 return self._formulaic_fieldset.default_group_template 

612 

613 return self._formulaic_fieldset.default_field_template 

614 

615 @property 

616 def entry_template(self): 

617 return self._definition.get("entry_template") 

618 

619 def has_validator(self, validator_name): 

620 for validator in self._definition.get("validate", []): 

621 if isinstance(validator, str) and validator == validator_name: 

622 return True 

623 if isinstance(validator, dict): 

624 if list(validator.keys())[0] == validator_name: 

625 return True 

626 return False 

627 

628 def get_validator_settings(self, validator_name): 

629 for validator in self._definition.get("validate", []): 

630 if isinstance(validator, str) and validator == validator_name: 

631 return {} 

632 if isinstance(validator, dict): 

633 name = list(validator.keys())[0] 

634 if name == validator_name: 

635 return validator[name] 

636 return False 

637 

638 def validators(self): 

639 for validator in self._definition.get("validate", []): 

640 if isinstance(validator, str): 

641 yield validator, {} 

642 if isinstance(validator, dict): 

643 name = list(validator.keys())[0] 

644 yield name, validator[name] 

645 

646 def get_subfields(self, option_value): 

647 for option in self.explicit_options: 

648 if option.get("value") == option_value: 

649 sfs = [] 

650 for sf in option.get("subfields", []): 

651 subimpl = self._formulaic_fieldset._formulaic_context.get(sf, self._formulaic_fieldset) 

652 sfs.append(subimpl) 

653 return sfs 

654 

655 def group_subfields(self): 

656 subs = self._definition.get("subfields") 

657 if subs is None: 

658 return None 

659 return [self._formulaic_fieldset.field(s) for s in subs] 

660 

661 def has_options_subfields(self): 

662 for option in self.explicit_options: 

663 if len(option.get("subfields", [])) > 0: 

664 return True 

665 return False 

666 

667 def has_errors(self): 

668 return len(self.errors()) > 0 

669 

670 def errors(self): 

671 wtf = self.wtfield 

672 return wtf.errors 

673 

674 def has_widget(self, widget_name): 

675 for w in self._definition.get("widgets", []): 

676 if w == widget_name: 

677 return True 

678 if isinstance(w, dict): 

679 if widget_name in w: 

680 return True 

681 return False 

682 

683 def render_form_control(self, custom_args=None, wtfinst=None): 

684 # ~~-> WTForms:Library~~ 

685 kwargs = deepcopy(self._definition.get("attr", {})) 

686 if "placeholder" in self._definition.get("help", {}): 

687 kwargs["placeholder"] = self._definition["help"]["placeholder"] 

688 

689 render_functions = self.function_map.get("validate", {}).get("render", {}) 

690 for validator, settings in self.validators(): 

691 if validator in render_functions: 

692 fn = render_functions[validator] 

693 if isinstance(fn, str): 

694 fn = plugin.load_function(fn) 

695 fn(settings, kwargs) 

696 

697 if self.has_options_subfields(): 

698 kwargs["formulaic"] = self 

699 

700 add_data_as_choice = False 

701 if self.is_disabled: 

702 kwargs["disabled"] = "disabled" 

703 add_data_as_choice = True 

704 

705 # allow custom args to overwite all other arguments 

706 if custom_args is not None: 

707 for k, v in custom_args.items(): 

708 kwargs[k] = v 

709 

710 wtf = None 

711 if wtfinst is not None: 

712 wtf = wtfinst 

713 else: 

714 wtf = self.wtfield 

715 

716 if add_data_as_choice and hasattr(wtf, "choices") and wtf.data not in [c[0] for c in wtf.choices]: 

717 wtf.choices += [(wtf.data, wtf.data)] 

718 

719 return wtf(**kwargs) 

720 

721 @classmethod 

722 def make_wtforms_field(cls, formulaic_context, field) -> UnboundField: 

723 builder = cls._get_wtforms_builder(field, formulaic_context.wtforms_builders) 

724 if builder is None: 

725 raise FormulaicException("No WTForms mapping for field '{x}'".format(x=field.get("name"))) 

726 

727 validators = [] 

728 vfuncs = formulaic_context.function_map.get("validate", {}).get("wtforms", {}) 

729 for v in field.get("validate", []): 

730 vname = v 

731 args = {} 

732 if isinstance(v, dict): 

733 vname = list(v.keys())[0] 

734 args = v[vname] 

735 if vname not in vfuncs: 

736 raise FormulaicException("No validate WTForms function defined for {x} in python function references".format(x=vname)) 

737 vfn = vfuncs[vname] 

738 if isinstance(vfn, str): 

739 vfn = plugin.load_function(vfn) 

740 validators.append(vfn(field, args)) 

741 

742 wtargs = { 

743 "label" : field.get("label"), 

744 "validators" : validators, 

745 "description": field.get("help", {}).get("description"), 

746 } 

747 if "default" in field: 

748 wtargs["default"] = field.get("default") 

749 if "options" in field or "options_fn" in field: 

750 wtargs["choices"] = cls._options2choices(field, formulaic_context) 

751 

752 return builder(formulaic_context, field, wtargs) 

753 

754 @classmethod 

755 def _get_wtforms_builder(self, field, wtforms_builders): 

756 for builder in wtforms_builders: 

757 if builder.match(field): 

758 return builder.wtform 

759 return None 

760 

761 @classmethod 

762 def _options2choices(self, field, formulaic_context): 

763 # function_map = formulaic_context.function_map.get("options", {}) 

764 

765 options = field.get("options", []) 

766 if len(options) == 0 and "options_fn" in field: 

767 # options = Formulaic.run_options_fn(field, formulaic_context) 

768 options = Formulaic.run_options_fn(field, formulaic_context) 

769 # fnpath = function_map.get(field["options_fn"]) 

770 # if fnpath is None: 

771 # raise FormulaicException("No function mapping defined for function reference '{x}'".format(x=field["options_fn"])) 

772 # fn = plugin.load_function(fnpath) 

773 # options = fn(field, formulaic_context.name) 

774 

775 choices = [] 

776 for o in options: 

777 display = o.get("display") 

778 value = o.get("value") 

779 if value is None: 

780 value = o.get("display") 

781 choices.append((value, display)) 

782 

783 return choices 

784 

785# ~~->$ FormProcessor:Feature~~ 

786class FormProcessor(object): 

787 def __init__(self, formdata=None, source=None, parent: FormulaicContext=None): 

788 # initialise our core properties 

789 self._source = source 

790 self._target = None 

791 self._formdata = formdata 

792 self._alert = [] 

793 self._info = '' 

794 self._formulaic = parent 

795 

796 # now create our form instance, with the form_data (if there is any) 

797 if formdata is not None: 

798 self.data2form() 

799 

800 # if there isn't any form data, then we should create the form properties from source instead 

801 elif source is not None: 

802 self.source2form() 

803 

804 # if there is no source, then a blank form object 

805 else: 

806 self.blank_form() 

807 

808 ############################################################ 

809 # getters and setters on the main FormContext properties 

810 ############################################################ 

811 

812 @property 

813 def form(self): 

814 # return self._form 

815 return self._formulaic.wtform_inst 

816 

817 @property 

818 def source(self): 

819 return self._source 

820 

821 @property 

822 def form_data(self): 

823 return self._formdata 

824 

825 @property 

826 def target(self): 

827 return self._target 

828 

829 @target.setter 

830 def target(self, val): 

831 self._target = val 

832 

833 # @property 

834 # def template(self): 

835 # return self._template 

836 # 

837 # @template.setter 

838 # def template(self, val): 

839 # self._template = val 

840 

841 @property 

842 def alert(self): 

843 return self._alert 

844 

845 def add_alert(self, val): 

846 self._alert.append(val) 

847 

848 @property 

849 def info(self): 

850 return self._info 

851 

852 @info.setter 

853 def info(self, val): 

854 self._info = val 

855 

856 ############################################################# 

857 # Lifecycle functions you don't have to overwrite unless you 

858 # want to do something different 

859 ############################################################# 

860 

861 def blank_form(self): 

862 """ 

863 This will be called during init, and must populate the self.form_data property with an instance of the form in this 

864 context, based on no originating source or form data 

865 """ 

866 self._formulaic.wtform() 

867 

868 def data2form(self): 

869 """ 

870 This will be called during init, and must convert the form_data into an instance of the form in this context, 

871 and write to self.form 

872 """ 

873 self._formulaic.wtform(formdata=self.form_data) 

874 

875 def source2form(self): 

876 """ 

877 This will be called during init, and must convert the source object into an instance of the form in this 

878 context, and write to self.form 

879 """ 

880 self._formulaic.obj2form(self.source) 

881 

882 def form2target(self): 

883 """ 

884 Convert the form object into a the target system object, and write to self.target 

885 """ 

886 self.target = self._formulaic.form2obj() 

887 

888 ############################################################ 

889 # Lifecycle functions that subclasses should implement 

890 ############################################################ 

891 

892 def pre_validate(self): 

893 """ 

894 This will be run before validation against the form is run. 

895 Use it to patch the form with any relevant data, such as fields which were disabled 

896 """ 

897 repeatables = self._formulaic.repeatable_fields() 

898 for repeatable in repeatables: 

899 wtf = repeatable.wtfield 

900 if not isinstance(wtf, FieldList): 

901 continue 

902 

903 # get all of the entries off the field list, leaving the field list temporarily empty 

904 entries = [] 

905 for i in range(len(wtf.entries)): 

906 entries.append(wtf.pop_entry()) 

907 

908 # go through each entry, and if it has any data in it put it back onto the list, otherwise 

909 # leave it off 

910 for entry in entries: 

911 if isinstance(entry, FormField): 

912 data = entry.data 

913 has_data = False 

914 for k, v in data.items(): 

915 if v: 

916 has_data = True 

917 break 

918 if not has_data: 

919 continue 

920 else: 

921 if not entry.data: 

922 continue 

923 wtf.append_entry(entry.data) 

924 

925 # finally, ensure that the minimum number of fields are populated in the list 

926 min = repeatable.get("repeatable", {}).get("minimum", 0) 

927 if min > len(wtf.entries): 

928 for i in range(len(wtf.entries), min): 

929 wtf.append_entry() 

930 

931 # patch over any disabled fields 

932 disableds = self._formulaic.disabled_fields() 

933 if len(disableds) > 0: 

934 alt_formulaic = self._formulaic.__class__(self._formulaic.name, self._formulaic._definition, self._formulaic._formulaic) 

935 other_processor = alt_formulaic.processor(source=self.source) 

936 other_form = other_processor.form 

937 for dis in disableds: 

938 if dis.get("group") is not None: 

939 dis = self._formulaic.get(dis.get("group")) 

940 

941 if dis.get("merge_disabled"): 

942 self._merge_disabled(dis, other_form) 

943 else: 

944 wtf = dis.wtfield 

945 wtf.data = other_form._fields[dis.get("name")].data 

946 

947 def _merge_disabled(self, disabled, other_form): 

948 fnref = disabled.get("merge_disabled") 

949 fn = self._formulaic.function_map.get("merge_disabled", {}).get(fnref) 

950 if not fn: 

951 raise FormulaicException("No merge_disabled function defined {x}".format(x=fnref)) 

952 fn(disabled, other_form) 

953 

954 def patch_target(self): 

955 """ 

956 Patch the target with data from the source. This will be run by the finalise method (unless you override it) 

957 """ 

958 pass 

959 

960 def finalise(self, *args, **kwargs): 

961 """ 

962 Finish up with the FormContext. Carry out any final workflow tasks, etc. 

963 """ 

964 self.form2target() 

965 self.patch_target() 

966 

967 def draft(self, *args, **kwargs): 

968 pass 

969 

970 ############################################################ 

971 # Functions which can be called directly, but may be overridden if desired 

972 ############################################################ 

973 

974 def validate(self): 

975 self.pre_validate() 

976 f = self.form 

977 valid = False 

978 if f is not None: 

979 valid = f.validate() 

980 return valid 

981 

982 @property 

983 def errors(self): 

984 f = self.form 

985 if f is not None: 

986 return f.errors 

987 return False 

988 

989 

990class WTFormsBuilder: 

991 @staticmethod 

992 def match(field): 

993 return False 

994 

995 @staticmethod 

996 def wtform(formulaic_context, field, wtfargs): 

997 return None