Coverage for portality/view/publisher.py: 29%
254 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-09-05 21:15 +0100
« prev ^ index » next coverage.py v6.4.2, created at 2022-09-05 21:15 +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
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
19from huey.exceptions import TaskException
21import uuid
23blueprint = Blueprint('publisher', __name__)
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')
30@blueprint.route("/")
31@login_required
32@ssl_required
33def index():
34 return render_template("publisher/index.html")
37@blueprint.route("/journal")
38@login_required
39@ssl_required
40def journals():
41 return render_template("publisher/journals.html", lcc_tree=lcc_jstree)
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"))
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())
56 return redirect(url_for("publisher.deleted_thanks"))
59@blueprint.route("/application/deleted")
60def deleted_thanks():
61 return render_template("publisher/application_deleted.html")
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()
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 ""
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)
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)
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"))
113 fc = ApplicationFormFactory.context("update_request")
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)
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)
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()
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)
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)
159 return fc.render_template(obj=application)
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))
170@blueprint.route('/progress')
171@login_required
172@ssl_required
173def updates_in_progress():
174 return render_template("publisher/updates_in_progress.html")
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 """
188 # all responses involve getting the previous uploads
189 previous = models.FileUpload.by_owner(current_user.id)
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)
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)
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")
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
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
221 if url is not None and url != "":
222 flash("File reference successfully received - it will be processed shortly", "success")
223 return resp
225 flash("No file or URL provided", "error")
226 return resp
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 """
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
245 if request.method == "GET":
246 return render_template('publisher/preservation.html', previous=previous)
248 if request.method == "POST":
250 f = request.files.get("file")
251 app.logger.info(f"Preservation file {f.filename}")
252 resp = make_response(redirect(url_for("publisher.preservation")))
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)
259 previous.insert(0, preservation_model)
261 app.logger.debug(f"Preservation model created with id {preservation_model.id}")
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
270 preservation_model.validated()
271 preservation_model.save()
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
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()
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)
298 flash("File uploaded and waiting to be processed.", "success")
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
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()
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":
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
337 fc.modify_authors_if_required(request.values)
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)
349 return fc.render_template(validated=validated)
352@blueprint.route("/help")
353@login_required
354@ssl_required
355def help():
356 return render_template("publisher/help.html")
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