Coverage for portality/forms/article_forms.py: 49%

463 statements  

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

1from copy import deepcopy 

2from datetime import datetime 

3 

4from flask import render_template, url_for, request 

5from flask_login import current_user 

6from wtforms import Form, validators 

7from wtforms import StringField, TextAreaField, FormField, FieldList 

8from wtforms.fields.core import UnboundField 

9 

10from portality import regex, models 

11from portality.bll import DOAJ 

12from portality.core import app 

13from portality.crosswalks.article_form import ArticleFormXWalk 

14from portality.forms.fields import DOAJSelectField, TagListField 

15from portality.forms.validate import OptionalIf, ThisOrThat, NoScriptTag, DifferentTo 

16from portality.ui.messages import Messages 

17 

18 

19######################################### 

20# Form infrastructure 

21 

22 

23class FormContext(object): 

24 """ 

25 ~~FormContext:FormContext->Formulaic:Library~~ 

26 """ 

27 def __init__(self, form_data=None, source=None, formulaic_context=None): 

28 # initialise our core properties 

29 self._source = source 

30 self._target = None 

31 self._form_data = form_data 

32 self._form = None 

33 self._renderer = None 

34 self._template = None 

35 self._alert = [] 

36 self._info = '' 

37 self._formulaic = formulaic_context 

38 

39 # initialise the renderer (falling back to a default if necessary) 

40 self.make_renderer() 

41 if self.renderer is None: 

42 self.renderer = Renderer() 

43 

44 # specify the jinja template that will wrap the renderer 

45 self.set_template() 

46 

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

48 if form_data is not None: 

49 self.data2form() 

50 

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

52 elif source is not None: 

53 self.source2form() 

54 

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

56 else: 

57 self.blank_form() 

58 

59 ############################################################ 

60 # getters and setters on the main FormContext properties 

61 ############################################################ 

62 

63 @property 

64 def form(self): 

65 return self._form 

66 

67 @form.setter 

68 def form(self, val): 

69 self._form = val 

70 

71 @property 

72 def source(self): 

73 return self._source 

74 

75 @property 

76 def form_data(self): 

77 return self._form_data 

78 

79 @property 

80 def target(self): 

81 return self._target 

82 

83 @target.setter 

84 def target(self, val): 

85 self._target = val 

86 

87 @property 

88 def renderer(self): 

89 return self._renderer 

90 

91 @renderer.setter 

92 def renderer(self, val): 

93 self._renderer = val 

94 

95 @property 

96 def template(self): 

97 return self._template 

98 

99 @template.setter 

100 def template(self, val): 

101 self._template = val 

102 

103 @property 

104 def alert(self): 

105 return self._alert 

106 

107 def add_alert(self, val): 

108 self._alert.append(val) 

109 

110 @property 

111 def info(self): 

112 return self._info 

113 

114 @info.setter 

115 def info(self, val): 

116 self._info = val 

117 

118 ############################################################ 

119 # Lifecycle functions that subclasses should implement 

120 ############################################################ 

121 

122 def make_renderer(self): 

123 """ 

124 This will be called during init, and must populate the self.render property 

125 """ 

126 pass 

127 

128 def set_template(self): 

129 """ 

130 This will be called during init, and must populate the self.template property with the path to the jinja template 

131 """ 

132 pass 

133 

134 def pre_validate(self): 

135 """ 

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

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

138 """ 

139 pass 

140 

141 def blank_form(self): 

142 """ 

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

144 context, based on no originating source or form data 

145 """ 

146 pass 

147 

148 def data2form(self): 

149 """ 

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

151 and write to self.form 

152 """ 

153 pass 

154 

155 def source2form(self): 

156 """ 

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

158 context, and write to self.form 

159 """ 

160 pass 

161 

162 def form2target(self): 

163 """ 

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

165 """ 

166 pass 

167 

168 def patch_target(self): 

169 """ 

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

171 """ 

172 pass 

173 

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

175 """ 

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

177 """ 

178 self.form2target() 

179 self.patch_target() 

180 

181 ############################################################ 

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

183 ############################################################ 

184 

185 def validate(self): 

186 self.pre_validate() 

187 f = self.form 

188 valid = False 

189 if f is not None: 

190 valid = f.validate() 

191 

192 # if this isn't a valid form, record the fields that have errors 

193 # with the renderer for use later 

194 if not valid: 

195 error_fields = [] 

196 for field in self.form: 

197 if field.errors: 

198 error_fields.append(field.short_name) 

199 

200 return valid 

201 

202 @property 

203 def errors(self): 

204 f = self.form 

205 if f is not None: 

206 return f.errors 

207 return False 

208 

209 def render_template(self, **kwargs): 

210 return render_template(self.template, form_context=self, **kwargs) 

211 

212 def fieldset(self, fieldset_name=None): 

213 return self._formulaic.fieldset(fieldset_name) 

214 

215 def fieldsets(self): 

216 return self._formulaic.fieldsets() 

217 

218 def check_field_group_exists(self, field_group_name): 

219 return self.renderer.check_field_group_exists(field_group_name) 

220 

221 @property 

222 def ui_settings(self): 

223 return self._formulaic.ui_settings 

224 

225 

226class Renderer(object): 

227 """ 

228 ~~FormContextRenderer:FormContext->FormHelper:FormContext~~ 

229 """ 

230 def __init__(self): 

231 self.FIELD_GROUPS = {} 

232 self.fh = FormHelperBS3() 

233 self._error_fields = [] 

234 self._disabled_fields = [] 

235 self._disable_all_fields = False 

236 self._highlight_completable_fields = False 

237 

238 def check_field_group_exists(self, field_group_name): 

239 """ Return true if the field group exists in this form """ 

240 group_def = self.FIELD_GROUPS.get(field_group_name) 

241 if group_def is None: 

242 return False 

243 else: 

244 return True 

245 

246 def render_field_group(self, form_context, field_group_name=None, group_cfg=None): 

247 if field_group_name is None: 

248 return self._render_all(form_context) 

249 

250 # get the group definition 

251 group_def = self.FIELD_GROUPS.get(field_group_name) 

252 if group_def is None: 

253 return "" 

254 

255 # build the frag 

256 frag = "" 

257 for entry in group_def: 

258 field_name = list(entry.keys())[0] 

259 config = entry.get(field_name) 

260 config = deepcopy(config) 

261 

262 config = self._rewrite_extra_fields(form_context, config) 

263 field = form_context.form[field_name] 

264 

265 if field_name in self.disabled_fields or self._disable_all_fields is True: 

266 config["disabled"] = "disabled" 

267 

268 if self._highlight_completable_fields is True: 

269 valid = field.validate(form_context.form) 

270 config["complete_me"] = not valid 

271 

272 if group_cfg is not None: 

273 config.update(group_cfg) 

274 

275 frag += self.fh.render_field(field, **config) 

276 

277 return frag 

278 

279 @property 

280 def error_fields(self): 

281 return self._error_fields 

282 

283 def set_error_fields(self, fields): 

284 self._error_fields = fields 

285 

286 @property 

287 def disabled_fields(self): 

288 return self._disabled_fields 

289 

290 def set_disabled_fields(self, fields): 

291 self._disabled_fields = fields 

292 

293 def disable_all_fields(self, disable): 

294 self._disable_all_fields = disable 

295 

296 def _rewrite_extra_fields(self, form_context, config): 

297 if "extra_input_fields" in config: 

298 config = deepcopy(config) 

299 for opt, field_ref in config.get("extra_input_fields").items(): 

300 extra_field = form_context.form[field_ref] 

301 config["extra_input_fields"][opt] = extra_field 

302 return config 

303 

304 def _render_all(self, form_context): 

305 frag = "" 

306 for field in form_context.form: 

307 frag += self.fh.render_field(form_context, field.short_name) 

308 return frag 

309 

310 def find_field(self, field, field_group): 

311 for index, item in enumerate(self.FIELD_GROUPS[field_group]): 

312 if field in item: 

313 return index 

314 

315 def insert_field_after(self, field_to_insert, after_this_field, field_group): 

316 self.FIELD_GROUPS[field_group].insert( 

317 self.find_field(after_this_field, field_group) + 1, 

318 field_to_insert 

319 ) 

320 

321 

322class FormHelperBS3(object): 

323 """ 

324 ~~FormHelper:FormContext->Bootstrap3:Technology~~ 

325 ~~->WTForms:Library~~ 

326 """ 

327 def render_field(self, field, **kwargs): 

328 # begin the frag 

329 frag = "" 

330 

331 # deal with the first error if it is relevant 

332 first_error = kwargs.pop("first_error", False) 

333 if first_error: 

334 frag += '<a name="first_problem"></a>' 

335 

336 # call the correct render function on the field type 

337 if field.type == "FormField": 

338 frag += self._form_field(field, **kwargs) 

339 elif field.type == "FieldList": 

340 frag += self._field_list(field, **kwargs) 

341 else: 

342 frag += self._wrap_control_group(field, self._render_field(field, **kwargs), **kwargs) 

343 

344 return frag 

345 

346 def _wrap_control_group(self, field, contents, **kwargs): 

347 hidden = kwargs.pop("hidden", False) 

348 container_class = kwargs.pop("container_class", None) 

349 disabled = kwargs.pop("disabled", False) 

350 render_subfields_horizontal = kwargs.pop("render_subfields_horizontal", False) 

351 complete_me = kwargs.get("complete_me", False) 

352 

353 frag = '<div class="form-group' 

354 if field.errors: 

355 frag += " error" 

356 if render_subfields_horizontal: 

357 frag += " row" 

358 if container_class is not None: 

359 frag += " " + container_class 

360 if complete_me: 

361 frag += " complete-me" 

362 frag += '" id="' 

363 frag += field.short_name + '-container"' 

364 if hidden: 

365 frag += ' style="display:none;"' 

366 frag += ">" 

367 if contents is not None: 

368 frag += contents 

369 frag += "</div>" 

370 

371 return frag 

372 

373 def _form_field(self, field, **kwargs): 

374 # get the useful kwargs 

375 render_subfields_horizontal = kwargs.pop("render_subfields_horizontal", False) 

376 

377 frag = "" 

378 # for each subfield, do the render 

379 for subfield in field: 

380 if render_subfields_horizontal and not (subfield.type == 'CSRFTokenField' and not subfield.value): 

381 subfield_width = "3" 

382 remove = [] 

383 for kwarg, val in kwargs.items(): 

384 if kwarg == 'subfield_display-' + subfield.short_name: 

385 subfield_width = val 

386 remove.append(kwarg) 

387 for rm in remove: 

388 del kwargs[rm] 

389 frag += '<div class="col-md-' + subfield_width + ' nested-field-container">' 

390 frag += self._render_field(subfield, maximise_width=True, **kwargs) 

391 frag += "</div>" 

392 else: 

393 frag += self._render_field(subfield, **kwargs) 

394 

395 return self._wrap_control_group(field, frag, **kwargs) 

396 

397 def _field_list(self, field, **kwargs): 

398 # for each subfield, do the render 

399 frag = "" 

400 for subfield in field: 

401 if subfield.type == "FormField": 

402 frag += self.render_field(subfield, **kwargs) 

403 else: 

404 frag = self._wrap_control_group(field, self._render_field(field, **kwargs), **kwargs) 

405 return frag 

406 

407 def _render_field(self, field, **kwargs): 

408 # interesting arguments from keywords 

409 extra_input_fields = kwargs.get("extra_input_fields") 

410 q_num = kwargs.pop("q_num", None) 

411 maximise_width = kwargs.pop("maximise_width", False) 

412 clazz = kwargs.get("class", "") 

413 label_width = kwargs.get("label_width", 3) 

414 field_width = 12 - label_width 

415 field_width = str(kwargs.get("field_width", field_width)) 

416 if label_width > 0: 

417 label_width = str(label_width) 

418 

419 if field.type == 'CSRFTokenField' and not field.value: 

420 return "" 

421 

422 frag = "" 

423 

424 # If this is the kind of field that requires a label, give it one 

425 if field.type not in ['SubmitField', 'HiddenField', 'CSRFTokenField']: 

426 if q_num is not None: 

427 frag += '<a class="animated" name="' + q_num + '"></a>' 

428 if label_width != 0: 

429 frag += '<label class="control-label col-md-' + label_width + '" for="' + field.short_name + '">' 

430 if q_num is not None: 

431 frag += '<a class="animated orange" href="#' + field.short_name + '-container" title="Link to this question" tabindex="-1">' + q_num + ')</a>&nbsp;' 

432 frag += field.label.text 

433 if field.flags.required or field.flags.display_required_star: 

434 frag += '&nbsp;<span class="red">*</span>' 

435 frag += "</label>" 

436 

437 # determine if this is a checkbox 

438 is_checkbox = False 

439 if (field.type == "SelectMultipleField" 

440 and field.option_widget.__class__.__name__ == 'CheckboxInput' 

441 and field.widget.__class__.__name__ == 'ListWidget'): 

442 is_checkbox = True 

443 

444 extra_class = "" 

445 if is_checkbox: 

446 extra_class += " checkboxes" 

447 

448 frag += '<div class="col-md-' + field_width + ' ' + extra_class + '">' 

449 if field.type == "RadioField": 

450 for subfield in field: 

451 frag += self._render_radio(subfield, **kwargs) 

452 elif is_checkbox: 

453 frag += '<ul id="' + field.short_name + '">' 

454 for subfield in field: 

455 frag += self._render_checkbox(subfield, **kwargs) 

456 frag += "</ul>" 

457 else: 

458 if maximise_width: 

459 clazz += " col-xs-12" 

460 kwargs["class"] = clazz 

461 render_args = {} 

462 # filter anything that shouldn't go in as a field attribute 

463 for k, v in kwargs.items(): 

464 if k in ["class", "style", "disabled"] or k.startswith("data-"): 

465 render_args[k] = v 

466 frag += field(**render_args) # FIXME: this is probably going to do some weird stuff 

467 

468 if field.errors: 

469 frag += '<div class="alert alert--danger"><ul>' 

470 for error in field.errors: 

471 frag += '<li>' + error + '</li>' 

472 frag += "</ul></div>" 

473 

474 if field.description: 

475 frag += '<p class="help-block">' + field.description + '</p>' 

476 

477 frag += "</div>" 

478 return frag 

479 

480 def _render_radio(self, field, **kwargs): 

481 extra_input_fields = kwargs.pop("extra_input_fields", {}) 

482 label_width = "12" 

483 

484 frag = '<label class="radio control-label col-md-' + label_width + '" for="' + field.short_name + '">' 

485 frag += field(**kwargs) 

486 frag += '<span class="label-text">' + field.label.text + '</span>' 

487 

488 if field.label.text in list(extra_input_fields.keys()): 

489 frag += "&nbsp;" + extra_input_fields[field.label.text](**{"class" : "extra_input_field"}) 

490 

491 frag += "</label>" 

492 return frag 

493 

494 def _render_checkbox(self, field, **kwargs): 

495 extra_input_fields = kwargs.pop("extra_input_fields", {}) 

496 

497 frag = "<li>" 

498 frag += field(**kwargs) 

499 frag += '<label class="control-label" for="' + field.short_name + '">' + field.label.text + '</label>' 

500 

501 if field.label.text in list(extra_input_fields.keys()): 

502 eif = extra_input_fields[field.label.text] 

503 if not isinstance(eif, UnboundField): 

504 frag += "&nbsp;" + extra_input_fields[field.label.text](**{"class" : "extra_input_field"}) 

505 

506 frag += "</li>" 

507 return frag 

508 

509 

510######################################### 

511# Form definition 

512# ~~Article:Form~~ 

513 

514ISSN_ERROR = 'An ISSN or EISSN should be 7 or 8 digits long, separated by a dash, e.g. 1234-5678. If it is 7 digits long, it must end with the letter X (e.g. 1234-567X).' 

515EMAIL_CONFIRM_ERROR = 'Please double check the email addresses - they do not match.' 

516DATE_ERROR = "Date must be supplied in the form YYYY-MM-DD" 

517DOI_ERROR = 'Invalid DOI. A DOI can optionally start with a prefix (such as "doi:"), followed by "10." and the remainder of the identifier' 

518ORCID_ERROR = "Invalid ORCID iD. Please enter your ORCID iD as a full URL of the form https://orcid.org/0000-0000-0000-0000" 

519IDENTICAL_ISSNS_ERROR = "The Print and Online ISSNs supplied are identical. If you supply 2 ISSNs they must be different." 

520 

521start_year = app.config.get("METADATA_START_YEAR", datetime.now().year - 15) 

522YEAR_CHOICES = [(str(y), str(y)) for y in range(datetime.now().year + 1, start_year - 1, -1)] 

523MONTH_CHOICES = [("1", "01"), ("2", "02"), ("3", "03"), ("4", "04"), ("5", "05"), ("6", "06"), ("7", "07"), ("8", "08"), ("9", "09"), ("10", "10"), ("11", "11"), ("12", "12")] 

524INITIAL_AUTHOR_FIELDS = 3 

525 

526 

527def choices_for_article_issns(user, article_id=None): 

528 if "admin" in user.role and article_id is not None: 

529 # ~~->Article:Model~~ 

530 a = models.Article.pull(article_id) 

531 # ~~->Journal:Model~~ 

532 issns = models.Journal.issns_by_owner(a.get_owner()) 

533 else: 

534 issns = models.Journal.issns_by_owner(user.id) 

535 ic = [("", "Select an ISSN")] + [(i, i) for i in issns] 

536 return ic 

537 

538 

539class AuthorForm(Form): 

540 """ 

541 ~~->$ Author:Form~~ 

542 """ 

543 name = StringField("Name", [validators.Optional(),NoScriptTag()]) 

544 affiliation = StringField("Affiliation", [validators.Optional(), NoScriptTag()]) 

545 orcid_id = StringField("ORCID iD", [validators.Optional(), validators.Regexp(regex=regex.ORCID_COMPILED, message=ORCID_ERROR)]) 

546 

547 

548class ArticleForm(Form): 

549 title = StringField("Article title <em>(required)</em>", [validators.DataRequired(), NoScriptTag()]) 

550 doi = StringField("DOI", [OptionalIf("fulltext", "You must provide the DOI or the Full-Text URL"), validators.Regexp(regex=regex.DOI_COMPILED, message=DOI_ERROR)], description="(You must provide a DOI and/or a Full-Text URL)") 

551 authors = FieldList(FormField(AuthorForm), min_entries=1) # We have to do the validation for this at a higher level 

552 abstract = TextAreaField("Abstract", [validators.Optional(), NoScriptTag()]) 

553 keywords = TagListField("Keywords", [validators.Optional(), NoScriptTag()], description="Use a , to separate keywords") # enhanced with select2 

554 fulltext = StringField("Full-text URL", [OptionalIf("doi", "You must provide the Full-Text URL or the DOI"), validators.URL()]) 

555 publication_year = DOAJSelectField("Year", [validators.Optional()], choices=YEAR_CHOICES, default=str(datetime.now().year)) 

556 publication_month = DOAJSelectField("Month", [validators.Optional()], choices=MONTH_CHOICES, default=str(datetime.now().month) ) 

557 pissn = DOAJSelectField("Print", [ThisOrThat("eissn", "Either this field or Online ISSN is required"), DifferentTo("eissn", message=IDENTICAL_ISSNS_ERROR)], choices=[]) # choices set at construction 

558 eissn = DOAJSelectField("Online", [ThisOrThat("pissn", "Either this field or Print ISSN is required"), DifferentTo("pissn", message=IDENTICAL_ISSNS_ERROR)], choices=[]) # choices set at construction 

559 

560 volume = StringField("Volume", [validators.Optional(), NoScriptTag()]) 

561 number = StringField("Issue", [validators.Optional(), NoScriptTag()]) 

562 start = StringField("Start", [validators.Optional(), NoScriptTag()]) 

563 end = StringField("End", [validators.Optional(), NoScriptTag()]) 

564 

565 def __init__(self, *args, **kwargs): 

566 super(ArticleForm, self).__init__(*args, **kwargs) 

567 try: 

568 self.pissn.choices = choices_for_article_issns(current_user) 

569 self.eissn.choices = choices_for_article_issns(current_user) 

570 except: 

571 # not logged in, and current_user is broken 

572 # probably you are loading the class from the command line 

573 pass 

574 

575 

576 

577######################################### 

578# Formcontexts and factory 

579 

580class ArticleFormFactory(object): 

581 """ 

582 ~~ArticleForm:Factory->AdminArticleMetadata:FormContext~~ 

583 ~~->PublisherArticleMetadata:FormContext~~ 

584 """ 

585 @classmethod 

586 def get_from_context(cls, role, source=None, form_data=None, user=None): 

587 if role == "admin": 

588 return AdminMetadataArticleForm(source=source, form_data=form_data, user=user) 

589 if role == "publisher": 

590 return PublisherMetadataForm(source=source, form_data=form_data, user=user) 

591 

592 

593class MetadataForm(FormContext): 

594 """ 

595 ~~ArticleMetadata:FormContext->Article:Form~~ 

596 ~~->ArticleForm:Crosswalk~~ 

597 ~~->Article:Service~~ 

598 """ 

599 

600 def __init__(self, source, form_data, user): 

601 self.user = user 

602 self.author_error = False 

603 super(MetadataForm, self).__init__(source=source, form_data=form_data) 

604 

605 def _set_choices(self): 

606 try: 

607 ic = choices_for_article_issns(user=self.user, article_id=self.source.id) 

608 self.form.pissn.choices = ic 

609 self.form.eissn.choices = ic 

610 except Exception as e: 

611 print (str(e)) 

612 # not logged in, and current_user is broken 

613 # probably you are loading the class from the command line 

614 pass 

615 

616 def modify_authors_if_required(self, request_data): 

617 

618 more_authors = request_data.get("more_authors") 

619 remove_author = None 

620 for v in list(request.values.keys()): 

621 if v.startswith("remove_authors"): 

622 remove_author = v.split("-")[1] 

623 

624 # if the user wants more authors, add an extra entry 

625 if more_authors: 

626 return self.render_template(more_authors=True) 

627 

628 # if the user wants to remove an author, do the various back-flips required 

629 if remove_author is not None: 

630 return self.render_template(remove_authors=remove_author) 

631 

632 def _check_for_author_errors(self, **kwargs): 

633 

634 if "more_authors" in kwargs and kwargs["more_authors"] == True: 

635 self.form.authors.append_entry() 

636 if "remove_authors" in kwargs: 

637 keep = [] 

638 while len(self.form.authors.entries) > 0: 

639 entry = self.form.authors.pop_entry() 

640 if entry.short_name == "authors-" + kwargs["remove_author"]: 

641 break 

642 else: 

643 keep.append(entry) 

644 while len(keep) > 0: 

645 self.form.authors.append_entry(keep.pop().data) 

646 

647 def _validate_authors(self): 

648 counted = 0 

649 for entry in self.form.authors.entries: 

650 name = entry.data.get("name") 

651 if name is not None and name != "": 

652 counted += 1 

653 return counted >= 1 

654 

655 def blank_form(self): 

656 self.form = ArticleForm() 

657 self._set_choices() 

658 

659 def source2form(self): 

660 self.form = ArticleForm() 

661 ArticleFormXWalk.obj2form(self.form, article=self.source) 

662 self._set_choices() 

663 

664 def data2form(self): 

665 self.form = ArticleForm(formdata=self.form_data) 

666 self._set_choices() 

667 

668 def form2target(self): 

669 self.target = ArticleFormXWalk.form2obj(form=self.form) 

670 

671 def validate(self): 

672 if not self._validate_authors(): 

673 self.author_error = True 

674 if not self.form.validate(): 

675 return False 

676 return True 

677 

678 def finalise(self, duplicate_check = True): 

679 self.form2target() 

680 if not self.author_error: 

681 article_service = DOAJ.articleService() 

682 article_service.create_article(self.target, self.user, add_journal_info=True, 

683 update_article_id=self.source.id if self.source is not None else None, 

684 duplicate_check = duplicate_check) 

685 article_url = url_for('doaj.article_page', identifier=self.target.id) 

686 msg, how = Messages.ARTICLE_METADATA_SUBMITTED_FLASH 

687 Messages.flash_with_url(msg.format(url=article_url), how) 

688 else: 

689 return 

690 

691 

692class PublisherMetadataForm(MetadataForm): 

693 """ 

694 ~~PublisherArticleMetadata:FormContext->ArticleMetadata:FormContext~~ 

695 """ 

696 def __init__(self, source, form_data, user): 

697 super(PublisherMetadataForm, self).__init__(source=source, form_data=form_data, user=user) 

698 

699 def set_template(self): 

700 self.template = "publisher/metadata.html" 

701 

702 def render_template(self, **kwargs): 

703 self._check_for_author_errors(**kwargs) 

704 if "validated" in kwargs and kwargs["validated"] == True: 

705 self.blank_form() 

706 return render_template(self.template, form=self.form, form_context=self, author_error=self.author_error) 

707 

708 

709class AdminMetadataArticleForm(MetadataForm): 

710 """ 

711 ~~AdminArticleMetadata:FormContext->ArticleMetadata:FormContext~~ 

712 """ 

713 def __init__(self, source, form_data, user): 

714 super(AdminMetadataArticleForm, self).__init__(source=source, form_data=form_data, user=user) 

715 

716 def set_template(self): 

717 self.template = "admin/article_metadata.html" 

718 

719 def render_template(self, **kwargs): 

720 self._check_for_author_errors(**kwargs) 

721 return render_template(self.template, form=self.form, form_context=self, author_error=self.author_error)