diff --git a/brownbag/t3/revisions.py b/brownbag/t3/revisions.py --- a/brownbag/t3/revisions.py +++ b/brownbag/t3/revisions.py @@ -1,246 +1,87 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # Totally fixed now import json import logging -import requests -from flask import Blueprint, current_app, redirect, render_template, session +from flask import Blueprint, current_app, render_template, redirect, session from landoui.forms import RevisionForm from landoui.helpers import is_user_authenticated, set_last_local_referrer +from landoui.landoapiclient import LandoAPIClient, LandingSubmissionError logger = logging.getLogger(__name__) revisions = Blueprint('revisions', __name__) revisions.before_request(set_last_local_referrer) @revisions.route('/revisions//', methods=('GET', 'POST')) -# This route is a GET only because the diff ID will be added via JavaScript @revisions.route('/revisions/') def revisions_handler(revision_id, diff_id=None): - # Load Revision and Landing History - try: - revision = _get_revision(revision_id, diff_id) - landing_statuses = _get_landing_statuses(revision_id) - except requests.HTTPError as exc: - if exc.response.status_code == 404: - return render_template('revision/404.html'), 404 - elif exc.response.status_code == 400: - error_msg = exc.response.json()['title'] - if error_msg == 'Diff not related to the revision': - return render_template('revision/400_wrong_diff.html'), 400 - raise - parent_revisions = _flatten_parent_revisions(revision) + landoapi = LandoAPIClient( + landoapi_url=current_app.config['LANDO_API_URL'], + phabricator_api_token=None, + auth0_access_token=session.get('access_token') + ) - # Fetch any potential landing issues to display. - # TODO: Save the dryrun confirmation token in the Revision form to be - # used when making the real landing request. https://trello.com/c/dh0c0YTs. - # TODO: We shouldn't check the dryrun result before a POST to actually - # submit the landing request. We do this now because the UI would otherwise - # lack data. When the landing request is submitted if there are any new - # warnings or blockers, they are returned and should be used. If landing - # is successful, we must redirect to the GET revision handler so that - # fresh revision and landing history is loaded. - # Also consider splitting into two routes: https://trello.com/c/Oo3RUzUR. - dryrun_confirmation_token = None # noqa - warnings = [] - blockers = [] - if is_user_authenticated(): - dryrun_result = _get_landing_dryrun_result(revision) - dryrun_confirmation_token = dryrun_result['confirmation_token'] # noqa - warnings = dryrun_result['warnings'] - blockers = dryrun_result['blockers'] - - # Creates a new form on GET or loads the submitted form on a POST + # Loads the submitted form if POST or creates a new one if GET form = RevisionForm() errors = [] + + # Submit the landing request if this is a POST if form.is_submitted(): if form.validate(): try: - return _post_landings(revision['id'], form.diff_id.data) + # Returns True or raises a LandingSubmissionError + if landoapi.post_landings(revision_id, form.diff_id.data): + redirect_url = '/revisions/{revision_id}/{diff_id}'.format( + revision_id=revision_id, diff_id=diff_id + ) + return redirect(redirect_url) except LandingSubmissionError as e: - errors.append(e.message) + errors.append(e.error) else: for field, field_errors in form.errors.items(): for error in field_errors: errors.append(error) - # Set the diff id explicitly to avoid timing conflicts with - # revision diff IDs being updated + # If this is a GET or the POST fails, load data to display revision page. + revision = landoapi.get_revision(revision_id, diff_id) + parent_revisions = _flatten_parent_revisions(revision) + landing_statuses = landoapi.get_landings(revision_id) + dryrun_result = {} + if is_user_authenticated(): + dryrun_result = landoapi.post_landings_dryrun(revision_id, diff_id) + # TODO: Save the dryrun confirmation token in the form so it is + # used when making the real landing request. form.diff_id.data = revision['diff']['id'] return render_template( 'revision/revision.html', revision=revision, landing_statuses=landing_statuses, parents=parent_revisions, form=form, - warnings=warnings, - blockers=blockers, + dryrun_confirmation_token=dryrun_result.get('confirmation_token'), + warnings=dryrun_result.get('warnings', []), + blockers=dryrun_result.get('blockers', []), errors=errors ) -def _post_landings(revision_id, diff_id): - """Submit a landing request to lando-api. - - Args: - revision_id: The id of the revision to land in 'D123' format. - diff_id: The id of the specific diff of the revision to land. - - Returns: - If successful, returns a redirect to the GET /revision page for the - revision id. Does not return if unsuccessful. - - Exceptions: - If the landing submission fails for any reason, a - LandingSubmissionError will be raised. - """ - # Double check the user is logged in so we can provide a helpful message. - if not is_user_authenticated(): - raise LandingSubmissionError('You must be logged to land.') - - # Setup request - land_url = '{host}/landings'.format( - host=current_app.config['LANDO_API_URL'] - ) - params = { - 'revision_id': revision_id, - 'diff_id': int(diff_id), - } - headers = { - 'Authorization': 'Bearer {}'.format(session['access_token']), - 'Content-Type': 'application/json', - } - - # Make request and handle response - try: - land_response = requests.post(land_url, json=params, headers=headers) - land_response.raise_for_status() - if land_response.status_code not in (200, 202): - # TODO logging/sentry of the request? - raise LandingSubmissionError( - message=( - 'Lando API did not respond successfully. ' - 'Please try again later.' - ) - ) - - redirect_url = '/revisions/{revision_id}/{diff_id}'.format( - revision_id=revision_id, diff_id=diff_id - ) - return redirect(redirect_url) - except requests.HTTPError as e: - # All HTTP errors from Lando API should be in the Connexions - # problem exception format and include title, detail, and type. - try: - problem = e.response.json() - problem_message = ( - '{title}: {detail}' - .format(title=problem['title'], detail=problem['detail']) - ) - except (json.JSONDecodeError, KeyError): - # TODO: logging/sentry - raise LandingSubmissionError( - message=( - 'Lando API did not respond successfully. ' - 'Please try again later.' - ) - ) - - # TODO: logging? - raise LandingSubmissionError( - message=problem_message, link=problem['type'] - ) - except requests.RequestException: - # TODO: How best to do logging? - raise LandingSubmissionError( - 'Failed to connect to Lando API. ' - 'Please try again later.' - ) - - -def _get_revision(revision_id, diff_id): - revision_api_url = '{host}/revisions/{revision_id}'.format( - host=current_app.config['LANDO_API_URL'], revision_id=revision_id - ) - result = requests.get(revision_api_url, params={'diff_id': diff_id}) - result.raise_for_status() - return result.json() - - -def _get_landing_statuses(revision_id): - landing_api_status_url = '{host}/landings'.format( - host=current_app.config['LANDO_API_URL'] - ) - result = requests.get( - landing_api_status_url, params={'revision_id': revision_id} - ) - result.raise_for_status() - return result.json() - - -def _get_landing_dryrun_result(revision): - """Fetch any warnings or blockers for landing the revision. - - Queries Lando API's /landings/dryrun endpoint which tells us - whether there are any issues before landings. - - Issues can one be either a warning or a blocker. - Users may acknowledge a warning and continue landing. - If there are any blockers, landing is blocked until those resolve. - - Lando API also provides a confirmation token which is expected when - performing the real landing via the POST /landings endpoint. This token - tells the API that we've properly tried a dryrun first and acknowledged - any warnings. - - Returns a dictionary with the following format: - { - 'confirmation_token': token, - 'warnings': [{'id': id, 'message': message}, ...], - 'blockers': [{'id': id, 'message': message}, ...] - } - """ - # TODO Replace me with a query to the real dryrun endpoint. - # Tracked in: https://trello.com/c/dh0c0YTs/ - result = { - 'confirmation_token': 'placeholder', - 'warnings': [], - 'blockers': [] - } - return result - - def _flatten_parent_revisions(revision): """ Transforms a JSON tree of parent revisions into a flat array. Args: - revision: A revision (hash) which has parent revisions, which + revision: A revision (dictionary) which has parent revisions, which can themselves have parent revisions, and so on. Returns: A new array containing the parent revisions in breath first order. """ parents = revision.get('parent_revisions', []) parents_of_parents = [] for parent in parents: parents_of_parents += _flatten_parent_revisions(parent) return parents + parents_of_parents - - -class LandingSubmissionError(Exception): - """Custom Exception to hold information when a landing immediately fails. - - Attributes: - message: Human readable message with details of what happened. - link: An optional link to a resource that further describes the error. - """ - - def __init__(self, message, link=None): - full_message = '{} ({})'.format(message, link) if link else message - super().__init__(full_message) - self.message = message - self.link = link