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

1from __future__ import absolute_import 

2 

3import copy 

4import weakref 

5import re 

6import itertools 

7import inspect 

8import webob 

9import uuid 

10 

11from . import templating 

12from . import core 

13from . import util 

14from . import validation as vd 

15from . import params as pm 

16import six 

17from six.moves import filter 

18from markupsafe import Markup 

19 

20try: 

21 import formencode 

22except ImportError: 

23 formencode = None 

24 

25reserved_names = ( 

26 'parent', 

27 'demo_for', 

28 'child', 

29 'submit', 

30 'datasrc', 

31 'newlink', 

32 'edit', 

33) 

34_widget_seq = itertools.count(0) 

35_omitted = object() 

36 

37 

38class WidgetMeta(pm.ParamMeta): 

39 """ 

40 This metaclass: 

41 

42 * Detects members that are widgets, and constructs the 

43 `children` parameter based on this. 

44 * Gives widgets a sequence number, so ordering works correctly. 

45 * Calls post_define for the widget class and base classes. This 

46 is needed as it's not possible to call super() in post_define. 

47 """ 

48 @classmethod 

49 def _collect_base_children(meta, bases): 

50 ''' Collect the children from the base classes ''' 

51 children = [] 

52 for b in bases: 

53 bcld = getattr(b, 'children', None) 

54 if bcld and not isinstance(bcld, RepeatingWidgetBunchCls): 

55 children.extend(bcld) 

56 return children 

57 

58 def __new__(meta, name, bases, dct): 

59 if name != 'Widget' and 'children' not in dct: 

60 # Children not provided,  

61 # build them from class attributes. 

62 new_children = [] 

63 for d, v in list(dct.items()): 

64 if isinstance(v, type) and \ 

65 issubclass(v, Widget) and \ 

66 d not in reserved_names: 

67 

68 new_children.append((v, d)) 

69 del dct[d] 

70 

71 base_children = meta._collect_base_children(bases) 

72 new_children = sorted(new_children, key=lambda t: t[0]._seq) 

73 direct_children = [ 

74 hasattr(v, 'id') and v or v(id=d) for v, d in new_children 

75 ] 

76 direct_children_ids = set([c.id for c in direct_children]) 

77 dct['children'] = [ 

78 # Do not include children that have been overwritten in a subclass. 

79 c for c in base_children if getattr(c, "id", _omitted) not in direct_children_ids 

80 ] + direct_children 

81 

82 widget = super(WidgetMeta, meta).__new__(meta, name, bases, dct) 

83 

84 widget._seq = six.advance_iterator(_widget_seq) 

85 for w in reversed(widget.__mro__): 

86 if 'post_define' in w.__dict__: 

87 w.post_define.__func__(widget) 

88 return widget 

89 

90 

91class Widget(six.with_metaclass(WidgetMeta, pm.Parametered)): 

92 """ 

93 Base class for all widgets. 

94 """ 

95 

96 id = pm.Param('Widget identifier', request_local=False) 

97 key = pm.Param('Widget data key; None just uses id', 

98 default=None, request_local=False) 

99 template = pm.Param( 

100 'Template file for the widget, in the format ' + 

101 'engine_name:template_path. If `engine_name` is specified, this ' + 

102 'is interepreted not as a path, but as an *inline template*', 

103 ) 

104 inline_engine_name = pm.Param( 

105 'Name of an engine. If specified, `template` is interpreted as ' + 

106 'an *inline template* and not a path.', 

107 default=None, 

108 ) 

109 validator = pm.Param( 

110 'Validator for the widget.', 

111 default=None, 

112 request_local=False, 

113 ) 

114 attrs = pm.Param( 

115 "Extra attributes to include in the widget's outer-most HTML tag.", 

116 default={}, 

117 ) 

118 css_class = pm.Param( 

119 'CSS class name', 

120 default=None, 

121 attribute=True, 

122 view_name='class', 

123 ) 

124 value = pm.Param("The value for the widget.", default=None) 

125 resources = pm.Param( 

126 "Resources used by the widget. This must be an iterable, each " + \ 

127 "item of which is a :class:`Resource` subclass.", 

128 default=[], 

129 request_local=False, 

130 ) 

131 

132 error_msg = pm.Variable("Validation error message.") 

133 parent = pm.Variable( 

134 "The parent of this widget, or None if this is a root widget." 

135 ) 

136 

137 _sub_compound = False 

138 _valid_id_re = re.compile(r'^[a-zA-Z][\w\-\_\.]*$') 

139 

140 @classmethod 

141 def req(cls, **kw): 

142 """ 

143 Generate an instance of the widget. 

144 

145 Return the validated widget for this request if one exists. 

146 """ 

147 

148 ins = None 

149 

150 # Create an instance. First check for the validated widget. 

151 vw = vw_class = core.request_local().get('validated_widget') 

152 if vw: 

153 # Pull out actual class instances to compare to see if this 

154 # is really the widget that was actually validated 

155 if not getattr(vw_class, '__bases__', None): 

156 vw_class = vw.__class__ 

157 

158 if vw_class is not cls: 

159 vw = None 

160 

161 if vw: 

162 ins = vw 

163 for key, value in kw.items(): 

164 setattr(ins, key, value) 

165 

166 if ins is None: 

167 # We weren't the validated widget (or there wasn't one), so 

168 # create a new instance 

169 ins = object.__new__(cls) 

170 ins.__init__(**kw) 

171 

172 return ins 

173 

174 def __new__(cls, id=None, **kw): 

175 """ 

176 New is overloaded to return a subclass of the widget, rather than an 

177 instance. 

178 """ 

179 

180 # Support backwards compatibility with tw1-style calling 

181 if id and 'id' not in kw: 

182 kw['id'] = id 

183 

184 newname = calc_name(cls, kw) 

185 return type(cls.__name__ + '_s', (cls, ), kw) 

186 

187 def __init__(self, **kw): 

188 for k, v in six.iteritems(kw): 

189 setattr(self, k, v) 

190 self._js_calls = [] 

191 

192 @classmethod 

193 def post_define(cls): 

194 """ 

195 This is a class method, that is called when a subclass of this Widget 

196 is created. Process static configuration here. Use it like this:: 

197 

198 class MyWidget(LeafWidget): 

199 @classmethod 

200 def post_define(cls): 

201 id = getattr(cls, 'id', None) 

202 if id and not id.startswith('my'): 

203 raise pm.ParameterError("id must start with 'my'") 

204 

205 post_define should always cope with missing data - the class may be an 

206 abstract class. There is no need to call super(), the metaclass will do 

207 this automatically. 

208 """ 

209 

210 if getattr(cls, 'id', None): 

211 if not cls._valid_id_re.match(cls.id): 

212 # http://www.w3schools.com/tags/att_standard_id.asp 

213 raise pm.ParameterError( 

214 "Not a valid W3C id: '%s'" % cls.id) 

215 

216 if hasattr(cls, 'id') and not getattr(cls, 'key', None): 

217 cls.key = cls.id 

218 

219 cls.compound_id = cls._gen_compound_id(for_url=False) 

220 if cls.compound_id: 

221 cls.attrs = cls.attrs.copy() 

222 cls.attrs['id'] = cls.compound_id 

223 

224 cls.compound_key = cls._gen_compound_key() 

225 

226 if hasattr(cls, 'request') and getattr(cls, 'id', None): 

227 from . import middleware 

228 path = cls._gen_compound_id(for_url=True) 

229 middleware.register_controller(cls, path) 

230 

231 if cls.validator: 

232 if cls.validator is pm.Required: 

233 vld = cls.__mro__[1].validator 

234 cls.validator = vld and vld.clone(required=True) or \ 

235 vd.Validator(required=True) 

236 

237 if isinstance(cls.validator, type) and \ 

238 issubclass(cls.validator, vd.Validator): 

239 cls.validator = cls.validator() 

240 

241 if formencode and isinstance(cls.validator, type) and \ 

242 issubclass(cls.validator, formencode.Validator): 

243 cls.validator = cls.validator() 

244 

245 if not isinstance(cls.validator, vd.Validator) and \ 

246 not (formencode and 

247 isinstance(cls.validator, formencode.Validator)): 

248 raise pm.ParameterError( 

249 "Validator must be either a tw2 or FormEncode validator" 

250 ) 

251 

252 cls.resources = [r(parent=cls) for r in cls.resources] 

253 cls._deferred = [k for k, v in inspect.getmembers(cls) 

254 if isinstance(v, pm.Deferred)] 

255 cls._attr = [p.name for p in cls._params.values() if p.attribute] 

256 

257 if cls.parent: 

258 for p in cls.parent._all_params.values(): 

259 if p.child_param and \ 

260 not hasattr(cls, p.name) and \ 

261 p.default is not pm.Required: 

262 

263 setattr(cls, p.name, p.default) 

264 

265 @classmethod 

266 def _gen_compound_name(cls, attr, for_url): 

267 ancestors = [] 

268 cur = cls 

269 while cur: 

270 if cur in ancestors: 

271 raise core.WidgetError('Parent loop') 

272 ancestors.append(cur) 

273 cur = cur.parent 

274 elems = reversed(list(filter(None, [ 

275 a._compound_name_elem(attr, for_url) for a in ancestors 

276 ]))) 

277 if getattr(cls, attr, None) or \ 

278 (cls.parent and issubclass(cls.parent, RepeatingWidget)): 

279 return ':'.join(elems) 

280 else: 

281 return None 

282 

283 @classmethod 

284 def _compound_name_elem(cls, attr, for_url): 

285 if cls.parent and issubclass(cls.parent, RepeatingWidget): 

286 if for_url: 

287 return None 

288 else: 

289 return str(getattr(cls, 'repetition', None)) 

290 else: 

291 return getattr(cls, attr, None) 

292 

293 @classmethod 

294 def _compound_id_elem(cls, for_url): 

295 return cls._compound_name_elem('id', for_url) 

296 

297 @classmethod 

298 def _gen_compound_id(cls, for_url): 

299 return cls._gen_compound_name('id', for_url) 

300 

301 @classmethod 

302 def _gen_compound_key(cls): 

303 return cls._gen_compound_name('key', False) 

304 

305 @classmethod 

306 def get_link(cls): 

307 """ 

308 Get the URL to the controller . This is called at run time, not startup 

309 time, so we know the middleware if configured with the controller path. 

310 Note: this function is a temporary measure, a cleaner API for this is 

311 planned. 

312 """ 

313 if not hasattr(cls, 'request') or not getattr(cls, 'id', None): 

314 raise core.WidgetError('Not a controller widget') 

315 mw = core.request_local()['middleware'] 

316 return mw.config.controller_prefix + cls._gen_compound_id(for_url=True) 

317 

318 def prepare(self): 

319 """ 

320 This is an instance method, that is called just before the Widget is 

321 displayed. Process request-local configuration here. For 

322 efficiency, widgets should do as little work as possible here. 

323 Use it like this:: 

324 

325 class MyWidget(Widget): 

326 def prepare(self): 

327 super(MyWidget, self).prepare() 

328 self.value = 'My: ' + str(self.value) 

329 """ 

330 

331 # First, if we don't already have an id, then pick a random one. 

332 if not hasattr(self, 'id'): 

333 self.id = 'id_' + str(uuid.uuid4()).replace('-', '') 

334 

335 # Then, enforce any params marked with twc.Required. 

336 for k, v in self._params.items(): 

337 if v.default is pm.Required and not hasattr(self, k): 

338 raise ValueError( 

339 "%r is a required Parameter for %r" % (k, self)) 

340 

341 for a in self._deferred: 

342 dfr = getattr(self, a) 

343 if isinstance(dfr, pm.Deferred): 

344 setattr(self, a, dfr.fn()) 

345 

346 if self.validator and not hasattr(self, '_validated'): 

347 value = self.value 

348 

349 # Handles the case where FE expects dict-like object, but 

350 # you have None at your disposal. 

351 if formencode and \ 

352 isinstance(self.validator, formencode.Validator) and \ 

353 self.value is None: 

354 value = {} 

355 

356 try: 

357 value = self.validator.from_python(value) 

358 except vd.catch as e: 

359 value = str(value) 

360 self.error_msg = e.msg 

361 

362 if formencode and value == {} and self.value is None: 

363 value = None 

364 

365 self.value = value 

366 

367 if self._attr or 'attrs' in self.__dict__: 

368 self.attrs = self.attrs.copy() 

369 if self.compound_id: 

370 self.attrs['id'] = self.compound_id 

371 

372 for a in self._attr: 

373 view_name = self._params[a].view_name 

374 if self.attrs.get(view_name): 

375 raise pm.ParameterError( 

376 "Attr param clashes with user-supplied attr: '%s'" % a 

377 ) 

378 self.attrs[view_name] = getattr(self, a) 

379 

380 def iteritems(self): 

381 """ 

382 An iterator which will provide the params of the widget in 

383 key, value pairs. 

384 """ 

385 for param in self._params.keys(): 

386 value = getattr(self, param) 

387 yield param, value 

388 

389 @util.class_or_instance 

390 def controller_path(self, cls): 

391 """ Return the URL path against which this widget's controller is 

392 mounted or None if it is not registered with the ControllerApp. 

393 """ 

394 

395 mw = core.request_local().get('middleware') 

396 return mw.controllers.controller_path(cls) 

397 

398 @util.class_or_instance 

399 def add_call(self, extra_arg, call, location="bodybottom"): 

400 """ 

401 Not sure what the "extra_arg" needed is for, but it is needed, as is 

402 the decorator, or an infinite loop ensues. 

403 

404 Adds a :func:`tw.api.js_function` call that will be made when the 

405 widget is rendered. 

406 """ 

407 #log.debug("Adding call <%s> for %r statically.", call, self) 

408 self._js_calls.append([call, location]) 

409 

410 @util.class_or_instance 

411 def display(self, cls, value=None, displays_on=None, **kw): 

412 """Display the widget - render the template. In the template, the 

413 widget instance is available as the variable ``$w``. 

414 

415 If display is called on a class, it automatically creates an instance. 

416 

417 `displays_on` 

418 The name of the template engine this widget is being displayed 

419 inside. If not specified, this is determined automatically, from 

420 the parent's template engine, or the default, if there is no 

421 parent. Set this to ``string`` to get raw string output. 

422 """ 

423 

424 # Support backwards compatibility with tw1-style calling 

425 if value is not None and 'value' not in kw: 

426 kw['value'] = value 

427 

428 # Support arguments to .display on either instance or class 

429 # https://github.com/toscawidgets/tw2.core/issues/41 

430 if self: 

431 for key, value in kw.items(): 

432 setattr(self, key, value) 

433 else: 

434 self = cls.req(**kw) 

435 

436 # Register any deferred params that are handed to us late in the game 

437 # (after post_define). The .prepare method handles processing them 

438 # later. 

439 self._deferred += [k for k, v in kw.items() if isinstance(v, pm.Deferred)] 

440 

441 if not self.parent: 

442 self.prepare() 

443 

444 if self._js_calls: 

445 self.safe_modify('resources') 

446 #avoids circular reference 

447 from . import resources as rs 

448 for item in self._js_calls: 

449 if 'JSFuncCall' in repr(item[0]): 

450 self.resources.append(item[0]) 

451 else: 

452 self.resources.append(rs._JSFuncCall( 

453 src=str(item[0]), 

454 location=item[1], 

455 )) 

456 

457 if self.resources: 

458 self.resources = WidgetBunch([r.req() for r in self.resources]) 

459 for r in self.resources: 

460 r.prepare() 

461 

462 return self.generate_output(displays_on) 

463 

464 def generate_output(self, displays_on): 

465 """ 

466 Generate the actual output text for this widget. 

467 

468 By default this renders the widget's template. Subclasses can override 

469 this method for purely programmatic output. 

470 

471 `displays_on` 

472 The name of the template engine this widget is being displayed 

473 inside. 

474 

475 Use it like this:: 

476 

477 class MyWidget(LeafWidget): 

478 def generate_output(self, displays_on): 

479 return "<span {0}>{1}</span>".format(self.attrs, self.text) 

480 """ 

481 

482 mw = core.request_local().get('middleware') 

483 

484 if not displays_on: 

485 displays_on = self._get_default_displays_on(mw) 

486 

487 # Build the arguments used while rendering the template 

488 kwargs = {'w': self} 

489 if mw and mw.config.params_as_vars: 

490 for p in self._params: 

491 if hasattr(self, p): 

492 kwargs[p] = getattr(self, p) 

493 

494 if self.template is None: 

495 raise ValueError("A template must be provided.") 

496 

497 return templating.render( 

498 self.template, 

499 displays_on, 

500 kwargs, 

501 self.inline_engine_name, 

502 mw, 

503 ) 

504 

505 def _get_default_displays_on(self, mw): 

506 if not self.parent: 

507 if mw: 

508 return mw.config.default_engine 

509 return 'string' 

510 else: 

511 return templating.get_engine_name(self.parent.template, mw) 

512 

513 @classmethod 

514 def validate(cls, params, state=None): 

515 """ 

516 Validate form input. This should always be called on a class. It 

517 either returns the validated data, or raises a 

518 :class:`ValidationError` exception. 

519 """ 

520 if cls.parent: 

521 raise core.WidgetError('Only call validate on root widgets') 

522 value = vd.unflatten_params(params) 

523 if hasattr(cls, 'id') and cls.id: 

524 value = value.get(cls.id, {}) 

525 ins = cls.req() 

526 

527 # Key the validated widget by class id 

528 core.request_local()['validated_widget'] = ins 

529 return ins._validate(value, state) 

530 

531 @vd.catch_errors 

532 def _validate(self, value, state=None): 

533 """ 

534 Inner validation method; this is called by validate and should not be 

535 called directly. Overriding this method in widgets is discouraged; a 

536 custom validator should be coded instead. However, in some 

537 circumstances overriding is necessary. 

538 """ 

539 self._validated = True 

540 self.value = value 

541 if self.validator: 

542 value = self.validator.to_python(value, state) 

543 return value 

544 

545 def safe_modify(self, attr): 

546 if (attr not in self.__dict__ and 

547 isinstance(getattr(self, attr, None), (dict, list))): 

548 setattr(self, attr, copy.copy(getattr(self, attr))) 

549 

550 @classmethod 

551 def children_deep(cls): 

552 yield cls 

553 

554 

555class LeafWidget(Widget): 

556 """ 

557 A widget that has no children; this is the most common kind, e.g. form 

558 fields. 

559 """ 

560 

561 

562class WidgetBunch(list): 

563 def __getattr__(self, id): 

564 for w in self: 

565 if w.id == id: 

566 return w 

567 raise AttributeError("Widget has no child named '%s'" % id) 

568 

569 

570class CompoundWidget(Widget): 

571 """ 

572 A widget that has an arbitrary number of children, this is common for 

573 layout components, such as :class:`tw2.forms.TableLayout`. 

574 """ 

575 children = pm.Param( 

576 'Children for this widget. This must be an iterable, ' + 

577 'each item of which is a Widget' 

578 ) 

579 c = pm.Variable( 

580 "Alias for children", 

581 default=property(lambda s: s.children), 

582 ) 

583 children_deep = pm.Variable( 

584 "Children, including any children from child " + 

585 "CompoundWidgets that have no id", 

586 ) 

587 template = 'tw2.core.templates.display_children' 

588 separator = pm.Param('HTML snippet which will be inserted ' 

589 'between each repeated child', default=None) 

590 

591 @classmethod 

592 def post_define(cls): 

593 """ 

594 Check children are valid; update them to have a link to the parent. 

595 """ 

596 cls._sub_compound = not getattr(cls, 'id', None) 

597 if not hasattr(cls, 'children'): 

598 return 

599 

600 joined_cld = [] 

601 for c in cls.children: 

602 if not isinstance(c, type) or not issubclass(c, Widget): 

603 raise pm.ParameterError("All children must be widgets") 

604 joined_cld.append(c(parent=cls)) 

605 

606 ids = set() 

607 for c in cls.children_deep(): 

608 if getattr(c, 'id', None): 

609 if c.id in ids: 

610 raise core.WidgetError("Duplicate id '%s'" % c.id) 

611 ids.add(c.id) 

612 

613 cls.children = WidgetBunch(joined_cld) 

614 cls.keyed_children = [ 

615 c.id for c in joined_cld 

616 if hasattr(c, 'key') and hasattr(c, 'id') and c.key != c.id 

617 ] 

618 

619 def __init__(self, **kw): 

620 super(CompoundWidget, self).__init__(**kw) 

621 self.children = WidgetBunch( 

622 c.req(parent=weakref.proxy(self)) 

623 for c in self.children 

624 ) 

625 

626 def prepare(self): 

627 """ 

628 Propagate the value for this widget to the children, based on their id. 

629 """ 

630 super(CompoundWidget, self).prepare() 

631 if self.separator: 

632 self.separator = Markup(self.separator) 

633 v = self.value or {} 

634 if not hasattr(self, '_validated'): 

635 if hasattr(v, '__getitem__'): 

636 for c in self.children: 

637 if c._sub_compound: 

638 c.value = v 

639 elif c.key in v: 

640 c.value = v[c.key] 

641 else: 

642 for c in self.children: 

643 if c._sub_compound: 

644 c.value = self.value 

645 else: 

646 c.value = getattr(self.value, c.key or '', None) 

647 for c in self.children: 

648 c.prepare() 

649 

650 def get_child_error_message(self, name): 

651 if isinstance(self.error_msg, six.string_types): 

652 if self.error_msg.startswith(name + ':'): 

653 return self.error_msg.split(':')[1] 

654 

655 @vd.catch_errors 

656 def _validate(self, value, state=None): 

657 """ 

658 The value must be a dict, or None. Each item in the dict is passed to 

659 the corresponding child widget for validation, with special 

660 consideration for _sub_compound widgets. If a child returns 

661 vd.EmptyField, that value is not included in the resulting dict at all, 

662 which is different to including None. Child widgets with a key are 

663 passed the validated value from the field the key references. The 

664 resulting dict is validated by this widget's validator. If any child 

665 widgets produce an errors, this results in a "childerror" failure. 

666 """ 

667 self._validated = True 

668 value = value or {} 

669 if not isinstance(value, dict): 

670 raise vd.ValidationError('corrupt', self.validator) 

671 self.value = value 

672 any_errors = False 

673 data = {} 

674 

675 state = util.clone_object(state, full_dict=value, validated_values=data) 

676 

677 # Validate compound children 

678 for c in (child for child in self.children if child._sub_compound): 

679 try: 

680 data.update(c._validate(value, state)) 

681 except vd.catch as e: 

682 if hasattr(e, 'msg'): 

683 c.error_msg = e.msg 

684 any_errors = True 

685 

686 # Validate non compound children 

687 for c in (child for child in self.children if not child._sub_compound): 

688 d = value.get(c.key, '') 

689 try: 

690 val = c._validate(d, state) 

691 if val is not vd.EmptyField: 

692 data[c.key] = val 

693 except vd.catch as e: 

694 if hasattr(e, 'msg'): 

695 c.error_msg = e.msg 

696 data[c.key] = vd.Invalid 

697 any_errors = True 

698 

699 # Validate self, usually a CompoundValidator or a FormEncode form-level 

700 # validator. 

701 exception_validator = self.validator 

702 if self.validator: 

703 try: 

704 data = self.validator.to_python(data, state) 

705 except vd.catch as e: 

706 # If it failed to validate, check if the error_dict has any 

707 # messages pertaining specifically to this widget's children. 

708 error_dict = getattr(e, 'error_dict', {}) 

709 if not error_dict: 

710 raise 

711 

712 for c in self.children: 

713 if getattr(c, 'key', None) in error_dict: 

714 c.error_msg = error_dict[c.key] 

715 data[c.key] = vd.Invalid 

716 exception_validator = None 

717 any_errors = True 

718 

719 # Only re-raise this top-level exception if the validation 

720 # error doesn't pertain to any of our children. 

721 if exception_validator: 

722 raise 

723 

724 if any_errors: 

725 raise vd.ValidationError('childerror', exception_validator) 

726 

727 return data 

728 

729 @classmethod 

730 def children_deep(cls): 

731 if getattr(cls, 'id', None): 

732 yield cls 

733 else: 

734 for c in getattr(cls, 'children', []): 

735 for cc in c.children_deep(): 

736 yield cc 

737 

738 

739class RepeatingWidgetBunchCls(object): 

740 

741 def __init__(self, parent): 

742 self.parent = parent 

743 self._repetition_cache = {} 

744 

745 def __getitem__(self, item): 

746 if not isinstance(item, int): 

747 raise KeyError("Must specify an integer") 

748 try: 

749 rep = self._repetition_cache[item] 

750 except KeyError: 

751 rep = self.parent.child(parent=self.parent, repetition=item) 

752 self._repetition_cache[item] = rep 

753 return rep 

754 

755 

756class RepeatingWidgetBunch(object): 

757 def __init__(self, parent, rwbc): 

758 self.parent = parent 

759 self.rwbc = rwbc 

760 self._repetition_cache = {} 

761 

762 def __len__(self): 

763 return self.parent.repetitions 

764 

765 def __iter__(self): 

766 for i in range(len(self)): 

767 yield self[i] 

768 

769 def __getitem__(self, item): 

770 if not isinstance(item, int): 

771 raise KeyError("Must specify an integer") 

772 try: 

773 rep = self._repetition_cache[item] 

774 except KeyError: 

775 rep = self.rwbc[item].req(parent=weakref.proxy(self.parent)) 

776 self._repetition_cache[item] = rep 

777 return rep 

778 

779 

780class RepeatingWidget(Widget): 

781 """ 

782 A widget that has a single child, which is repeated an arbitrary number 

783 of times, such as :class:`tw2.forms.GridLayout`. 

784 """ 

785 child = pm.Param('Child for this widget. The child must have no id.') 

786 repetitions = pm.Param( 

787 'Fixed number of repetitions. If this is None, it dynamically ' + 

788 'determined, based on the length of the value list.', 

789 default=None, 

790 ) 

791 min_reps = pm.Param('Minimum number of repetitions', default=None) 

792 max_reps = pm.Param('Maximum number of repetitions', default=None) 

793 extra_reps = pm.Param( 

794 'Number of extra repeitions, beyond the length of the value list.', 

795 default=0, 

796 ) 

797 children = pm.Param( 

798 'Children specified for this widget will be passed to the child. ' + 

799 'In the template, children gets the list of repeated childen.', 

800 default=[], 

801 ) 

802 

803 repetition = pm.ChildVariable('The repetition of a child widget.') 

804 

805 template = 'tw2.core.templates.display_children' 

806 separator = pm.Param('HTML snippet which will be inserted ' 

807 'between each repeated child', default=None) 

808 

809 @classmethod 

810 def post_define(cls): 

811 """ 

812 Check child is valid; update with link to parent. 

813 """ 

814 if not hasattr(cls, 'child'): 

815 return 

816 

817 if getattr(cls, 'children', None): 

818 cls.child = cls.child(children=cls.children) 

819 cls.children = [] 

820 

821 if not isinstance(cls.child, type) or \ 

822 not issubclass(cls.child, Widget): 

823 raise pm.ParameterError("Child must be a Widget") 

824 

825 if issubclass(cls.child, DisplayOnlyWidget): 

826 raise pm.ParameterError('Child cannot be a DisplayOnlyWidget') 

827 

828 if getattr(cls.child, 'id', None): 

829 raise pm.ParameterError("Child must have no id") 

830 

831 cls.child = cls.child(parent=cls) 

832 cls.rwbc = RepeatingWidgetBunchCls(parent=cls) 

833 

834 def __init__(self, **kw): 

835 super(RepeatingWidget, self).__init__(**kw) 

836 self.children = RepeatingWidgetBunch(self, self.rwbc) 

837 

838 def prepare(self): 

839 """ 

840 Propagate the value for this widget to the children, based on their 

841 index. 

842 """ 

843 super(RepeatingWidget, self).prepare() 

844 if self.separator: 

845 self.separator = Markup(self.separator) 

846 value = self.value or [] 

847 if self.repetitions is None: 

848 reps = len(value) + self.extra_reps 

849 if self.max_reps is not None and reps > self.max_reps: 

850 reps = self.max_reps 

851 if self.min_reps is not None and reps < self.min_reps: 

852 reps = self.min_reps 

853 self.repetitions = reps 

854 

855 for i, v in enumerate(value): 

856 self.children[i].value = v 

857 for c in self.children: 

858 c.prepare() 

859 if not self.repetitions: 

860 self.children[0].prepare() 

861 

862 @vd.catch_errors 

863 def _validate(self, value, state=None): 

864 """ 

865 The value must either be a list or None. Each item in the list is 

866 passed to the corresponding child widget for validation. The resulting 

867 list is passed to this widget's validator. If any of the child widgets 

868 produces a validation error, this widget generates a "childerror" 

869 failure. 

870 """ 

871 self._validated = True 

872 value = value or [] 

873 if not isinstance(value, list): 

874 raise vd.ValidationError('corrupt', self.validator, self) 

875 self.value = value 

876 any_errors = False 

877 data = [] 

878 

879 state = util.clone_object(state, full_dict=value, validated_values=data) 

880 

881 for i, v in enumerate(value): 

882 try: 

883 data.append(self.children[i]._validate(v, state)) 

884 except vd.catch: 

885 data.append(vd.Invalid) 

886 any_errors = True 

887 if self.validator: 

888 data = self.validator.to_python(data, state) 

889 if any_errors: 

890 raise vd.ValidationError('childerror', self.validator, self) 

891 return data 

892 

893 

894class DisplayOnlyWidgetMeta(WidgetMeta): 

895 @classmethod 

896 def _collect_base_children(meta, bases): 

897 children = [] 

898 for b in bases: 

899 bchild = getattr(b, 'child', None) 

900 if bchild: 

901 b = b.child 

902 bcld = getattr(b, 'children', None) 

903 if bcld and not isinstance(bcld, RepeatingWidgetBunchCls): 

904 children.extend(bcld) 

905 return children 

906 

907 

908def calc_name(cls, kw, char='s'): 

909 if 'parent' in kw: 

910 newname = kw['parent'].__name__ + '__' + cls.__name__ 

911 else: 

912 newname = cls.__name__ + '_%s' % char 

913 return newname 

914 

915 

916class DisplayOnlyWidget(six.with_metaclass(DisplayOnlyWidgetMeta, Widget)): 

917 """ 

918 A widget that has a single child. The parent widget is only used for 

919 display purposes; it does not affect value propagation or validation. 

920 This is used by widgets like :class:`tw2.forms.FieldSet`. 

921 """ 

922 child = pm.Param('Child for this widget.') 

923 children = pm.Param( 

924 'Children specified for this widget will be passed to the child', 

925 default=[], 

926 ) 

927 id_suffix = pm.Variable('Suffix to append to compound IDs', default=None) 

928 

929 def __new__(cls, **kw): 

930 newname = calc_name(cls, kw, 'd') 

931 return type(newname, (cls,), kw) 

932 

933 @classmethod 

934 def post_define(cls): 

935 if not getattr(cls, 'child', None): 

936 return 

937 

938 if getattr(cls, 'children', None): 

939 cls.child = cls.child(children=cls.children) 

940 cls.children = [] 

941 

942 if getattr(cls, 'validator', None): 

943 cls.child.validator = cls.validator 

944 cls.validator = None 

945 

946 if not isinstance(cls.child, type) or \ 

947 not issubclass(cls.child, Widget): 

948 raise pm.ParameterError("Child must be a widget") 

949 

950 cls.compound_key = None 

951 cls._sub_compound = cls.child._sub_compound 

952 cls_id = getattr(cls, 'id', None) 

953 child_id = getattr(cls.child, 'id', None) 

954 if cls_id and child_id and cls_id != child_id: 

955 raise pm.ParameterError( 

956 "Can only specify id on either a DisplayOnlyWidget, or " + 

957 "its child, not both: '%s' '%s'" % (cls_id, child_id) 

958 ) 

959 if not cls_id and child_id: 

960 cls.id = child_id 

961 DisplayOnlyWidget.post_define.__func__(cls) 

962 Widget.post_define.__func__(cls) 

963 cls.child = cls.child(parent=cls, key=cls.key) 

964 else: 

965 cls.child = cls.child(id=cls_id, key=cls.key, parent=cls) 

966 

967 @classmethod 

968 def _gen_compound_name(cls, attr, for_url): 

969 elems = [ 

970 Widget._gen_compound_name.__func__(cls, attr, for_url), 

971 getattr(cls, attr, None) 

972 ] 

973 elems = list(filter(None, elems)) 

974 if not elems: 

975 return None 

976 if not for_url and attr=='id' and getattr(cls, 'id_suffix', None): 

977 elems.append(cls.id_suffix) 

978 return ':'.join(elems) 

979 

980 @classmethod 

981 def _compound_name_elem(cls, attr, for_url): 

982 if cls.parent and issubclass(cls.parent, RepeatingWidget): 

983 Widget._compound_name_elem.__func__(cls, attr, for_url) 

984 else: 

985 return None 

986 

987 def __init__(self, **kw): 

988 super(DisplayOnlyWidget, self).__init__(**kw) 

989 if hasattr(self, 'child'): 

990 self.child = self.child.req(parent=weakref.proxy(self)) 

991 else: 

992 self.child = None 

993 

994 def prepare(self): 

995 super(DisplayOnlyWidget, self).prepare() 

996 if self.child: 

997 if not hasattr(self, '_validated'): 

998 self.child.value = self.value 

999 self.child.prepare() 

1000 

1001 @vd.catch_errors 

1002 def _validate(self, value, state=None): 

1003 self._validated = True 

1004 try: 

1005 return self.child._validate(value, state) 

1006 except vd.ValidationError: 

1007 raise vd.ValidationError('childerror', self.validator, self) 

1008 

1009 @classmethod 

1010 def children_deep(cls): 

1011 for c in cls.child.children_deep(): 

1012 yield c 

1013 

1014 

1015def default_content_type(): 

1016 "default_content_type" 

1017 return "text/html; charset=%s" % ( 

1018 core.request_local()['middleware'].config.encoding 

1019 ) 

1020 

1021 

1022class Page(DisplayOnlyWidget): 

1023 """ 

1024 An HTML page. This widget includes a :meth:`request` method that serves 

1025 the page. 

1026 """ 

1027 title = pm.Param('Title for the page', default=None) 

1028 content_type = pm.Param( 

1029 'Content type header', 

1030 default=pm.Deferred(default_content_type), 

1031 request_local=False, 

1032 ) 

1033 template = "tw2.core.templates.page" 

1034 id_suffix = 'page' 

1035 _no_autoid = True 

1036 

1037 @classmethod 

1038 def post_define(cls): 

1039 if not getattr(cls, 'id', None) and '_no_autoid' not in cls.__dict__: 

1040 cls.id = cls.__name__.lower() 

1041 DisplayOnlyWidget.post_define.__func__(cls) 

1042 Widget.post_define.__func__(cls) 

1043 

1044 @classmethod 

1045 def request(cls, req): 

1046 ct = cls.content_type 

1047 if isinstance(ct, pm.Deferred): 

1048 ct = ct.fn() 

1049 resp = webob.Response(request=req, content_type=ct) 

1050 ins = cls.req() 

1051 ins.fetch_data(req) 

1052 resp.body = ins.display().encode( 

1053 core.request_local()['middleware'].config.encoding 

1054 ) 

1055 return resp 

1056 

1057 def fetch_data(self, req): 

1058 pass