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

472 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-04 09:41 +0100

1from copy import deepcopy 

2from typing import Literal, Optional 

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.lib import dates 

17from portality.ui.messages import Messages 

18from portality.ui import templates 

19 

20 

21######################################### 

22# Form infrastructure 

23 

24 

25class FormContext(object): 

26 """ 

27 ~~FormContext:FormContext->Formulaic:Library~~ 

28 """ 

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

30 # initialise our core properties 

31 self._source = source 

32 self._target = None 

33 self._form_data = form_data 

34 self._form = None 

35 self._renderer = None 

36 self._template = None 

37 self._alert = [] 

38 self._info = '' 

39 self._formulaic = formulaic_context 

40 

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

42 self.make_renderer() 

43 if self.renderer is None: 

44 self.renderer = Renderer() 

45 

46 # specify the jinja template that will wrap the renderer 

47 self.set_template() 

48 

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

50 if form_data is not None: 

51 self.data2form() 

52 

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

54 elif source is not None: 

55 self.source2form() 

56 

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

58 else: 

59 self.blank_form() 

60 

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

62 # getters and setters on the main FormContext properties 

63 ############################################################ 

64 

65 @property 

66 def form(self): 

67 return self._form 

68 

69 @form.setter 

70 def form(self, val): 

71 self._form = val 

72 

73 @property 

74 def source(self) -> Optional: 

75 return self._source 

76 

77 @property 

78 def form_data(self): 

79 return self._form_data 

80 

81 @property 

82 def target(self): 

83 return self._target 

84 

85 @target.setter 

86 def target(self, val): 

87 self._target = val 

88 

89 @property 

90 def renderer(self): 

91 return self._renderer 

92 

93 @renderer.setter 

94 def renderer(self, val): 

95 self._renderer = val 

96 

97 @property 

98 def template(self): 

99 return self._template 

100 

101 @template.setter 

102 def template(self, val): 

103 self._template = val 

104 

105 @property 

106 def alert(self): 

107 return self._alert 

108 

109 def add_alert(self, val): 

110 self._alert.append(val) 

111 

112 @property 

113 def info(self): 

114 return self._info 

115 

116 @info.setter 

117 def info(self, val): 

118 self._info = val 

119 

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

121 # Lifecycle functions that subclasses should implement 

122 ############################################################ 

123 

124 def make_renderer(self): 

125 """ 

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

127 """ 

128 pass 

129 

130 def set_template(self): 

131 """ 

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

133 """ 

134 pass 

135 

136 def pre_validate(self): 

137 """ 

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

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

140 """ 

141 pass 

142 

143 def blank_form(self): 

144 """ 

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

146 context, based on no originating source or form data 

147 """ 

148 pass 

149 

150 def data2form(self): 

151 """ 

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

153 and write to self.form 

154 """ 

155 pass 

156 

157 def source2form(self): 

158 """ 

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

160 context, and write to self.form 

161 """ 

162 pass 

163 

164 def form2target(self): 

165 """ 

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

167 """ 

168 pass 

169 

170 def patch_target(self): 

171 """ 

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

173 """ 

174 pass 

175 

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

177 """ 

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

179 """ 

180 self.form2target() 

181 self.patch_target() 

182 

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

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

185 ############################################################ 

186 

187 def validate(self): 

188 self.pre_validate() 

189 f = self.form 

190 valid = False 

191 if f is not None: 

192 valid = f.validate() 

193 

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

195 # with the renderer for use later 

196 if not valid: 

197 error_fields = [] 

198 for field in self.form: 

199 if field.errors: 

200 error_fields.append(field.short_name) 

201 

202 return valid 

203 

204 @property 

205 def errors(self): 

206 f = self.form 

207 if f is not None: 

208 return f.errors 

209 return False 

210 

211 def render_template(self, **kwargs): 

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

213 

214 def fieldset(self, fieldset_name=None): 

215 return self._formulaic.fieldset(fieldset_name) 

216 

217 def fieldsets(self): 

218 return self._formulaic.fieldsets() 

219 

220 def check_field_group_exists(self, field_group_name): 

221 return self.renderer.check_field_group_exists(field_group_name) 

222 

223 @property 

224 def ui_settings(self): 

225 return self._formulaic.ui_settings 

226 

227 

228class Renderer(object): 

229 """ 

230 ~~FormContextRenderer:FormContext->FormHelper:FormContext~~ 

231 """ 

232 def __init__(self): 

233 self.FIELD_GROUPS = {} 

234 self.fh = FormHelperBS3() 

235 self._error_fields = [] 

236 self._disabled_fields = [] 

237 self._disable_all_fields = False 

238 self._highlight_completable_fields = False 

239 

240 def check_field_group_exists(self, field_group_name): 

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

242 group_def = self.FIELD_GROUPS.get(field_group_name) 

243 if group_def is None: 

244 return False 

245 else: 

246 return True 

247 

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

249 if field_group_name is None: 

250 return self._render_all(form_context) 

251 

252 # get the group definition 

253 group_def = self.FIELD_GROUPS.get(field_group_name) 

254 if group_def is None: 

255 return "" 

256 

257 # build the frag 

258 frag = "" 

259 for entry in group_def: 

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

261 config = entry.get(field_name) 

262 config = deepcopy(config) 

263 

264 config = self._rewrite_extra_fields(form_context, config) 

265 field = form_context.form[field_name] 

266 

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

268 config["disabled"] = "disabled" 

269 

270 if self._highlight_completable_fields is True: 

271 valid = field.validate(form_context.form) 

272 config["complete_me"] = not valid 

273 

274 if group_cfg is not None: 

275 config.update(group_cfg) 

276 

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

278 

279 return frag 

280 

281 @property 

282 def error_fields(self): 

283 return self._error_fields 

284 

285 def set_error_fields(self, fields): 

286 self._error_fields = fields 

287 

288 @property 

289 def disabled_fields(self): 

290 return self._disabled_fields 

291 

292 def set_disabled_fields(self, fields): 

293 self._disabled_fields = fields 

294 

295 def disable_all_fields(self, disable): 

296 self._disable_all_fields = disable 

297 

298 def _rewrite_extra_fields(self, form_context, config): 

299 if "extra_input_fields" in config: 

300 config = deepcopy(config) 

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

302 extra_field = form_context.form[field_ref] 

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

304 return config 

305 

306 def _render_all(self, form_context): 

307 frag = "" 

308 for field in form_context.form: 

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

310 return frag 

311 

312 def find_field(self, field, field_group): 

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

314 if field in item: 

315 return index 

316 

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

318 self.FIELD_GROUPS[field_group].insert( 

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

320 field_to_insert 

321 ) 

322 

323 

324class FormHelperBS3(object): 

325 """ 

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

327 ~~->WTForms:Library~~ 

328 """ 

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

330 # begin the frag 

331 frag = "" 

332 

333 # deal with the first error if it is relevant 

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

335 if first_error: 

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

337 

338 # call the correct render function on the field type 

339 if field.type == "FormField": 

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

341 elif field.type == "FieldList": 

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

343 else: 

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

345 

346 return frag 

347 

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

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

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

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

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

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

354 

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

356 if field.errors: 

357 frag += " error" 

358 if render_subfields_horizontal: 

359 frag += " row" 

360 if container_class is not None: 

361 frag += " " + container_class 

362 if complete_me: 

363 frag += " complete-me" 

364 frag += '" id="' 

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

366 if hidden: 

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

368 frag += ">" 

369 if contents is not None: 

370 frag += contents 

371 frag += "</div>" 

372 

373 return frag 

374 

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

376 # get the useful kwargs 

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

378 

379 frag = "" 

380 # for each subfield, do the render 

381 for subfield in field: 

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

383 subfield_width = "3" 

384 remove = [] 

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

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

387 subfield_width = val 

388 remove.append(kwarg) 

389 for rm in remove: 

390 del kwargs[rm] 

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

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

393 frag += "</div>" 

394 else: 

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

396 

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

398 

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

400 # for each subfield, do the render 

401 frag = "" 

402 for subfield in field: 

403 if subfield.type == "FormField": 

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

405 else: 

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

407 return frag 

408 

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

410 # interesting arguments from keywords 

411 extra_input_fields = kwargs.get("extra_input_fields") 

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

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

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

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

416 field_width = 12 - label_width 

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

418 if label_width > 0: 

419 label_width = str(label_width) 

420 

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

422 return "" 

423 

424 frag = "" 

425 

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

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

428 if q_num is not None: 

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

430 if label_width != 0: 

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

432 if q_num is not None: 

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

434 frag += field.label.text 

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

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

437 frag += "</label>" 

438 

439 # determine if this is a checkbox 

440 is_checkbox = False 

441 if (field.type == "SelectMultipleField" 

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

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

444 is_checkbox = True 

445 

446 extra_class = "" 

447 if is_checkbox: 

448 extra_class += " checkboxes" 

449 

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

451 if field.type == "RadioField": 

452 for subfield in field: 

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

454 elif is_checkbox: 

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

456 for subfield in field: 

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

458 frag += "</ul>" 

459 else: 

460 if maximise_width: 

461 clazz += " col-xs-12" 

462 kwargs["class"] = clazz 

463 render_args = {} 

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

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

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

467 render_args[k] = v 

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

469 

470 if field.errors: 

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

472 for error in field.errors: 

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

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

475 

476 if field.description: 

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

478 

479 frag += "</div>" 

480 return frag 

481 

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

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

484 label_width = "12" 

485 

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

487 frag += field(**kwargs) 

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

489 

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

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

492 

493 frag += "</label>" 

494 return frag 

495 

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

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

498 

499 frag = "<li>" 

500 frag += field(**kwargs) 

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

502 

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

504 eif = extra_input_fields[field.label.text] 

505 if not isinstance(eif, UnboundField): 

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

507 

508 frag += "</li>" 

509 return frag 

510 

511 

512######################################### 

513# Form definition 

514# ~~Article:Form~~ 

515 

516ISSN_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).' 

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

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

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

520ORCID_ERROR = "Invalid ORCID iD. Please enter your ORCID iD structured as: https://orcid.org/0000-0000-0000-0000. URLs must start with https." 

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

522 

523start_year = app.config.get("METADATA_START_YEAR", dates.now().year - 15) 

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

525MONTH_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")] 

526INITIAL_AUTHOR_FIELDS = 3 

527 

528 

529def choices_for_article_issns(user, article_id=None, 

530 issn_type: Literal['eissn', 'pissn', 'all'] = 'all'): 

531 

532 owner = None 

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

534 # ~~->Article:Model~~ 

535 a = models.Article.pull(article_id) 

536 if a: 

537 owner = a.get_owner() 

538 

539 if not owner: 

540 owner = user.id 

541 

542 if issn_type == 'eissn': 

543 issn_field = 'bibjson.eissn.exact' 

544 elif issn_type == 'pissn': 

545 issn_field = 'bibjson.pissn.exact' 

546 else: 

547 issn_field = 'index.issn.exact' 

548 

549 # ~~->Journal:Model~~ 

550 issns = models.Journal.issns_by_owner(owner, in_doaj=True, issn_field=issn_field) 

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

552 return ic 

553 

554 

555class AuthorForm(Form): 

556 """ 

557 ~~->$ Author:Form~~ 

558 """ 

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

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

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

562 

563 

564class ArticleForm(Form): 

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

566 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)") 

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

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

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

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

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

572 publication_month = DOAJSelectField("Month", [validators.Optional()], choices=MONTH_CHOICES, default="" ) 

573 pissn = DOAJSelectField("Print", [ 

574 ThisOrThat("eissn", "Either this field or Online ISSN is required"), 

575 DifferentTo("eissn", message=IDENTICAL_ISSNS_ERROR) 

576 ], choices=[]) # choices set at construction 

577 eissn = DOAJSelectField("Online", [ 

578 ThisOrThat("pissn", "Either this field or Print ISSN is required"), 

579 DifferentTo("pissn", message=IDENTICAL_ISSNS_ERROR) 

580 ], choices=[]) # choices set at construction 

581 

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

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

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

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

586 

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

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

589 self.set_choices() 

590 

591 def set_choices(self, user=None, article_id=None): 

592 user = user or current_user 

593 try: 

594 self.pissn.choices = choices_for_article_issns(user, issn_type='pissn', article_id=article_id) 

595 self.eissn.choices = choices_for_article_issns(user, issn_type='eissn', article_id=article_id) 

596 except Exception as e: 

597 # not logged in, and current_user is broken 

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

599 app.logger.exception(str(e)) 

600 

601 

602 

603######################################### 

604# Formcontexts and factory 

605 

606class ArticleFormFactory(object): 

607 """ 

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

609 ~~->PublisherArticleMetadata:FormContext~~ 

610 """ 

611 @classmethod 

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

613 if role == "admin": 

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

615 if role == "publisher": 

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

617 

618 

619class MetadataForm(FormContext): 

620 """ 

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

622 ~~->ArticleForm:Crosswalk~~ 

623 ~~->Article:Service~~ 

624 """ 

625 

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

627 self.user = user 

628 self.author_error = False 

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

630 

631 def _set_choices(self): 

632 if self.source is not None: 

633 self.form.set_choices(user=self.user, article_id=self.source.id) 

634 

635 def modify_authors_if_required(self, request_data): 

636 

637 more_authors = request_data.get("more_authors") 

638 remove_author = None 

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

640 if v.startswith("remove_authors"): 

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

642 

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

644 if more_authors: 

645 return self.render_template(more_authors=True) 

646 

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

648 if remove_author is not None: 

649 return self.render_template(remove_authors=remove_author) 

650 

651 def _check_for_author_errors(self, **kwargs): 

652 

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

654 self.form.authors.append_entry() 

655 if "remove_authors" in kwargs: 

656 keep = [] 

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

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

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

660 break 

661 else: 

662 keep.append(entry) 

663 while len(keep) > 0: 

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

665 

666 def _validate_authors(self): 

667 counted = 0 

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

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

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

671 counted += 1 

672 return counted >= 1 

673 

674 def blank_form(self): 

675 self.form = ArticleForm() 

676 self._set_choices() 

677 

678 def source2form(self): 

679 self.form = ArticleForm() 

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

681 self._set_choices() 

682 

683 def data2form(self): 

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

685 self._set_choices() 

686 

687 def form2target(self): 

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

689 

690 def validate(self): 

691 if not self._validate_authors(): 

692 self.author_error = True 

693 if not self.form.validate(): 

694 return False 

695 return True 

696 

697 def finalise(self, duplicate_check = True): 

698 self.form2target() 

699 if not self.author_error: 

700 article_service = DOAJ.articleService() 

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

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

703 duplicate_check = duplicate_check) 

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

705 msg, how = Messages.ARTICLE_METADATA_SUBMITTED_FLASH 

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

707 else: 

708 return 

709 

710 

711class PublisherMetadataForm(MetadataForm): 

712 """ 

713 ~~PublisherArticleMetadata:FormContext->ArticleMetadata:FormContext~~ 

714 """ 

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

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

717 

718 def set_template(self): 

719 self.template = templates.PUBLISHER_ARTICLE_METADATA 

720 

721 def render_template(self, **kwargs): 

722 self._check_for_author_errors(**kwargs) 

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

724 self.blank_form() 

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

726 

727 

728class AdminMetadataArticleForm(MetadataForm): 

729 """ 

730 ~~AdminArticleMetadata:FormContext->ArticleMetadata:FormContext~~ 

731 """ 

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

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

734 

735 def set_template(self): 

736 self.template = templates.ADMIN_ARTICLE_FORM 

737 

738 def render_template(self, **kwargs): 

739 self._check_for_author_errors(**kwargs) 

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