|
1 | 1 | import json |
2 | 2 | import pprint |
3 | 3 | import traceback |
| 4 | +from urllib.parse import urlencode |
4 | 5 |
|
5 | 6 | import structlog |
6 | 7 | from channels.generic.websocket import WebsocketConsumer |
7 | 8 | from django.conf import settings |
| 9 | +from django.contrib.auth import REDIRECT_FIELD_NAME |
8 | 10 | from django.template.loader import render_to_string |
9 | | -from gspread.exceptions import SpreadsheetNotFound |
| 11 | +from django.urls import reverse |
| 12 | +from google.auth.exceptions import RefreshError |
| 13 | +from gspread.exceptions import APIError, SpreadsheetNotFound |
10 | 14 | from gspread.utils import extract_id_from_url |
11 | 15 | from requests import Response |
12 | 16 |
|
13 | 17 | from apps.publish_mdm.models import FormTemplate |
14 | 18 |
|
15 | 19 | from .etl.load import PublishTemplateEvent, publish_form_template |
| 20 | +from .utils import get_login_url |
16 | 21 |
|
17 | 22 | logger = structlog.getLogger(__name__) |
18 | 23 |
|
@@ -58,15 +63,86 @@ def receive(self, text_data): |
58 | 63 | logger.exception("Error publishing form") |
59 | 64 | tbe = traceback.TracebackException.from_exception(exc=e, compact=True) |
60 | 65 | message = "".join(tbe.format()) |
61 | | - summary = None |
62 | | - if isinstance(e, SpreadsheetNotFound): |
63 | | - summary = self.get_google_picker(form_template_id=event_data.get("form_template")) |
| 66 | + summary = self.get_error_summary(e, event_data) |
64 | 67 | # If the error is from ODK Central, format the error message for easier reading |
65 | 68 | if len(e.args) >= 2 and isinstance(e.args[1], Response): |
66 | 69 | data = e.args[1].json() |
67 | 70 | message = f"ODK Central error:\n\n{pprint.pformat(data)}\n\n{message}" |
68 | 71 | self.send_message(message, error=True, error_summary=summary) |
69 | 72 |
|
| 73 | + def get_error_summary(self, exc: Exception, event_data: dict): |
| 74 | + """For some exceptions, add a helpful message or instructions that will be |
| 75 | + displayed above the traceback. |
| 76 | + """ |
| 77 | + if isinstance(exc, SpreadsheetNotFound): |
| 78 | + # User has not authorized us to access the file using their credentials. |
| 79 | + # Display a message to that effect and a button for them to give us access |
| 80 | + # using the Google Picker |
| 81 | + return self.get_google_picker(form_template_id=event_data.get("form_template")) |
| 82 | + |
| 83 | + error_message = None |
| 84 | + button = None |
| 85 | + |
| 86 | + if (is_refresh_error := isinstance(exc, RefreshError)) or ( |
| 87 | + # gspread raises an APIError, catches it, then does `raise PermissionError from ...` |
| 88 | + isinstance(exc, PermissionError) and isinstance(exc.__context__, APIError) |
| 89 | + ): |
| 90 | + if ( |
| 91 | + is_refresh_error |
| 92 | + or "Request had insufficient authentication scopes" |
| 93 | + in exc.__context__.error["message"] |
| 94 | + ): |
| 95 | + # Either an expired/invalid refresh token, or the user did not |
| 96 | + # check the checkbox to give us access to their Google Drive files |
| 97 | + # when they first logged in. Ask them to log in again |
| 98 | + error_message = ( |
| 99 | + "Sorry, you need to log in again to be able to publish. " |
| 100 | + "Please click the button below." |
| 101 | + ) |
| 102 | + form_template = FormTemplate.objects.get(id=event_data.get("form_template")) |
| 103 | + publish_url = reverse( |
| 104 | + "publish_mdm:form-template-publish", |
| 105 | + args=[ |
| 106 | + form_template.project.organization.slug, |
| 107 | + form_template.project.id, |
| 108 | + form_template.id, |
| 109 | + ], |
| 110 | + ) |
| 111 | + # Add a link that will log them out then redirect to the login page. |
| 112 | + # User will be taken through the OAuth flow again then redirected |
| 113 | + # back to the publish page |
| 114 | + logout_url = reverse("account_logout") |
| 115 | + login_url = get_login_url(publish_url) |
| 116 | + querystring = urlencode({REDIRECT_FIELD_NAME: login_url}) |
| 117 | + button = { |
| 118 | + "href": f"{logout_url}?{querystring}", |
| 119 | + "text": "Log in again", |
| 120 | + } |
| 121 | + elif "The caller does not have permission" in exc.__context__.error["message"]: |
| 122 | + # User does not have access to the file in Google Sheets. |
| 123 | + # Display instructions on how to confirm if they have access |
| 124 | + error_message = ( |
| 125 | + "Unfortunately, we could not access the form in Google Sheets. " |
| 126 | + 'Click the button below to open the spreadsheet and request access.' |
| 127 | + '<br><br>' |
| 128 | + 'Within the spreadsheet, you or someone else with access will need to click ' |
| 129 | + '<strong>Share</strong> and confirm the Google user ' |
| 130 | + f'<strong>{self.scope["user"].email}</strong> appears in the list of ' |
| 131 | + 'people with access.' |
| 132 | + '<br><br>' |
| 133 | + "When done, return to this page and click " |
| 134 | + "<strong>Publish next version</strong> again." |
| 135 | + ) |
| 136 | + form_template = FormTemplate.objects.get(id=event_data.get("form_template")) |
| 137 | + button = {"href": form_template.template_url, "text": "Open spreadsheet"} |
| 138 | + |
| 139 | + if error_message or button: |
| 140 | + context = { |
| 141 | + "error_message": error_message, |
| 142 | + "button": button, |
| 143 | + } |
| 144 | + return render_to_string("publish_mdm/ws/form_template_error_summary.html", context) |
| 145 | + |
70 | 146 | def publish_form_template(self, event_data: dict): |
71 | 147 | """Publish a form template to ODK Central and stream progress to the browser.""" |
72 | 148 | # Parse the event data and raise an error if it's invalid |
|
0 commit comments