Coverage for portality/view/admin.py: 34%
528 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-11-09 16:22 +0000
« prev ^ index » next coverage.py v6.4.2, created at 2022-11-09 16:22 +0000
1import json
3from flask import Blueprint, request, flash, abort, make_response
4from flask import render_template, redirect, url_for
5from flask_login import current_user, login_required
6from werkzeug.datastructures import MultiDict
8from portality import dao
9import portality.models as models
10from portality import constants
11from portality import lock
12from portality.background import BackgroundSummary
13from portality.bll import DOAJ, exceptions
14from portality.bll.exceptions import ArticleMergeConflict, DuplicateArticleException
15from portality.core import app
16from portality.crosswalks.application_form import ApplicationFormXWalk
17from portality.decorators import ssl_required, restrict_to_role, write_required
18from portality.forms.application_forms import ApplicationFormFactory, application_statuses
19from portality.forms.application_forms import JournalFormFactory
20from portality.forms.article_forms import ArticleFormFactory
21from portality.lcc import lcc_jstree
22from portality.lib.query_filters import remove_search_limits, update_request, not_update_request
23from portality.tasks import journal_in_out_doaj, journal_bulk_edit, suggestion_bulk_edit, journal_bulk_delete, \
24 article_bulk_delete
25from portality.ui.messages import Messages
26from portality.util import flash_with_url, jsonp, make_json_resp, get_web_json_payload, validate_json
27from portality.view.forms import EditorGroupForm, MakeContinuation
29from portality.bll.services.query import Query
31# ~~Admin:Blueprint~~
32blueprint = Blueprint('admin', __name__)
35# restrict everything in admin to logged in users with the "admin" role
36@blueprint.before_request
37def restrict():
38 return restrict_to_role('admin')
41# build an admin page where things can be done
42@blueprint.route('/')
43@login_required
44@ssl_required
45def index():
46 return render_template('admin/index.html', admin_page=True)
49@blueprint.route("/journals", methods=["GET"])
50@login_required
51@ssl_required
52def journals():
53 qs = request.query_string
54 target = url_for("admin.index")
55 if qs:
56 target += "?" + qs.decode()
57 return redirect(target)
60@blueprint.route("/journals", methods=["POST", "DELETE"])
61@login_required
62@ssl_required
63@write_required()
64@jsonp
65def journals_list():
66 if request.method == "POST":
67 try:
68 query = json.loads(request.values.get("q"))
69 except:
70 app.logger.warn("Bad Request at admin/journals: " + str(request.values.get("q")))
71 abort(400)
73 # get the total number of journals to be affected
74 jtotal = models.Journal.hit_count(query, consistent_order=False)
76 # get the total number of articles to be affected
77 issns = models.Journal.issns_by_query(query)
78 atotal = models.Article.count_by_issns(issns)
80 resp = make_response(json.dumps({"journals" : jtotal, "articles" : atotal}))
81 resp.mimetype = "application/json"
82 return resp
84 elif request.method == "DELETE":
85 if not current_user.has_role("delete_article"):
86 abort(401)
88 try:
89 query = json.loads(request.data)
90 except:
91 app.logger.warn("Bad Request at admin/journals: " + str(request.data))
92 abort(400)
94 # get only the query part
95 query = {"query" : query.get("query")}
96 models.Journal.delete_selected(query=query, articles=True, snapshot_journals=True, snapshot_articles=True)
97 resp = make_response(json.dumps({"status" : "success"}))
98 resp.mimetype = "application/json"
99 return resp
102@blueprint.route("/articles", methods=["POST", "DELETE"])
103@login_required
104@ssl_required
105@write_required()
106@jsonp
107def articles_list():
108 if request.method == "POST":
109 try:
110 query = json.loads(request.values.get("q"))
111 except:
112 print(request.values.get("q"))
113 abort(400)
114 total = models.Article.hit_count(query, consistent_order=False)
115 resp = make_response(json.dumps({"total" : total}))
116 resp.mimetype = "application/json"
117 return resp
118 elif request.method == "DELETE":
119 if not current_user.has_role("delete_article"):
120 abort(401)
122 try:
123 query = json.loads(request.data)
124 except:
125 app.logger.warn("Bad Request at admin/journals: " + str(request.data))
126 abort(400)
128 # get only the query part
129 query = {"query" : query.get("query")}
130 models.Article.delete_selected(query=query, snapshot=True)
131 resp = make_response(json.dumps({"status" : "success"}))
132 resp.mimetype = "application/json"
133 return resp
136@blueprint.route("/delete/article/<article_id>", methods=["POST"])
137@login_required
138@ssl_required
139@write_required()
140def article_endpoint(article_id):
141 if not current_user.has_role("delete_article"):
142 abort(401)
143 a = models.Article.pull(article_id)
144 if a is None:
145 abort(404)
146 delete = request.values.get("delete", "false")
147 if delete != "true":
148 abort(400)
149 a.snapshot()
150 a.delete()
151 # return a json response
152 resp = make_response(json.dumps({"success" : True}))
153 resp.mimetype = "application/json"
154 return resp
156@blueprint.route("/article/<article_id>", methods=["GET", "POST"])
157@login_required
158@ssl_required
159@write_required()
160def article_page(article_id):
161 if not current_user.has_role("edit_article"):
162 abort(401)
163 ap = models.Article.pull(article_id)
164 if ap is None:
165 abort(404)
167 fc = ArticleFormFactory.get_from_context(role="admin", source=ap, user=current_user)
168 if request.method == "GET":
169 return fc.render_template()
171 elif request.method == "POST":
172 user = current_user._get_current_object()
173 fc = ArticleFormFactory.get_from_context(role="admin", source=ap, user=user, form_data=request.form)
175 fc.modify_authors_if_required(request.values)
177 if fc.validate():
178 try:
179 fc.finalise()
180 except ArticleMergeConflict:
181 Messages.flash(Messages.ARTICLE_METADATA_MERGE_CONFLICT)
182 except DuplicateArticleException:
183 Messages.flash(Messages.ARTICLE_METADATA_UPDATE_CONFLICT)
185 return fc.render_template()
188@blueprint.route("/journal/<journal_id>", methods=["GET", "POST"])
189@login_required
190@ssl_required
191@write_required()
192def journal_page(journal_id):
193 # ~~JournalForm:Page~~
194 auth_svc = DOAJ.authorisationService()
195 journal_svc = DOAJ.journalService()
197 journal, _ = journal_svc.journal(journal_id)
198 if journal is None:
199 abort(404)
201 try:
202 auth_svc.can_edit_journal(current_user._get_current_object(), journal)
203 except exceptions.AuthoriseException:
204 abort(401)
206 # attempt to get a lock on the object
207 try:
208 lockinfo = lock.lock(constants.LOCK_JOURNAL, journal_id, current_user.id)
209 except lock.Locked as l:
210 return render_template("admin/journal_locked.html", journal=journal, lock=l.lock)
212 fc = JournalFormFactory.context("admin")
213 if request.method == "GET":
214 job = None
215 job_id = request.values.get("job")
216 if job_id is not None and job_id != "":
217 # ~~-> BackgroundJobs:Model~~
218 job = models.BackgroundJob.pull(job_id)
219 # ~~-> BackgroundJobs:Page~~
220 url = url_for("admin.background_jobs_search") + "?source=" + dao.Facetview2.url_encode_query(dao.Facetview2.make_query(job_id))
221 Messages.flash_with_url(Messages.ADMIN__WITHDRAW_REINSTATE.format(url=url), "success")
222 fc.processor(source=journal)
223 return fc.render_template(lock=lockinfo, job=job, obj=journal, lcc_tree=lcc_jstree)
225 elif request.method == "POST":
226 processor = fc.processor(formdata=request.form, source=journal)
227 if processor.validate():
228 try:
229 processor.finalise()
230 flash('Journal updated.', 'success')
231 for a in processor.alert:
232 flash_with_url(a, "success")
233 return redirect(url_for("admin.journal_page", journal_id=journal.id, _anchor='done'))
234 except Exception as e:
235 flash(str(e))
236 return redirect(url_for("admin.journal_page", journal_id=journal.id, _anchor='cannot_edit'))
237 else:
238 return fc.render_template(lock=lockinfo, obj=journal, lcc_tree=lcc_jstree)
240######################################################
241# Endpoints for reinstating/withdrawing journals from the DOAJ
242#
244@blueprint.route("/journal/<journal_id>/activate", methods=["GET", "POST"])
245@login_required
246@ssl_required
247@write_required()
248def journal_activate(journal_id):
249 job = journal_in_out_doaj.change_in_doaj([journal_id], True)
250 return redirect(url_for('.journal_page', journal_id=journal_id, job=job.id))
253@blueprint.route("/journal/<journal_id>/deactivate", methods=["GET", "POST"])
254@login_required
255@ssl_required
256@write_required()
257def journal_deactivate(journal_id):
258 job = journal_in_out_doaj.change_in_doaj([journal_id], False)
259 return redirect(url_for('.journal_page', journal_id=journal_id, job=job.id))
262@blueprint.route("/journals/bulk/withdraw", methods=["POST"])
263@login_required
264@ssl_required
265@write_required()
266def journals_bulk_withdraw():
267 payload = get_web_json_payload()
268 validate_json(payload, fields_must_be_present=['selection_query'], error_to_raise=BulkAdminEndpointException)
270 q = get_query_from_request(payload)
271 summary = journal_in_out_doaj.change_by_query(q, False, dry_run=payload.get("dry_run", True))
272 return make_json_resp(summary.as_dict(), status_code=200)
274@blueprint.route("/journals/bulk/reinstate", methods=["POST"])
275@login_required
276@ssl_required
277@write_required()
278def journals_bulk_reinstate():
279 payload = get_web_json_payload()
280 validate_json(payload, fields_must_be_present=['selection_query'], error_to_raise=BulkAdminEndpointException)
282 q = get_query_from_request(payload)
283 summary = journal_in_out_doaj.change_by_query(q, True, dry_run=payload.get("dry_run", True))
284 return make_json_resp(summary.as_dict(), status_code=200)
286#
287#####################################################################
289@blueprint.route("/journal/<journal_id>/continue", methods=["GET", "POST"])
290@login_required
291@ssl_required
292@write_required()
293def journal_continue(journal_id):
294 j = models.Journal.pull(journal_id)
295 if j is None:
296 abort(404)
298 if request.method == "GET":
299 type = request.values.get("type")
300 form = MakeContinuation()
301 form.type.data = type
302 return render_template("admin/continuation.html", form=form, current=j)
304 elif request.method == "POST":
305 form = MakeContinuation(request.form)
306 if not form.validate():
307 return render_template('admin/continuation.html', form=form, current=j)
309 if form.type.data is None:
310 abort(400)
312 if form.type.data not in ["replaces", "is_replaced_by"]:
313 abort(400)
315 try:
316 cont = j.make_continuation(form.type.data, eissn=form.eissn.data, pissn=form.pissn.data, title=form.title.data)
317 except:
318 abort(400)
320 flash("The continuation has been created (see below). You may now edit the other metadata associated with it. The original journal has also been updated with this continuation's ISSN(s). Once you are happy with this record, you can publish it to the DOAJ", "success")
321 return redirect(url_for('.journal_page', journal_id=cont.id))
324@blueprint.route("/applications", methods=["GET"])
325@login_required
326@ssl_required
327def suggestions():
328 fc = ApplicationFormFactory.context("admin")
329 return render_template("admin/applications.html",
330 admin_page=True,
331 application_status_choices=application_statuses(None, fc))
333@blueprint.route("/update_requests", methods=["GET"])
334@login_required
335@ssl_required
336def update_requests():
337 fc = ApplicationFormFactory.context("admin")
338 return render_template("admin/update_requests.html",
339 admin_page=True,
340 application_status_choices=application_statuses(None, fc))
343@blueprint.route("/application/<application_id>", methods=["GET", "POST"])
344@write_required()
345@login_required
346@ssl_required
347def application(application_id):
348 auth_svc = DOAJ.authorisationService()
349 application_svc = DOAJ.applicationService()
351 ap, _ = application_svc.application(application_id)
353 if ap is None:
354 abort(404)
356 try:
357 auth_svc.can_edit_application(current_user._get_current_object(), ap)
358 except exceptions.AuthoriseException:
359 abort(401)
361 try:
362 lockinfo = lock.lock(constants.LOCK_APPLICATION, application_id, current_user.id)
363 except lock.Locked as l:
364 return render_template("admin/application_locked.html", application=ap, lock=l.lock)
366 fc = ApplicationFormFactory.context("admin")
367 form_diff, current_journal = ApplicationFormXWalk.update_request_diff(ap)
369 if request.method == "GET":
370 fc.processor(source=ap)
371 return fc.render_template(obj=ap, lock=lockinfo, form_diff=form_diff,
372 current_journal=current_journal, lcc_tree=lcc_jstree)
374 elif request.method == "POST":
375 processor = fc.processor(formdata=request.form, source=ap)
376 if processor.validate():
377 try:
378 processor.finalise(current_user._get_current_object())
379 # if (processor.form.resettedFields):
380 # text = "Some fields has been resetted due to invalid value:"
381 # for f in processor.form.resettedFields:
382 # text += "<br>field: {}, invalid value: {}, new value: {}".format(f["name"], f["data"], f["default"])
383 # flash(text, 'info')
384 flash('Application updated.', 'success')
385 for a in processor.alert:
386 flash_with_url(a, "success")
387 return redirect(url_for("admin.application", application_id=ap.id, _anchor='done'))
388 except Exception as e:
389 flash(str(e))
390 return redirect(url_for("admin.application", application_id=ap.id, _anchor='cannot_edit'))
391 else:
392 return fc.render_template(obj=ap, lock=lockinfo, form_diff=form_diff, current_journal=current_journal, lcc_tree=lcc_jstree)
395@blueprint.route("/application_quick_reject/<application_id>", methods=["POST"])
396@login_required
397@ssl_required
398@write_required()
399def application_quick_reject(application_id):
401 # extract the note information from the request
402 canned_reason = request.values.get("quick_reject", "")
403 additional_info = request.values.get("quick_reject_details", "")
404 reasons = []
405 if canned_reason != "":
406 reasons.append(canned_reason)
407 if additional_info != "":
408 reasons.append(additional_info)
409 if len(reasons) == 0:
410 abort(400)
411 reason = " - ".join(reasons)
412 note = Messages.REJECT_NOTE_WRAPPER.format(editor=current_user.id, note=reason)
414 applicationService = DOAJ.applicationService()
416 # retrieve the application and an edit lock on that application
417 application = None
418 try:
419 application, alock = applicationService.application(application_id, lock_application=True, lock_account=current_user._get_current_object())
420 except lock.Locked as e:
421 abort(409)
423 # determine if this was a new application or an update request
424 update_request = application.current_journal is not None
425 if update_request:
426 abort(400)
428 if application.owner is None:
429 Messages.flash_with_url(Messages.ADMIN__QUICK_REJECT__NO_OWNER, "error")
430 # redirect the user back to the edit page
431 return redirect(url_for('.application', application_id=application_id))
433 # reject the application
434 old_status = application.application_status
435 applicationService.reject_application(application, current_user._get_current_object(), note=note)
437 # send the notification email to the user
438 if old_status != constants.APPLICATION_STATUS_REJECTED:
439 eventsSvc = DOAJ.eventsService()
440 eventsSvc.trigger(models.Event(constants.EVENT_APPLICATION_STATUS, current_user.id, {
441 "application": application.data,
442 "old_status": old_status,
443 "new_status": constants.APPLICATION_STATUS_REJECTED,
444 "process": constants.PROCESS__QUICK_REJECT,
445 "note": reason
446 }))
448 # sort out some flash messages for the user
449 flash(note, "success")
451 msg = Messages.SENT_REJECTED_APPLICATION_EMAIL_TO_OWNER.format(user=application.owner)
452 flash(msg, "success")
454 # redirect the user back to the edit page
455 return redirect(url_for('.application', application_id=application_id))
458@blueprint.route("/admin_site_search", methods=["GET"])
459@login_required
460@ssl_required
461def admin_site_search():
462 #edit_formcontext = formcontext.ManEdBulkEdit()
463 #edit_form = edit_formcontext.render_template()
464 edit_formulaic_context = JournalFormFactory.context("bulk_edit")
465 edit_form = edit_formulaic_context.render_template()
467 return render_template("admin/admin_site_search.html",
468 admin_page=True,
469 edit_form=edit_form)
472@blueprint.route("/editor_groups")
473@login_required
474@ssl_required
475def editor_group_search():
476 return render_template("admin/editor_group_search.html", admin_page=True)
478@blueprint.route("/background_jobs")
479@login_required
480@ssl_required
481def background_jobs_search():
482 return render_template("admin/background_jobs_search.html", admin_page=True)
484@blueprint.route("/editor_group", methods=["GET", "POST"])
485@blueprint.route("/editor_group/<group_id>", methods=["GET", "POST"])
486@login_required
487@ssl_required
488@write_required()
489def editor_group(group_id=None):
490 if not current_user.has_role("modify_editor_groups"):
491 abort(401)
493 # ~~->EditorGroup:Form~~
494 if request.method == "GET":
495 form = EditorGroupForm()
496 if group_id is not None:
497 eg = models.EditorGroup.pull(group_id)
498 form.group_id.data = eg.id
499 form.name.data = eg.name
500 form.maned.data = eg.maned
501 form.editor.data = eg.editor
502 form.associates.data = ",".join(eg.associates)
503 return render_template("admin/editor_group.html", admin_page=True, form=form)
505 elif request.method == "POST":
507 if request.values.get("delete", "false") == "true":
508 # we have been asked to delete the id
509 if group_id is None:
510 # we can only delete things that exist
511 abort(400)
512 eg = models.EditorGroup.pull(group_id)
513 if eg is None:
514 abort(404)
516 eg.delete()
518 # return a json response
519 resp = make_response(json.dumps({"success" : True}))
520 resp.mimetype = "application/json"
521 return resp
523 # otherwise, we want to edit the content of the form or the object
524 form = EditorGroupForm(request.form)
526 if form.validate():
527 # get the group id from the url or from the request parameters
528 if group_id is None:
529 group_id = request.values.get("group_id")
530 group_id = group_id if group_id != "" else None
532 # if we have a group id, this is an edit, so get the existing group
533 if group_id is not None:
534 eg = models.EditorGroup.pull(group_id)
535 if eg is None:
536 abort(404)
537 else:
538 eg = models.EditorGroup()
540 associates = form.associates.data
541 if associates is not None:
542 associates = [a.strip() for a in associates.split(",") if a.strip() != ""]
544 # prep the user accounts with the correct role(s)
545 ed = models.Account.pull(form.editor.data)
546 ed.add_role("editor")
547 ed.save()
548 if associates is not None:
549 for a in associates:
550 ae = models.Account.pull(a)
551 if ae is not None: # If the account has been deleted, pull fails
552 ae.add_role("associate_editor")
553 ae.save()
555 eg.set_name(form.name.data)
556 eg.set_maned(form.maned.data)
557 eg.set_editor(form.editor.data)
558 if associates is not None:
559 eg.set_associates(associates)
560 eg.save()
562 flash("Group was updated - changes may not be reflected below immediately. Reload the page to see the update.", "success")
563 return redirect(url_for('admin.editor_group_search'))
564 else:
565 return render_template("admin/editor_group.html", admin_page=True, form=form)
568@blueprint.route("/autocomplete/user")
569@login_required
570@ssl_required
571def user_autocomplete():
572 q = request.values.get("q")
573 s = request.values.get("s", 10)
574 ac = models.Account.autocomplete("id", q, size=s)
576 # return a json response
577 resp = make_response(json.dumps(ac))
578 resp.mimetype = "application/json"
579 return resp
582# Route which returns the associate editor account names within a given editor group
583@blueprint.route("/dropdown/eg_associates")
584@login_required
585@ssl_required
586def eg_associates_dropdown():
587 egn = request.values.get("egn")
588 eg = models.EditorGroup.pull_by_key("name", egn)
590 if eg is not None:
591 editors = [eg.editor]
592 editors += eg.associates
593 editors = list(set(editors))
594 else:
595 editors = None
597 # return a json response
598 resp = make_response(json.dumps(editors))
599 resp.mimetype = "application/json"
600 return resp
603####################################################
604## endpoints for bulk edit
606class BulkAdminEndpointException(Exception):
607 pass
610@app.errorhandler(BulkAdminEndpointException)
611def bulk_admin_endpoints_bad_request(exception):
612 r = {}
613 r['error'] = exception.message
614 return make_json_resp(r, status_code=400)
617def get_bulk_edit_background_task_manager(doaj_type):
618 if doaj_type == 'journals':
619 return journal_bulk_edit.journal_manage
620 elif doaj_type in ['applications', 'update_requests']:
621 return suggestion_bulk_edit.suggestion_manage
622 else:
623 raise BulkAdminEndpointException('Unsupported DOAJ type - you can currently only bulk edit journals and applications/update_requests.')
626def get_query_from_request(payload, doaj_type=None):
627 q = payload['selection_query']
628 q = remove_search_limits(q)
630 q = Query(q)
632 if doaj_type == "update_requests":
633 update_request(q)
634 elif doaj_type == "applications":
635 not_update_request(q)
637 return q.as_dict()
640@blueprint.route("/<doaj_type>/bulk/assign_editor_group", methods=["POST"])
641@login_required
642@ssl_required
643@write_required()
644def bulk_assign_editor_group(doaj_type):
645 task = get_bulk_edit_background_task_manager(doaj_type)
647 payload = get_web_json_payload()
648 validate_json(payload, fields_must_be_present=['selection_query', 'editor_group'], error_to_raise=BulkAdminEndpointException)
650 summary = task(
651 selection_query=get_query_from_request(payload, doaj_type),
652 editor_group=payload['editor_group'],
653 dry_run=payload.get('dry_run', True)
654 )
656 return make_json_resp(summary.as_dict(), status_code=200)
659@blueprint.route("/<doaj_type>/bulk/add_note", methods=["POST"])
660@login_required
661@ssl_required
662@write_required()
663def bulk_add_note(doaj_type):
664 task = get_bulk_edit_background_task_manager(doaj_type)
666 payload = get_web_json_payload()
667 validate_json(payload, fields_must_be_present=['selection_query', 'note'], error_to_raise=BulkAdminEndpointException)
669 summary = task(
670 selection_query=get_query_from_request(payload, doaj_type),
671 note=payload['note'],
672 dry_run=payload.get('dry_run', True)
673 )
675 return make_json_resp(summary.as_dict(), status_code=200)
677@blueprint.route("/journals/bulk/edit_metadata", methods=["POST"])
678@login_required
679@ssl_required
680@write_required()
681def bulk_edit_journal_metadata():
682 task = get_bulk_edit_background_task_manager("journals")
684 payload = get_web_json_payload()
685 if not "metadata" in payload:
686 raise BulkAdminEndpointException("key 'metadata' not present in request json")
688 formdata = MultiDict(payload["metadata"])
689 formulaic_context = JournalFormFactory.context("bulk_edit")
690 fc = formulaic_context.processor(formdata=formdata)
692 if not fc.validate():
693 msg = "Unable to submit your request due to form validation issues: "
694 for field in fc.form:
695 if field.errors:
696 msg += field.label.text + " - " + ",".join(field.errors)
697 summary = BackgroundSummary(None, error=msg)
698 else:
699 summary = task(
700 selection_query=get_query_from_request(payload),
701 dry_run=payload.get('dry_run', True),
702 **payload["metadata"]
703 )
705 return make_json_resp(summary.as_dict(), status_code=200)
708@blueprint.route("/<doaj_type>/bulk/change_status", methods=["POST"])
709@login_required
710@ssl_required
711@write_required()
712def applications_bulk_change_status(doaj_type):
713 if doaj_type not in ["applications", "update_requests"]:
714 abort(403)
715 payload = get_web_json_payload()
716 validate_json(payload, fields_must_be_present=['selection_query', 'application_status'], error_to_raise=BulkAdminEndpointException)
718 q = get_query_from_request(payload, doaj_type)
719 summary = get_bulk_edit_background_task_manager('applications')(
720 selection_query=q,
721 application_status=payload['application_status'],
722 dry_run=payload.get('dry_run', True)
723 )
725 return make_json_resp(summary.as_dict(), status_code=200)
728@blueprint.route("/journals/bulk/delete", methods=['POST'])
729@write_required()
730def bulk_journals_delete():
731 if not current_user.has_role("ultra_bulk_delete"):
732 abort(403)
733 payload = get_web_json_payload()
734 validate_json(payload, fields_must_be_present=['selection_query'], error_to_raise=BulkAdminEndpointException)
736 q = get_query_from_request(payload)
737 summary = journal_bulk_delete.journal_bulk_delete_manage(
738 selection_query=q,
739 dry_run=payload.get('dry_run', True)
740 )
741 return make_json_resp(summary.as_dict(), status_code=200)
744@blueprint.route("/articles/bulk/delete", methods=['POST'])
745@write_required()
746def bulk_articles_delete():
747 if not current_user.has_role("ultra_bulk_delete"):
748 abort(403)
749 payload = get_web_json_payload()
750 validate_json(payload, fields_must_be_present=['selection_query'], error_to_raise=BulkAdminEndpointException)
752 q = get_query_from_request(payload)
753 summary = article_bulk_delete.article_bulk_delete_manage(
754 selection_query=q,
755 dry_run=payload.get('dry_run', True)
756 )
758 return make_json_resp(summary.as_dict(), status_code=200)
760#################################################