def request_withdrawal(method: str, params: MultiDict, session: Session, submission_id: int, **kwargs) -> Response: """Request withdrawal of a paper.""" submitter, client = user_and_client_from_session(session) logger.debug(f'method: {method}, submission: {submission_id}. {params}') # Will raise NotFound if there is no such submission. submission, _ = load_submission(submission_id) # The submission must be announced for this to be a withdrawal request. if not submission.is_announced: alerts.flash_failure( Markup( "Submission must first be announced. See " "<a href='https://arxiv.org/help/withdraw'>the arXiv help pages" "</a> for details.")) loc = url_for('ui.create_submission') return {}, status.SEE_OTHER, {'Location': loc} # The form should be prepopulated based on the current state of the # submission. if method == 'GET': params = MultiDict({}) params.setdefault("confirmed", False) form = WithdrawalForm(params) response_data = { 'submission_id': submission_id, 'submission': submission, 'form': form, } if method == 'POST': # We require the user to confirm that they wish to proceed. if not form.validate(): raise BadRequest(response_data) cmd = RequestWithdrawal(reason=form.withdrawal_reason.data, creator=submitter, client=client) if not validate_command(form, cmd, submission, 'withdrawal_reason'): raise BadRequest(response_data) if not form.confirmed.data: response_data['require_confirmation'] = True return response_data, status.OK, {} response_data['require_confirmation'] = True try: # Save the events created during form validation. submission, _ = save(cmd, submission_id=submission_id) except SaveError as e: raise InternalServerError(response_data) from e # Success! Send user back to the submission page. alerts.flash_success("Withdrawal request submitted.") status_url = url_for('ui.create_submission') return {}, status.SEE_OTHER, {'Location': status_url} logger.debug('Nothing to do, return 200') return response_data, status.OK, {}
def start_compilation(params: MultiDict, session: Session, submission_id: int, token: str, **kwargs) -> Response: submitter, client = user_and_client_from_session(session) submission, submission_events = load_submission(submission_id) form = CompilationForm(params) response_data = { 'submission_id': submission_id, 'submission': submission, 'form': form, 'status': None, 'must_process': _must_process(submission) } # Create label and link for PS/PDF stamp/watermark. # # Stamp format for submission is of form [identifier category date] # # "arXiv:submit/<submission_id> [<primary category>] DD MON YYYY # # Date segment is optional and added automatically by converter. # stamp_label = f'arXiv:submit/{submission_id}' if submission.primary_classification \ and submission.primary_classification.category: # Create stamp label string - for now we'll let converter # add date segment to stamp label primary_category = submission.primary_classification.category stamp_label = stamp_label + f' [{primary_category}]' stamp_link = f'/{submission_id}/preview.pdf' if not form.validate(): raise BadRequest(response_data) try: logger.debug('Start compilation for %s (identifier) %s (checksum)', submission.source_content.identifier, submission.source_content.checksum) stat = Compiler.compile(submission.source_content.identifier, submission.source_content.checksum, token, stamp_label, stamp_link) except exceptions.RequestFailed as e: alerts.flash_failure(f"We couldn't compile your submission. {SUPPORT}", title="Compilation failed") logger.error('Error while requesting compilation for %s: %s', submission_id, e) raise InternalServerError(response_data) from e response_data['status'] = stat if stat.status is Compilation.Status.FAILED: alerts.flash_failure(f"Compilation failed") else: alerts.flash_success( "We are compiling your submission. This may take a minute or two." " This page will refresh automatically every 5 seconds. You can " " also refresh this page manually to check the current status. ", title="Compilation started" ) redirect = url_for('ui.file_process', submission_id=submission_id) return response_data, status.SEE_OTHER, {'Location': redirect}
def test_safe_flash_message(self, mock_flash): """Flash a simple message that is HTML-safe.""" alerts.flash_success('The message', safe=True) (data, category), kwargs = mock_flash.call_args self.assertEqual(data['message'], 'The message') self.assertIsInstance(data['message'], Markup) self.assertTrue(data['dismissable']) self.assertEqual(category, alerts.SUCCESS)
def test_flash_message_no_dismiss(self, mock_flash): """Flash a simple message that can't be dismissed.""" alerts.flash_success('The message', dismissable=False) (data, category), kwargs = mock_flash.call_args self.assertEqual(data['message'], 'The message') self.assertIsInstance(data['message'], str) self.assertFalse(data['dismissable']) self.assertEqual(category, alerts.SUCCESS)
def test_flash_message(self, mock_flash): """Just flash a simple message.""" alerts.flash_success('The message') (data, category), kwargs = mock_flash.call_args self.assertEqual(data['message'], 'The message') self.assertIsInstance(data['message'], str) self.assertTrue(data['dismissable']) self.assertEqual(category, alerts.SUCCESS)
def request_cross(method: str, params: MultiDict, session: Session, submission_id: int, **kwargs) -> Response: """Request cross-list classification for an announced e-print.""" submitter, client = user_and_client_from_session(session) logger.debug(f'method: {method}, submission: {submission_id}. {params}') # Will raise NotFound if there is no such submission. submission, submission_events = load_submission(submission_id) # The submission must be announced for this to be a cross-list request. if not submission.is_announced: alerts.flash_failure( Markup("Submission must first be announced. See <a" " href='https://arxiv.org/help/cross'>the arXiv help" " pages</a> for details.")) status_url = url_for('ui.create_submission') return {}, status.SEE_OTHER, {'Location': status_url} if method == 'GET': params = MultiDict({}) params.setdefault("confirmed", False) params.setdefault("operation", CrossListForm.ADD) form = CrossListForm(params) selected = [v for v in form.selected.data if v] form.filter_choices(submission, session, exclude=selected) response_data = { 'submission_id': submission_id, 'submission': submission, 'form': form, 'selected': selected, 'formset': CrossListForm.formset(selected) } if submission.primary_classification: response_data['primary'] = \ CATEGORIES[submission.primary_classification.category] if method == 'POST': if not form.validate(): raise BadRequest(response_data) if form.confirmed.data: # Stop adding new categories, and submit. response_data['form'].operation.data = CrossListForm.ADD response_data['require_confirmation'] = True command = RequestCrossList(creator=submitter, client=client, categories=form.selected.data) if not validate_command(form, command, submission, 'category'): alerts.flash_failure(Markup( "There was a problem with your request. Please try again." f" {CONTACT_SUPPORT}" )) raise BadRequest(response_data) try: # Submit the cross-list request. save(command, submission_id=submission_id) except SaveError as e: # This would be due to a database error, or something else # that likely isn't the user's fault. logger.error('Could not save cross list request event') alerts.flash_failure(Markup( "There was a problem processing your request. Please try" f" again. {CONTACT_SUPPORT}" )) raise InternalServerError(response_data) from e # Success! Send user back to the submission page. alerts.flash_success("Cross-list request submitted.") status_url = url_for('ui.create_submission') return {}, status.SEE_OTHER, {'Location': status_url} else: # User is adding or removing a category. if form.operation.data: if form.operation.data == CrossListForm.REMOVE: selected.remove(form.category.data) elif form.operation.data == CrossListForm.ADD: selected.append(form.category.data) # Update the "remove" formset to reflect the change. response_data['formset'] = CrossListForm.formset(selected) response_data['selected'] = selected # Now that we've handled the request, get a fresh form for adding # more categories or submitting the request. response_data['form'] = CrossListForm() response_data['form'].filter_choices(submission, session, exclude=selected) response_data['form'].operation.data = CrossListForm.ADD response_data['require_confirmation'] = True return response_data, status.OK, {} return response_data, status.OK, {}
def jref(method: str, params: MultiDict, session: Session, submission_id: int, **kwargs) -> Response: """Set journal reference metadata on a announced submission.""" creator, client = user_and_client_from_session(session) logger.debug(f'method: {method}, submission: {submission_id}. {params}') # Will raise NotFound if there is no such submission. submission, submission_events = load_submission(submission_id) # The submission must be announced for this to be a real JREF submission. if not submission.is_announced: alerts.flash_failure( Markup("Submission must first be announced. See " "<a href='https://arxiv.org/help/jref'>" "the arXiv help pages</a> for details.")) status_url = url_for('ui.create_submission') return {}, status.SEE_OTHER, {'Location': status_url} # The form should be prepopulated based on the current state of the # submission. if method == 'GET': params = MultiDict({ 'doi': submission.metadata.doi, 'journal_ref': submission.metadata.journal_ref, 'report_num': submission.metadata.report_num }) params.setdefault("confirmed", False) form = JREFForm(params) response_data = { 'submission_id': submission_id, 'submission': submission, 'form': form, } if method == 'POST': # We require the user to confirm that they wish to proceed. We show # them a preview of what their paper's abs page will look like after # the proposed change. They can either make further changes, or # confirm and submit. if not form.validate(): logger.debug('Invalid form data; return bad request') raise BadRequest(response_data) if not form.confirmed.data: response_data['require_confirmation'] = True logger.debug('Not confirmed') return response_data, status.OK, {} commands, valid = _generate_commands(form, submission, creator, client) if commands: # Metadata has changed; we have things to do. if not all(valid): raise BadRequest(response_data) response_data['require_confirmation'] = True logger.debug('Form is valid, with data: %s', str(form.data)) try: # Save the events created during form validation. submission, _ = save(*commands, submission_id=submission_id) except SaveError as e: logger.error('Could not save metadata event') raise InternalServerError(response_data) from e response_data['submission'] = submission # Success! Send user back to the submission page. alerts.flash_success("Journal reference updated") status_url = url_for('ui.create_submission') return {}, status.SEE_OTHER, {'Location': status_url} logger.debug('Nothing to do, return 200') return response_data, status.OK, {}
def _new_file(params: MultiDict, pointer: FileStorage, session: Session, submission: Submission, rdata: Dict[str, Any], token: str) \ -> Response: """ Handle a POST request with a new file to add to an existing upload package. This occurs in the case that there is already an upload workspace associated with the submission. See the :attr:`Submission.source_content` attribute, which is set using :class:`SetUploadPackage`. Parameters ---------- params : :class:`MultiDict` The form data from the request. pointer : :class:`FileStorage` The file upload stream. session : :class:`Session` The authenticated session for the request. submission : :class:`Submission` The submission for which the upload is being made. Returns ------- dict Response data, to render in template. int HTTP status code. This should be ``303``, unless something goes wrong. dict Extra headers to add/update on the response. This should include the `Location` header for use in the 303 redirect response. """ submitter, client = user_and_client_from_session(session) fm = FileManager.current_session() upload_id = submission.source_content.identifier # Using a form object provides some extra assurance that this is a legit # request; provides CSRF goodies. params['file'] = pointer form = UploadForm(params) rdata['form'] = form if not form.validate(): logger.error('Invalid upload form: %s', form.errors) alerts.flash_failure("Something went wrong. Please try again.", title="Whoops") # redirect = url_for('ui.file_upload', # submission_id=submission.submission_id) # return {}, status.SEE_OTHER, {'Location': redirect} logger.debug('Invalid form data') raise BadRequest(rdata) ancillary: bool = form.ancillary.data try: stat = fm.add_file(upload_id, pointer, token, ancillary=ancillary) except exceptions.RequestFailed as e: try: e_data = e.response.json() except Exception: e_data = None if e_data is not None and 'reason' in e_data: alerts.flash_failure( Markup('There was a problem carrying out your request:' f' {e_data["reason"]}. {PLEASE_CONTACT_SUPPORT}')) raise BadRequest(rdata) alerts.flash_failure( Markup('There was a problem carrying out your request. Please try' f' again. {PLEASE_CONTACT_SUPPORT}')) logger.debug('Failed to add file: %s', ) logger.error(traceback.format_exc()) raise InternalServerError(rdata) from e submission = _update(form, submission, stat, submitter, client, rdata) converted_size = tidy_filesize(stat.size) if stat.status is Upload.Status.READY: alerts.flash_success( f'Uploaded {pointer.filename} successfully. Total submission' f' package size is {converted_size}', title='Upload successful') elif stat.status is Upload.Status.READY_WITH_WARNINGS: alerts.flash_warning( f'Uploaded {pointer.filename} successfully. Total submission' f' package size is {converted_size}. See below for warnings.', title='Upload complete, with warnings') elif stat.status is Upload.Status.ERRORS: alerts.flash_warning( f'Uploaded {pointer.filename} successfully. Total submission' f' package size is {converted_size}. See below for errors.', title='Upload complete, with errors') status_data = stat.to_dict() alerts.flash_hidden(status_data, '_status') loc = url_for('ui.file_upload', submission_id=submission.submission_id) return {}, status.SEE_OTHER, {'Location': loc}
def delete(method: str, params: MultiDict, session: Session, submission_id: int, token: Optional[str] = None, **kwargs) -> Response: """ Handle a request to delete a file. The file will only be deleted if a POST request is made that also contains the ``confirmed`` parameter. The process can be initiated with a GET request that contains the ``path`` (key) for the file to be deleted. For example, a button on the upload interface may link to the deletion route with the file path as a query parameter. This will generate a deletion confirmation form, which can be POSTed to complete the action. Parameters ---------- method : str ``GET`` or ``POST`` params : :class:`MultiDict` The query or form data from the request. session : :class:`Session` The authenticated session for the request. submission_id : int The identifier of the submission for which the deletion is being made. token : str The original (encrypted) auth token on the request. Used to perform subrequests to the file management service. Returns ------- dict Response data, to render in template. int HTTP status code. This should be ``200`` or ``303``, unless something goes wrong. dict Extra headers to add/update on the response. This should include the `Location` header for use in the 303 redirect response, if applicable. """ if token is None: logger.debug('Missing auth token') raise BadRequest('Missing auth token') submission, submission_events = load_submission(submission_id) upload_id = submission.source_content.identifier submitter, client = user_and_client_from_session(session) rdata = {'submission': submission, 'submission_id': submission_id} if method == 'GET': # The only thing that we want to get from the request params on a GET # request is the file path. This way there is no way for a GET request # to trigger actual deletion. The user must explicitly indicate via # a valid POST that the file should in fact be deleted. params = MultiDict({'file_path': params['path']}) form = DeleteFileForm(params) rdata.update({'form': form}) if method == 'POST': if not (form.validate() and form.confirmed.data): logger.debug('Invalid form data') raise BadRequest(rdata) stat: Optional[Upload] = None try: file_path = form.file_path.data stat = FileManager.delete_file(upload_id, file_path, token) alerts.flash_success( f'File <code>{form.file_path.data}</code> was deleted' ' successfully', title='Deleted file successfully', safe=True) except exceptions.RequestForbidden: alerts.flash_failure( Markup( 'There was a problem authorizing your request. Please try' f' again. {PLEASE_CONTACT_SUPPORT}')) except exceptions.BadRequest: alerts.flash_warning( Markup('Something odd happened when processing your request.' f'{PLEASE_CONTACT_SUPPORT}')) except exceptions.RequestFailed: alerts.flash_failure( Markup( 'There was a problem carrying out your request. Please try' f' again. {PLEASE_CONTACT_SUPPORT}')) if stat is not None: command = UpdateUploadPackage(creator=submitter, checksum=stat.checksum, uncompressed_size=stat.size, source_format=stat.source_format) if not validate_command(form, command, submission): logger.debug('Command validation failed') raise BadRequest(rdata) commands = [command] if submission.submitter_compiled_preview: commands.append(UnConfirmCompiledPreview(creator=submitter)) try: submission, _ = save(*commands, submission_id=submission_id) except SaveError: alerts.flash_failure( Markup( 'There was a problem carrying out your request. Please try' f' again. {PLEASE_CONTACT_SUPPORT}')) redirect = url_for('ui.file_upload', submission_id=submission_id) return {}, status.SEE_OTHER, {'Location': redirect} return rdata, status.OK, {}