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

254 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-09-13 22:06 +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 

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

8from portality.decorators import ssl_required, restrict_to_role 

9from portality.dao import ESMappingMissingError 

10from portality.forms.application_forms import ApplicationFormFactory 

11from portality.tasks.ingestarticles import IngestArticlesBackgroundTask, BackgroundException 

12from portality.tasks.preservation import * 

13from portality.ui.messages import Messages 

14from portality import lock 

15from portality.models import DraftApplication 

16from portality.lcc import lcc_jstree 

17from portality.forms.article_forms import ArticleFormFactory 

18 

19from huey.exceptions import TaskException 

20 

21import uuid 

22 

23blueprint = Blueprint('publisher', __name__) 

24 

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

26@blueprint.before_request 

27def restrict(): 

28 return restrict_to_role('publisher') 

29 

30@blueprint.route("/") 

31@login_required 

32@ssl_required 

33def index(): 

34 return render_template("publisher/index.html") 

35 

36 

37@blueprint.route("/journal") 

38@login_required 

39@ssl_required 

40def journals(): 

41 return render_template("publisher/journals.html", lcc_tree=lcc_jstree) 

42 

43 

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

45def delete_application(application_id): 

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

47 draft_application = DraftApplication.pull(application_id) 

48 if draft_application is not None: 

49 draft_application.delete() 

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

51 

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

53 appService = DOAJ.applicationService() 

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

55 

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

57 

58 

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

60def deleted_thanks(): 

61 return render_template("publisher/application_deleted.html") 

62 

63 

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

65@login_required 

66@ssl_required 

67@write_required() 

68def update_request(journal_id): 

69 # DOAJ BLL for this request 

70 journalService = DOAJ.journalService() 

71 applicationService = DOAJ.applicationService() 

72 

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

74 if request.method == "DELETE": 

75 journal, _ = journalService.journal(journal_id) 

76 application_id = journal.current_application 

77 if application_id is not None: 

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

79 else: 

80 abort(404) 

81 return "" 

82 

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

84 application = None 

85 jlock = None 

86 alock = None 

87 try: 

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

89 except AuthoriseException as e: 

90 if e.reason == AuthoriseException.WRONG_STATUS: 

91 journal, _ = journalService.journal(journal_id) 

92 return render_template("publisher/application_already_submitted.html", journal=journal) 

93 else: 

94 abort(404) 

95 except lock.Locked as e: 

96 journal, _ = journalService.journal(journal_id) 

97 return render_template("publisher/locked.html", journal=journal, lock=e.lock) 

98 

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

100 if application is None: 

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

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

103 abort(404) 

104 

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

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

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

108 if cancelled is not None: 

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

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

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

112 

113 fc = ApplicationFormFactory.context("update_request") 

114 

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

116 if request.method == "GET": 

117 fc.processor(source=application) 

118 return fc.render_template(obj=application) 

119 

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

121 elif request.method == "POST": 

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

123 if processor.validate(): 

124 try: 

125 processor.finalise() 

126 Messages.flash(Messages.PUBLISHER_APPLICATION_UPDATE_SUBMITTED_FLASH) 

127 for a in processor.alert: 

128 Messages.flash_with_url(a, "success") 

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

130 except Exception as e: 

131 Messages.flash(str(e)) 

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

133 finally: 

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

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

136 else: 

137 return fc.render_template(obj=application) 

138 

139 

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

141@login_required 

142@ssl_required 

143@write_required() 

144def application_readonly(application_id): 

145 # DOAJ BLL for this request 

146 applicationService = DOAJ.applicationService() 

147 authService = DOAJ.authorisationService() 

148 

149 application, _ = applicationService.application(application_id) 

150 try: 

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

152 except AuthoriseException as e: 

153 abort(404) 

154 

155 fc = ApplicationFormFactory.context("application_read_only") 

156 fc.processor(source=application) 

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

158 

159 return fc.render_template(obj=application) 

160 

161 

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

163@login_required 

164@ssl_required 

165@write_required() 

166def update_request_readonly(application_id): 

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

168 

169 

170@blueprint.route('/progress') 

171@login_required 

172@ssl_required 

173def updates_in_progress(): 

174 return render_template("publisher/updates_in_progress.html") 

175 

176 

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

178@login_required 

179@ssl_required 

180@write_required() 

181def upload_file(): 

182 """ 

183 ~~UploadMetadata: Feature->UploadMetadata:Page~~ 

184 ~~->Crossref442:Feature~~ 

185 ~~->Crossref531:Feature~~ 

186 """ 

187 

188 # all responses involve getting the previous uploads 

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

190 

191 if request.method == "GET": 

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

193 if schema is None: 

194 schema = "" 

195 return render_template('publisher/uploadmetadata.html', previous=previous, schema=schema) 

196 

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

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

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

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

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

202 resp.set_cookie("schema", schema) 

203 

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

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

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

207 

208 try: 

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

210 IngestArticlesBackgroundTask.submit(job) 

211 except (BackgroundException, TaskException) as e: 

212 magic = str(uuid.uuid1()) 

213 flash("An error has occurred and your upload may not have succeeded. If the problem persists please report the issue with the ID " + magic) 

214 app.logger.exception('File upload error. ' + magic) 

215 return resp 

216 

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

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

219 return resp 

220 

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

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

223 return resp 

224 

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

226 return resp 

227 

228 

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

230@login_required 

231@ssl_required 

232@write_required() 

233def preservation(): 

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

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

236 """ 

237 

238 previous = [] 

239 try: 

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

241 # handle exception if there are no records available 

242 except ESMappingMissingError: 

243 pass 

244 

245 if request.method == "GET": 

246 return render_template('publisher/preservation.html', previous=previous) 

247 

248 if request.method == "POST": 

249 

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

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

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

253 

254 # create model object to store status details 

255 preservation_model = models.PreservationState() 

256 preservation_model.set_id() 

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

258 

259 previous.insert(0, preservation_model) 

260 

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

262 

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

264 error_str = "No file provided to upload" 

265 flash(error_str, "error") 

266 preservation_model.failed(error_str) 

267 preservation_model.save() 

268 return resp 

269 

270 preservation_model.validated() 

271 preservation_model.save() 

272 

273 # check if collection has been assigned for the user 

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

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

276 collection_available = True 

277 collection_dict = app.config.get("PRESERVATION_COLLECTION") 

278 if collection_dict and not current_user.id in collection_dict: 

279 collection_available = False 

280 elif collection_dict: 

281 params = collection_dict[current_user.id] 

282 if not len(params) == 2: 

283 collection_available = False 

284 

285 if not collection_available: 

286 flash( 

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

288 "error") 

289 preservation_model.failed(FailedReasons.collection_not_available) 

290 preservation_model.save() 

291 

292 else: 

293 try: 

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

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

296 PreservationBackgroundTask.submit(job) 

297 

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

299 

300 except EmailException: 

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

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

303 try: 

304 uid = str(uuid.uuid1()) 

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

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

307 preservation_model.save() 

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

309 if job: 

310 background_task = PreservationBackgroundTask(job) 

311 background_task.cleanup() 

312 except Exception as e: 

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

314 return resp 

315 

316 

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

318@login_required 

319@ssl_required 

320@write_required() 

321def metadata(): 

322 user = current_user._get_current_object() 

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

324 if request.method == "GET": 

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

326 return fc.render_template() 

327 

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

329 # a bunch of work 

330 elif request.method == "POST": 

331 

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

333 form_data=request.form) 

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

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

336 

337 fc.modify_authors_if_required(request.values) 

338 

339 validated = False 

340 if fc.validate(): 

341 try: 

342 fc.finalise() 

343 validated = True 

344 except ArticleMergeConflict: 

345 Messages.flash(Messages.ARTICLE_METADATA_MERGE_CONFLICT) 

346 except DuplicateArticleException: 

347 Messages.flash(Messages.ARTICLE_METADATA_UPDATE_CONFLICT) 

348 

349 return fc.render_template(validated=validated) 

350 

351 

352@blueprint.route("/help") 

353@login_required 

354@ssl_required 

355def help(): 

356 return render_template("publisher/help.html") 

357 

358 

359def _validate_authors(form, require=1): 

360 counted = 0 

361 for entry in form.authors.entries: 

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

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

364 counted += 1 

365 return counted >= require