Example #1
0
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, {}
Example #2
0
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}
Example #3
0
 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)
Example #4
0
 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)
Example #5
0
 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)
Example #6
0
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, {}
Example #7
0
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, {}
Example #8
0
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}
Example #9
0
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, {}