Coverage for portality / view / publisher.py: 37%

305 statements  

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

1from flask import Blueprint, request, make_response 

2from flask import render_template, abort, redirect, url_for, flash 

3from flask_login import current_user, login_required 

4 

5from portality.app_email import EmailException 

6from portality import models, constants 

7from portality.bll.exceptions import AuthoriseException, ArticleMergeConflict, DuplicateArticleException, \ 

8 ArticleNotAcceptable, NoSuchObjectException 

9from portality.decorators import ssl_required, restrict_to_role, write_required 

10from portality.dao import ESMappingMissingError 

11from portality.forms.application_forms import ApplicationFormFactory 

12from portality.tasks.ingestarticles import IngestArticlesBackgroundTask, BackgroundException 

13from portality.tasks.preservation import * 

14from portality.ui.messages import Messages 

15from portality.ui import templates 

16from portality import lock 

17from portality.models import DraftApplication 

18from portality.lcc import lcc_jstree 

19from portality.forms.article_forms import ArticleFormFactory 

20from portality.store import StoreFactory 

21 

22from huey.exceptions import TaskException 

23 

24import uuid 

25 

26from portality.view.view_helper import exparam_editing_user 

27 

28blueprint = Blueprint('publisher', __name__) 

29 

30# restrict everything in admin to logged in users with the "publisher" role 

31@blueprint.before_request 

32def restrict(): 

33 return restrict_to_role('publisher') 

34 

35@blueprint.route("/") 

36@login_required 

37@ssl_required 

38def index(): 

39 return render_template(templates.PUBLISHER_DRAFTS) 

40 

41 

42@blueprint.route("/journal") 

43@login_required 

44@ssl_required 

45def journals(): 

46 return render_template(templates.PUBLISHER_JOURNAL_SEARCH, lcc_tree=lcc_jstree) 

47 

48 

49@blueprint.route("/application/<application_id>/delete", methods=["GET"]) 

50@write_required() 

51def delete_application(application_id): 

52 # FIXME: this should be a POST or DELETE request 

53 # if this is a draft application, we can just remove it 

54 draft_application = DraftApplication.pull(application_id) 

55 if draft_application is not None: 

56 draft_application.delete() 

57 return redirect(url_for("publisher.deleted_thanks")) 

58 

59 # otherwise delegate to the application service to sort this out 

60 appService = DOAJ.applicationService() 

61 try: 

62 appService.delete_application(application_id, current_user._get_current_object()) 

63 except NoSuchObjectException: 

64 abort(404) 

65 

66 return redirect(url_for("publisher.deleted_thanks")) 

67 

68 

69@blueprint.route("/application/deleted") 

70def deleted_thanks(): 

71 return render_template(templates.PUBLISHER_APPLICATION_DELETED) 

72 

73 

74@blueprint.route("/update_request/<journal_id>", methods=["GET", "POST", "DELETE"]) 

75@login_required 

76@ssl_required 

77@write_required() 

78def update_request(journal_id): 

79 # DOAJ BLL for this request 

80 journalService = DOAJ.journalService() 

81 applicationService = DOAJ.applicationService() 

82 

83 # if this is a delete request, deal with it first and separately from the below logic 

84 if request.method == "DELETE": 

85 journal, _ = journalService.journal(journal_id) 

86 application_id = journal.current_application 

87 if application_id is not None: 

88 applicationService.delete_application(application_id, current_user._get_current_object()) 

89 else: 

90 abort(404) 

91 return "" 

92 

93 # load the application either directly or by crosswalking the journal object 

94 application = None 

95 jlock = None 

96 alock = None 

97 try: 

98 application, jlock, alock = applicationService.update_request_for_journal(journal_id, account=current_user._get_current_object()) 

99 except AuthoriseException as e: 

100 if e.reason == AuthoriseException.WRONG_STATUS: 

101 journal, _ = journalService.journal(journal_id) 

102 return render_template(templates.PUBLISHER_APPLICATION_ALREADY_SUBMITTED, journal=journal) 

103 else: 

104 abort(404) 

105 except lock.Locked as e: 

106 journal, _ = journalService.journal(journal_id) 

107 return render_template(templates.PUBLISHER_LOCKED, journal=journal, lock=e.lock) 

108 

109 # if we didn't find an application or journal, 404 the user 

110 if application is None: 

111 if jlock is not None: jlock.delete() 

112 if alock is not None: alock.delete() 

113 abort(404) 

114 

115 # if we have a live application and cancel was hit, then cancel the operation and redirect 

116 # first determine if this is a cancel request on the form 

117 cancelled = request.values.get("cancel") 

118 if cancelled is not None: 

119 if jlock is not None: jlock.delete() 

120 if alock is not None: alock.delete() 

121 return redirect(url_for("publisher.updates_in_progress")) 

122 

123 fc = ApplicationFormFactory.context("update_request", extra_param=exparam_editing_user()) 

124 

125 # if we are requesting the page with a GET, we just want to show the form 

126 if request.method == "GET": 

127 fc.processor(source=application) 

128 return fc.render_template(obj=application) 

129 

130 # if we are requesting the page with a POST, we need to accept the data and handle it 

131 elif request.method == "POST": 

132 processor = fc.processor(formdata=request.form, source=application) 

133 if processor.validate(): 

134 try: 

135 processor.finalise() 

136 Messages.flash(Messages.PUBLISHER_APPLICATION_UPDATE_SUBMITTED_FLASH) 

137 for a in processor.alert: 

138 Messages.flash_with_url(a, "success") 

139 return redirect(url_for("publisher.updates_in_progress")) 

140 except Exception as e: 

141 Messages.flash(str(e)) 

142 return redirect(url_for("publisher.update_request", journal_id=journal_id, _anchor='cannot_edit')) 

143 finally: 

144 if jlock is not None: jlock.delete() 

145 if alock is not None: alock.delete() 

146 else: 

147 return fc.render_template(obj=application) 

148 

149 

150@blueprint.route("/view_application/<application_id>", methods=["GET"]) 

151@login_required 

152@ssl_required 

153def application_readonly(application_id): 

154 # DOAJ BLL for this request 

155 applicationService = DOAJ.applicationService() 

156 authService = DOAJ.authorisationService() 

157 

158 application, _ = applicationService.application(application_id) 

159 try: 

160 authService.can_view_application(current_user._get_current_object(), application) 

161 except AuthoriseException as e: 

162 abort(404) 

163 

164 fc = ApplicationFormFactory.context("application_read_only") 

165 fc.processor(source=application) 

166 # fc = formcontext.ApplicationFormFactory.get_form_context(role="update_request_readonly", source=application) 

167 

168 return fc.render_template(obj=application) 

169 

170 

171@blueprint.route("/view_update_request/<application_id>", methods=["GET", "POST"]) 

172@login_required 

173@ssl_required 

174def update_request_readonly(application_id): 

175 return redirect(url_for("publisher.application_readonly", application_id=application_id)) 

176 

177 

178@blueprint.route('/progress') 

179@login_required 

180@ssl_required 

181def updates_in_progress(): 

182 return render_template(templates.PUBLISHER_UPDATE_REQUESTS) 

183 

184 

185@blueprint.route("/uploadfile", methods=["GET", "POST"]) 

186@login_required 

187@ssl_required 

188@write_required() 

189def upload_file(): 

190 """ 

191 ~~UploadMetadata:Feature->UploadMetadata:Page~~ 

192 ~~->Crossref442:Feature~~ 

193 ~~->Crossref531:Feature~~ 

194 """ 

195 

196 # all responses involve getting the previous uploads 

197 previous = models.FileUpload.by_owner(current_user.id) 

198 

199 if request.method == "GET": 

200 schema = request.cookies.get("schema") 

201 if schema is None: 

202 schema = "" 

203 return render_template(templates.PUBLISHER_XML_UPLOAD, previous=previous, schema=schema, error=False) 

204 

205 # otherwise we are dealing with a POST - file upload or supply of url 

206 f = request.files.get("file") 

207 schema = request.values.get("schema") 

208 if not schema: 

209 abort(400) 

210 url = request.values.get("upload-xml-link") 

211 resp = make_response(redirect(url_for("publisher.upload_file"))) 

212 resp.set_cookie("schema", schema) 

213 

214 # file upload takes precedence over URL, in case the user has given us both 

215 if f is not None and f.filename != "" and url is not None and url != "": 

216 flash("You provided a file and a URL - the URL has been ignored") 

217 

218 try: 

219 job = IngestArticlesBackgroundTask.prepare(current_user.id, upload_file=f, schema=schema, url=url, previous=previous) 

220 IngestArticlesBackgroundTask.submit(job) 

221 except TaskException as e: 

222 flash(Messages.PUBLISHER_UPLOAD_ERROR.format(error_str=str(e))) 

223 app.logger.exception('File upload error. ' + str(e)) 

224 return resp 

225 except BackgroundException as e: 

226 if str(e) == Messages.NO_FILE_UPLOAD_ID: 

227 schema = request.cookies.get("schema") 

228 if schema is None: 

229 schema = "" 

230 return render_template(templates.PUBLISHER_XML_UPLOAD, previous=previous, schema=schema, error=True) 

231 

232 flash(Messages.PUBLISHER_UPLOAD_ERROR.format(error_str=str(e))) 

233 app.logger.exception('File upload error. ' + str(e)) 

234 return resp 

235 

236 if f is not None and f.filename != "": 

237 flash("File uploaded and waiting to be processed. Check back here for updates.", "success") 

238 return resp 

239 

240 if url is not None and url != "": 

241 flash("File successfully received - it will be processed shortly", "success") 

242 return resp 

243 

244 flash("No file or URL provided", "error") 

245 return resp 

246 

247 

248@blueprint.route("/preservation", methods=["GET", "POST"]) 

249@login_required 

250@ssl_required 

251@write_required() 

252def preservation(): 

253 """Upload articles on Internet Servers for archiving. 

254 This feature is available for the users who has 'preservation' role. 

255 """ 

256 

257 if app.config.get('PRESERVATION_PAGE_UNDER_MAINTENANCE', False): 

258 return render_template(templates.PUBLISHER_PRESERVATION_READONLY) 

259 

260 previous = [] 

261 try: 

262 previous = models.PreservationState.by_owner(current_user.id) 

263 # handle exception if there are no records available 

264 except ESMappingMissingError: 

265 pass 

266 

267 if request.method == "GET": 

268 return render_template(templates.PUBLISHER_PRESERVATION, previous=previous) 

269 

270 if request.method == "POST": 

271 

272 f = request.files.get("file") 

273 

274 resp = make_response(redirect(url_for("publisher.preservation"))) 

275 

276 # create model object to store status details 

277 preservation_model = models.PreservationState() 

278 preservation_model.set_id() 

279 

280 

281 previous.insert(0, preservation_model) 

282 

283 app.logger.debug(f"Preservation model created with id {preservation_model.id}") 

284 

285 if f is None or f.filename == "": 

286 error_str = Messages.PRESERVATION_NO_FILE 

287 flash(error_str, "error") 

288 preservation_model.initiated(current_user.id, "none") 

289 preservation_model.failed(error_str) 

290 preservation_model.save() 

291 return resp 

292 

293 app.logger.info(f"Preservation file {f.filename}") 

294 

295 preservation_model.initiated(current_user.id, f.filename) 

296 preservation_model.validated() 

297 preservation_model.save() 

298 

299 # check if collection has been assigned for the user 

300 # collection must be in the format {"user_id1",["collection_name1","collection_id1"], 

301 # "user_id2",["collection_name2","collection_id2"]} 

302 collection_dict = app.config.get("PRESERVATION_COLLECTION", {}) 

303 collection_available = True if collection_dict else False 

304 if collection_dict and not current_user.id in collection_dict: 

305 collection_available = False 

306 elif collection_dict: 

307 params = collection_dict[current_user.id] 

308 if not len(params) == 2: 

309 collection_available = False 

310 

311 if not collection_available: 

312 flash( 

313 "Cannot process upload - you do not have Collection details associated with your user ID. Please contact the DOAJ team.", 

314 "error") 

315 preservation_model.failed(FailedReasons.collection_not_available) 

316 preservation_model.save() 

317 

318 else: 

319 try: 

320 job = PreservationBackgroundTask.prepare(current_user.id, upload_file=f) 

321 PreservationBackgroundTask.set_param(job.params, "model_id", preservation_model.id) 

322 PreservationBackgroundTask.submit(job) 

323 

324 flash("File uploaded and waiting to be processed.", "success") 

325 

326 except EmailException: 

327 app.logger.exception('Error sending email' ) 

328 except (PreservationStorageException, PreservationException, Exception) as exp: 

329 try: 

330 uid = str(uuid.uuid1()) 

331 flash("An error has occurred and your preservation upload may not have succeeded. Please report the issue with the ID " + uid) 

332 preservation_model.failed(str(exp) + " Issue id : " + uid) 

333 preservation_model.save() 

334 app.logger.exception('Preservation upload error. ' + uid) 

335 if job: 

336 background_task = PreservationBackgroundTask(job) 

337 background_task.cleanup() 

338 except Exception as e: 

339 app.logger.exception('Unknown error.' + str(e)) 

340 return resp 

341 

342 

343@blueprint.route("/metadata", methods=["GET", "POST"]) 

344@login_required 

345@ssl_required 

346@write_required() 

347def metadata(): 

348 user = current_user._get_current_object() 

349 # if this is a get request, give the blank form - there is no edit feature 

350 if request.method == "GET": 

351 fc = ArticleFormFactory.get_from_context(user=user, role="publisher") 

352 return fc.render_template() 

353 

354 # if this is a post request, a form button has been hit and we need to do 

355 # a bunch of work 

356 elif request.method == "POST": 

357 

358 fc = ArticleFormFactory.get_from_context(role="publisher", user=user, 

359 form_data=request.form) 

360 # first we need to do any server-side form modifications which 

361 # the user might request by pressing the add/remove authors buttons 

362 

363 fc.modify_authors_if_required(request.values) 

364 

365 validated = False 

366 if fc.validate(): 

367 try: 

368 fc.finalise() 

369 validated = True 

370 except ArticleMergeConflict: 

371 Messages.flash(Messages.ARTICLE_METADATA_MERGE_CONFLICT) 

372 except DuplicateArticleException: 

373 Messages.flash(Messages.ARTICLE_METADATA_UPDATE_CONFLICT) 

374 except ArticleNotAcceptable as e: 

375 Messages.flash_with_param(e.message, "error") 

376 return fc.render_template(validated=validated) 

377 

378@blueprint.route("/journal-csv", methods=["GET"]) 

379@login_required 

380@ssl_required 

381@write_required() 

382def journal_csv(): 

383 if current_user.has_role(constants.ROLE_PUBLISHER_JOURNAL_CSV): 

384 return render_template(templates.PUBLISHER_CSV_UPLOAD) 

385 abort(403) 

386 

387@blueprint.route("/journal-csv/validate", methods=["POST"]) 

388@login_required 

389@ssl_required 

390@write_required() 

391def journal_csv_validate(): 

392 if not current_user.has_role(constants.ROLE_PUBLISHER_JOURNAL_CSV): 

393 abort(403) 

394 if "journal_csv" not in request.files: 

395 abort(400) 

396 file = request.files["journal_csv"] 

397 if not file.filename.endswith(".csv"): 

398 abort(400) 

399 

400 tmpStore = StoreFactory.tmp() 

401 container_id = app.config.get("JOURNAL_CSV_UPLOAD__TMP_CONTAINER", "publisher_csvs_validation") 

402 filename = current_user.id + "_" + dates.now_str() + ".csv" 

403 out = tmpStore.path(container_id, filename, create_container=True, must_exist=False) 

404 file.save(out) 

405 

406 try: 

407 report = DOAJ.applicationService().validate_update_csv(out, current_user) 

408 finally: 

409 tmpStore.delete_file(container_id, filename) 

410 

411 resp = make_response(report.json()) 

412 resp.mimetype = "application/json" 

413 return resp 

414 

415 

416@blueprint.route("/help") 

417@login_required 

418@ssl_required 

419def help(): 

420 return render_template(templates.PUBLISHER_XML_HELP) 

421 

422 

423def _validate_authors(form, require=1): 

424 counted = 0 

425 for entry in form.authors.entries: 

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

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

428 counted += 1 

429 return counted >= require