diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1dd7bd19..478b3990 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,7 +52,7 @@ jobs: - name: Run tests run: | - just test-all + RUN_SCREENSHOT_TESTS=True just test-all # not actually needed for tests, but we want to make sure the dev tooling # is still working diff --git a/airlock/settings.py b/airlock/settings.py index cbcb4a12..3964c806 100644 --- a/airlock/settings.py +++ b/airlock/settings.py @@ -438,3 +438,5 @@ def filter(self, record): # pragma: no cover }, }, } + +SCREENSHOT_DIR = BASE_DIR / "docs" / "screenshots" diff --git a/airlock/templates/file_browser/file.html b/airlock/templates/file_browser/file.html index 5c5969cc..eca18384 100644 --- a/airlock/templates/file_browser/file.html +++ b/airlock/templates/file_browser/file.html @@ -205,6 +205,7 @@ height=1000 style="width: 100%;" sandbox="{{ path_item.iframe_sandbox }} allow-same-origin" + id="content-iframe" > {% /card %} diff --git a/airlock/templates/file_browser/group.html b/airlock/templates/file_browser/group.html index 12659f4a..b1fdbabd 100644 --- a/airlock/templates/file_browser/group.html +++ b/airlock/templates/file_browser/group.html @@ -8,101 +8,102 @@ } {% #card title=group.title container=True class="group_modal" %} -
- {% csrf_token %} -
-
- {% form_textarea field=group.c2_edit_form.context show_placeholder=True resize=True placeholder="Describe the data to be released in this group of files" class="w-full max-w-lg mx-auto" rows=6 disabled=group.c2_readonly readonly=group.c2_readonly %} +
+ + {% csrf_token %} +
+
+ {% form_textarea field=group.c2_edit_form.context show_placeholder=True resize=True placeholder="Describe the data to be released in this group of files" class="w-full max-w-lg mx-auto" rows=6 disabled=group.c2_readonly readonly=group.c2_readonly %} +
+
+ {% form_textarea field=group.c2_edit_form.controls resize=True show_placeholder=True placeholder="Describe the disclosure controls that have been applied to these files" class="w-full max-w-lg mx-auto" rows=6 disabled=group.c2_readonly readonly=group.c2_readonly %} +
-
- {% form_textarea field=group.c2_edit_form.controls resize=True show_placeholder=True placeholder="Describe the disclosure controls that have been applied to these files" class="w-full max-w-lg mx-auto" rows=6 disabled=group.c2_readonly readonly=group.c2_readonly %} -
-
- {% if not group.c2_readonly %} -
- {% #button type="submit" variant="success" id="edit-group-button" disabled=group.c2_readonly %}Save{% /button %} -
- {% endif %} - + {% if not group.c2_readonly %} +
+ {% #button type="submit" variant="success" id="edit-group-button" disabled=group.c2_readonly %}Save{% /button %} +
+ {% endif %} + -
- {% #list_group %} - {% for comment, comment_class in group.comments %} +
+ {% #list_group %} + {% for comment, comment_class in group.comments %} - {% fragment as comment_status %} - - {% if request.user.output_checker %} - {% if release_request.get_turn_phase.name == "INDEPENDENT" and comment.review_turn == release_request.review_turn %} - {% #pill variant="info" text="Blinded" class="group" %} + {% fragment as comment_status %} + + {% if request.user.output_checker %} + {% if release_request.get_turn_phase.name == "INDEPENDENT" and comment.review_turn == release_request.review_turn %} + {% #pill variant="info" text="Blinded" class="group" %} {% comment%} TODO: get tooltips working for pills {% tooltip content=comment.visibility.blinded_description %} {% endcomment%} - {% /pill%} - {% endif %} - {% #pill variant="info" class="group" text=comment.visibility.name.title %} + {% /pill%} + {% endif %} + {% #pill variant="info" class="group" text=comment.visibility.name.title %} {% comment%} TODO: get tooltips working for pills {%tooltip content=comment.visibility.description %} {% endcomment%} - {% /pill%} - {% endif %} - {% pill variant="info" text=comment.created_at|date:"Y-m-d H:i" %} - - {% endfragment %} - {% #list_group_rich_item custom_status=comment_status class=comment_class title=comment.author %} - {{ comment.comment }} - {% if request.user.username == comment.author %} - {% if comment.visibility.name == "PRIVATE" and comment.review_turn == release_request.review_turn %} -
-
- {% csrf_token %} - - {% #button variant="danger" type="cancel" %}Make comment visible to all users{% /button %} -
-
- {% endif %} - {% if group.user_can_comment %} -
-
- {% csrf_token %} - - {% #button variant="danger" type="submit" %}Delete comment{% /button %} -
-
- {% endif %} - {% endif %} - {% /list_group_rich_item %} - {% endfor %} - {% if group.user_can_comment and not group.inline %} - {% #list_group_item %} -
- {% csrf_token %} - {% if request.user.output_checker and release_request.get_turn_phase.name == "INDEPENDENT" %} - {% #alert variant="warning" title="Comments are hidden" dismissible=True %} - Only you will see this comment until two independent reviews have been submitted - {% /alert %} - {% else %} - {% #alert variant="info" title="Comments are pending" no_icon=True %} - Any comments will be shown to other users once you submit or return a request - {% /alert %} + {% /pill%} + {% endif %} + {% pill variant="info" text=comment.created_at|date:"Y-m-d H:i" %} + + {% endfragment %} + {% #list_group_rich_item custom_status=comment_status class=comment_class title=comment.author %} + {{ comment.comment }} + {% if request.user.username == comment.author %} + {% if comment.visibility.name == "PRIVATE" and comment.review_turn == release_request.review_turn %} +
+ + {% csrf_token %} + + {% #button variant="danger" type="submit" %}Make comment visible to all users{% /button %} + +
+ {% endif %} + {% if group.user_can_comment %} +
+
+ {% csrf_token %} + + {% #button variant="danger" type="submit" %}Delete comment{% /button %} +
+
+ {% endif %} {% endif %} + {% /list_group_rich_item %} + {% endfor %} + {% if group.user_can_comment and not group.inline %} + {% #list_group_item %} +
+ {% csrf_token %} + {% if request.user.output_checker and release_request.get_turn_phase.name == "INDEPENDENT" %} + {% #alert variant="warning" title="Comments are hidden" dismissible=True %} + Only you will see this comment until two independent reviews have been submitted + {% /alert %} + {% else %} + {% #alert variant="info" title="Comments are pending" no_icon=True %} + Any comments will be shown to other users once you submit or return a request + {% /alert %} + {% endif %} - {% form_textarea field=group.comment_form.comment placeholder=" " label="Add Comment" show_placeholder=True class="w-full max-w-lg" rows=6 required=False %} - {% if group.comment_form.visibility.field.choices|length == 1 %} - - {% else %} - {% form_radios field=group.comment_form.visibility choices=group.comment_form.visibility.field.choices class="w-full max-w-lg" %} - {% endif%} -
- {% #button type="submit" variant="success" id="edit-comment-button" %}Comment{% /button %} -
-
- {% /list_group_item %} - {% endif %} - {% /list_group %} + {% form_textarea field=group.comment_form.comment placeholder=" " label="Add Comment" show_placeholder=True class="w-full max-w-lg" rows=6 required=False %} + {% if group.comment_form.visibility.field.choices|length == 1 %} + + {% else %} + {% form_radios field=group.comment_form.visibility choices=group.comment_form.visibility.field.choices class="w-full max-w-lg" %} + {% endif%} +
+ {% #button type="submit" variant="success" id="edit-comment-button" %}Comment{% /button %} +
+ + {% /list_group_item %} + {% endif %} + {% /list_group %} +
- {% if not group.inline %} {% include "activity.html" with activity=group.activity %} {% else %} diff --git a/airlock/templates/file_browser/request.html b/airlock/templates/file_browser/request.html index 2bef12cd..8ba9407e 100644 --- a/airlock/templates/file_browser/request.html +++ b/airlock/templates/file_browser/request.html @@ -55,7 +55,7 @@ Please confirm you wish to withdraw this request.
{% #button type="submit" variant="danger" class="action-button" small=True id="withdraw-request-confirm" %}Withdraw{% /button %} - {% #button variant="primary" type="cancel" %}Cancel{% /button %} + {% #button variant="primary" type="cancel" small=True %}Cancel{% /button %} {% /card %} {% /modal %} diff --git a/airlock/templates/login.html b/airlock/templates/login.html index 39cfbaf9..b88be5ff 100644 --- a/airlock/templates/login.html +++ b/airlock/templates/login.html @@ -27,7 +27,7 @@ {{ dev_users_file }} {% /alert %} {% endif %} -
+ {% csrf_token %} {% form_input type="text" field=token_login_form.user required=True label="GitHub username or OpenSAFELY email address" placeholder="opensafely" class="w-full max-w-md" %} diff --git a/docs/airlock-includes/glossary.md b/docs/airlock-includes/glossary.md new file mode 100644 index 00000000..488281bf --- /dev/null +++ b/docs/airlock-includes/glossary.md @@ -0,0 +1,12 @@ +*[release request]: a request to release one or more output files from a workspace to the Jobs site +*[file group]: A collection of files that share the same context and disclosure control information +*[workspace]: a directory of files generated from the jobs that have been run on the Jobs site +*[output file]: A file that has been added to a release request and is to be released +*[supporting file]: A file that has been added to a release request for additional information or context, and will not be released +*[turn]: A stage of the release request process during which the request is considered to be "owned" by either the researcher (author) or the reviewer (output checker). +*[independent review]: Initial blinded review by output checkers +*[consolidation]: Phase after independent review, when output checkers can discuss and consolidate feedback +*[context]: Contextual description of the output files within a file group +*[controls]: Statistical disclosure control measures that have been applied to the files within a file group +*[vote]: An individual output checker's review of a file (i.e. approve/request changed) +*[decision]: The combined decision on the file, based on the submitted reviews from output checkers (approved, request changes, conflicted.) diff --git a/docs/creating-a-release-request.md b/docs/creating-a-release-request.md deleted file mode 100644 index 6a7170b7..00000000 --- a/docs/creating-a-release-request.md +++ /dev/null @@ -1,77 +0,0 @@ -After logging into Airlock, researchers can view files in workspaces that they -have permission to access. - -Researchers populate a release request by adding, and if necessary withdrawing -files. - -## Adding files - -To add a file, researchers select the relevant workspace, navigate to view the file, and -use "Add File to Request". Airlock will ask the researcher to specify the type of -file and the file group that the file should be added to. - -### File types - -When adding a file, researchers should choose one of these two options: - -- **Output** Files of this type contain the data the researcher wishes to be - released. These files will ultimately be released if approved. -- **Supporting** Files of this type contain supplementary data to support the - review of "output" type files in the release request, e.g. the underlying data used to generate a figure. These files will ultimately not be released. - -### File groups - -File groups allow the researcher to group the various files in a release request -into logical groups, in order to help the output checker understand the request. -Supporting files should be placed in the same file group as the Output file they support. - -## Adding context and controls - -Context and controls should be added to each file group. These allow researchers to -provide information about the files requested for release. Files should be organised -into groups that share the same context and controls so that this information only -needs to be provided once per group of files. - -* Context: infomation about what data is contained in the files in the file group. -* Controls: information about what disclosure controls (e.g. rounding/suppression) have been applied. - -To add context and controls to a group, the researcher should navigate to the current -release request and click on the name of the relevant group. This will open a page with -options to enter context and control information. - -## Withdrawing files - -To withdraw a file, researchers select the current release request, navigate to view the file, -and use "Withdraw from Request". - -!!!info "Withdrawing files" - Files can only be withdrawn whilst a release request is in the Pending or Returned states. - If a request is in the Submitted or Reviewed state, it should first be returned to - the author in order to withdraw a file. - Once a request reaches the Approved, Released, Rejected, or Withdrawn states, - files can no longer be withdrawn through this mechanism. If it is necessary - to withdraw a file in this case, please refer to the documentation for - [reporting a data breach](https://docs.opensafely.org/releasing-files/#reporting-a-data-breach). - - -## Updating files - -When a file is added to a release request, a copy is taken of the current contents -of that file. This is deliberate, and ensures that a file added to a request -does not change during the review process. - -If a subsequent job is run within that workspace which changes the file, -the workspace view will show the new file contents and the release request view -will continue to show the old file contents from the time that it was added to -the request. - -To update a file, select the workspace, navigate to view the file, -and use "Add file to request" as usual. This will remove the old version of the file -from the request, and add the new version of the file. - -!!!info "Updating files" - Files can only be updated by the author of the request, and only during the Pending - or Returned states. - This means that if a request is in the Submitted or Reviewed states, the output - checking team must return the request to the author in order for files to be updated. - Updating a file will reset any reviews associated with that file. diff --git a/docs/explanation/index.md b/docs/explanation/index.md new file mode 100644 index 00000000..ad32e952 --- /dev/null +++ b/docs/explanation/index.md @@ -0,0 +1,6 @@ +The explanation provides background knowledge about Airlock and its features. + +* [Why Airlock?](why-airlock.md) +* [How does a workspace file differ from a request file?](workspace-vs-request-files.md) +* [Workflow and permissions](workflow-and-permissions.md) +* [Notifications about events](notifications.md) diff --git a/docs/explanation/notifications.md b/docs/explanation/notifications.md new file mode 100644 index 00000000..cf883270 --- /dev/null +++ b/docs/explanation/notifications.md @@ -0,0 +1,41 @@ +Notifications are sent at various stags during the release request workflow. Notifications may take the form of: + +- Emails to the release request author +- GitHub issues in the relevant output-checking repo +- Slack notifications to the output checking team + + +## Request submitted + +- GitHub issue created in relevant repo +- Slack notification to output checkers + +## Independent review submitted + +- GitHub issue updated +- Slack notification to output checkers + +## Request returned + +- GitHub issue updated +- Author notified by email + +## Request re-submitted + +- GitHub issue updated +- Slack notification to output checkers + +## Request rejected + +- GitHub issue closed +- Author notified by email + +## Request withdrawn + +- GitHub issue closed +- Slack notification to output checkers + +## Files released + +- GitHub issue closed +- Author notified by email diff --git a/docs/explanation/why-airlock.md b/docs/explanation/why-airlock.md new file mode 100644 index 00000000..cdb88d6a --- /dev/null +++ b/docs/explanation/why-airlock.md @@ -0,0 +1,14 @@ +## Security + +Airlock allows us to enforce some safety controls and policies automatically. This includes things such as the number of required reviews, independence of reviews, and ensuring that researchers who are also output checkers are not able to review their own requests. + +## User experience + +Airlock provides a simple way for users to view their workspace files and +build a release request. It reduces the burden on both researchers and output checkers, +and reduces the risk of user error by automating the workflow and enforcing who is +permitted to act on the request at each stage. + +## Audit log + +Airlock logs actions taken by users during the life cycle of the request. diff --git a/docs/explanation/workflow-and-permissions.md b/docs/explanation/workflow-and-permissions.md new file mode 100644 index 00000000..302f8eaa --- /dev/null +++ b/docs/explanation/workflow-and-permissions.md @@ -0,0 +1,104 @@ +## Permission to view workspace files + +Users can view medium privacy outputs from any workspace they have permission to +access (workspaces for which they have the Project Developer role on the Jobs site). + + +## Permission to create a release request + +Users can create a release request from any workspace they have permission to +access. A user can only have at most one active release request at any time. + +## Permission to review a release request + +Only trained output checkers are allowed to review release requests and release +files to the Jobs site. Output checkers who also have access to a workspace may +create a release request for that workspace, but they will not be able to +review it. + +## The release request workflow + +A request moves through a [series of statuses](../reference/request-states.md) during the release request workflow. At each status, the request is considered to +be "owned" by either the researcher (request author) or the output checker. + +During researcher-owned statuses, the researcher can add, withdraw and update +files on a request, and can add or edit context, controls and comments on a file +group. They can also choose to withdraw the request entirely. + +During output checker-owned statuses, output checkers can vote on files, and, +depending on the status of the file votes, return the request to the researcher, +reject it, or release files. They can also add comments and questions on file groups. + +Researchers and output checkers take it in turns to work on the release request. +We refer to the change from a researcher-owned status to an output checker-owned +status (or vice versa) as a new turn. + +For example: a researcher creates a new request and adds files to it. The release request is in status PENDING. It is "owned" by the researcher, who can continue +to edit it. + +The researcher then submits the request. The release request is now in status +SUBMITTED. It is owned by the output checkers. The researcher can no longer add or +withdraw files, or comment on file groups. It is now the turn of the output checkers +to work on the request, reviewing files and asking questions where necessary. + +The RELEASED, REJECTED and WITHDRAWN statuses are considered final states. When a +release request is in one of these statuses, it cannot be edited by any user, and +it cannot be moved into any other status. + +```mermaid +flowchart TD + subgraph Status owner key + direction LR + 1([Author]):::author + 2([Output checker]):::checker + 3(Final status) + end + + subgraph Release Request Workflow + A([PENDING]):::author -- Adds files and submits --> B([SUBMITTED]):::checker + B -- 1st independent review --> C([PARTIALLY_REVIEWED]):::checker + C -- 2nd independent review --> D([REVIEWED]):::checker + D -- Consolidation --> E([RETURNED]):::author + E -- Updates and responds to questions --> B + D -- All files approved ----> F(RELEASED) + D -- Not approved ----> G(REJECTED) + A --> H(WITHDRAWN) + E --> H + end + +classDef author fill:#ff9,stroke:#333; +classDef checker fill:#9ff,stroke:#333; + +``` + +### Withdrawing files + +Files can only be withdrawn by the author of the request and only while the release +request is in the PENDING or RETURNED status. + +If a request is in the Submitted or Reviewed state, it should first be returned to +the author in order to withdraw a file. + +Once a request reaches the Approved, Released, Rejected, or Withdrawn states, +files can no longer be withdrawn through this mechanism. If it is necessary +to withdraw a file in this case, please refer to the documentation for +[reporting a data breach](https://docs.opensafely.org/releasing-files/#reporting-a-data-breach). + + +### Updating files +Files can only be updated by the author of the requestand only while the release +request is in the PENDING or RETURNED status. + +If a request is in the Submitted or Reviewed state, it should first be returned to +the author in order for files to be updated. + +Updating a file will reset any reviews associated with that file. + +## Retrictions for authors +Airlock does not permit users to approve or request changes to files that are part of release requests they created themselves. + +## Downloading restrictions +Please note that only request files (not workspace files) can be downloaded. +Downloading is permitted **only** for the purposes of output checking. +Request authors are not permitted to download files from their own requests, +even if they are also output checkers. diff --git a/docs/explanation/workspace-vs-request-files.md b/docs/explanation/workspace-vs-request-files.md new file mode 100644 index 00000000..e5badeae --- /dev/null +++ b/docs/explanation/workspace-vs-request-files.md @@ -0,0 +1,19 @@ +A **workspace file** is always the latest version of the output file created by +a job run via the Jobs site. + +A **request file** is the version of the output file at the time that it was +added to a release request. + +When a file is added to a release request, a copy is taken of the current contents +of that file. This is deliberate, and ensures that a file added to a request +does not change during the review process. + +If a subsequent job is run within that workspace which changes the file, +the workspace view will show the new file contents and the release request view +will continue to show the old file contents from the time that it was added to +the request. Authors can choose to [update the file](../how-tos/edit-file-on-request.md#update-a-file) +on the release request, but this does not happen automatically. + +After a file has been updated on a release request, it must be reviewed again by +two output checkers, even if the previous version of the file was already +approved. diff --git a/docs/how-tos/access-airlock.md b/docs/how-tos/access-airlock.md new file mode 100644 index 00000000..2105e71b --- /dev/null +++ b/docs/how-tos/access-airlock.md @@ -0,0 +1,41 @@ +!!!warning "Airlock is only supported in Chrome :fontawesome-brands-chrome:" + Please ensure you use Chrome when accessing Airlock. Features + may not work as expected in other browsers. + + +You will use your OpenSAFELY email address or your GitHub username to log in. These are +the same credentials you use to access the Jobs website. + +However, whilst normally you log into https://jobs.opensafely.org from your browser using GitHub, +the secure server does not have access to GitHub. So you need to use an alternate method to login to Airlock, by generating a Single Use Token via the Jobs site, and then using it on the secure +server to log in to Airlock. + + +## Obtain a single use token via the Jobs site + +**Before logging into the secure server**, visit , and click on "Generate a Single Use Token". This will be 3 english words, which you can memorize or write down. This token can be used to log in as you, but is only valid for a short time, and only works once. + +![Generate Single Use Token on Jobs site](../screenshots/manual/job-server-token-form.png) + +![Single Use Token generated on Jobs site](../screenshots/manual/job-server-token.png) + +## Log in to the secure server + +Now that you have your single use token, log into the server via the VPN. + + +## Log in to Airlock + +Navigate to Airlock using Google Chrome. Airlock is accessed at `https://.backends.opensafely.org`. +e.g. on the TPP backend, go to `https://tpp.backends.opensafely.org`. + + +Log in using your GitHub username or OpenSAFELY email and the Single Use Token from the above step. + +![Single Use Token generated on Jobs site](../screenshots/login_form.png) + +You should be now logged in. This login will expire after two weeks of not being used. + +--- + +* Next: [View workspace files](view-workspace-files.md) diff --git a/docs/how-tos/create-and-submit-a-release-request.md b/docs/how-tos/create-and-submit-a-release-request.md new file mode 100644 index 00000000..76800009 --- /dev/null +++ b/docs/how-tos/create-and-submit-a-release-request.md @@ -0,0 +1,78 @@ +Researchers construct a release request by adding, and if necessary, withdrawing +files. + +## Adding files + +While [navigating a workspace](view-workspace-files.md), files can be added to a request in +two ways: + +- individual files can be added while viewing the workspace file +- Multiple files can be added while viewing the directory containing the files + +From a file view, click on the "Add file to Request" button. + +![Add file to request button](../screenshots/add_file_button.png) + +From a directory view, select the files you wish to add and click the "Add files to Request" button. + +![Add files with multiselect](../screenshots/workspace_directory_content.png) + +In the dialogue that opens, specify the file group you wish to add the file(s) to, and +select the type of file (output file or supporting file). + +![Add file form](../screenshots/add_file_modal.png) + +!!! note + You can only have one active release request for a workspace at any one time. If you + already have an active release request, file will be added to it. If you do not have + an active release request, a new one will be created. + +If you added a file that you did not intend to, you can +[withdraw the file](edit-file-on-request.md#withdraw-a-file) prior to submitting the release request. + +## Navigating the request + +After your request has been created, you will see a link to the current request in the +workspace header. Click on this to access the request. + +Alternatively, use the link in the navigation bar to view a list of all your requests. + +Files in a request are organised in a tree structure within each file group, similarly +to workspaces, and can be navigated in the same way. + +![Request tree browser](../screenshots/request_tree.png) + +Output and supporting files are [differentiated in the tree by colour and icon](../reference/file-icons.md), as shown above. + +## Add context and controls + +Context and controls must be added to each file group before the release request +can be submitted. + +To add context and controls to a group, navigate to the current release request and click +on the name of the relevant group. + +![Add context and controls to a file group](../screenshots/context_and_controls.png) + +Enter your context and controls in the text boxes and click Save. + + +## Submit the request + +Navigate to the request overview by clicking the link in the header or by clicking on the +root of the file browser tree. + +Click on the "Submit for review" button. You will need to read and confirm that you have +met the conditions for submission before completing the submission. + +![Submit request button](../screenshots/submit_request.png) + +Your release request's status will transition to "Submitted", and you will no longer be +able to edit it. Output checkers will be [automatically notified](../explanation/notifications.md) that the request is ready for review. + +![Submitted request](../screenshots/submitted_request.png) + +--- + +* Previous: [View workspace files](view-workspace-files.md) +* Next: [Respond to a returned request](respond-to-returned-request.md) diff --git a/docs/how-tos/edit-file-on-request.md b/docs/how-tos/edit-file-on-request.md new file mode 100644 index 00000000..4b773b18 --- /dev/null +++ b/docs/how-tos/edit-file-on-request.md @@ -0,0 +1,62 @@ +!!! info + Files can only be added, updated or withdrawn when the request is in the Pending or Returned + status. See the documentation on [workflow and permissions](../explanation/workflow-and-permissions.md) + for further information. + +## Update a file + +If a workspace file has changed after it has been added to a request, it will +be displayed in the workspace view. In the file browser tree, changed files are +indicated with the colour and icon below: + +![Change file icon](../screenshots/changed_tree_file.png) + +The directory that contains the changed file will also indicate in the "Review State" +metadata that it has been updated. + +![Changed file directory view](../screenshots/multiselect_update.png) + +To update a file, [navigate to the workspace](view-workspace-files.md). +Files can be updated from the file view or the directory view. + +![Changed file view](../screenshots/file_update.png) + + +Use the "Update file(s)" button to update the file with the new content. Note that +in the dialogue that opens, there is no option to +[change the file group](#move-a-file-to-a-different-group) or [file type](#change-a-file-type). + +![File update dialogue](../screenshots/file_update_modal.png) + + +## Withdraw a file + +To withdraw a file, navigate to view the file within the release request, +and use "Withdraw from Request". + +If the request is in Pending status, the file is removed from the request entirely. + +If the request is in Returned status, the file will still be visible on the request, but marked as withdrawn. + +![Withdrawn file](../screenshots/withdrawn_file.png) + +In either case, the file can be [re-added](create-and-submit-a-release-request.md#adding-files) from the workspace view,. + + +## Change a file type + +In order to change a file type (e.g. change a file initially added as an output file to a +supporting file), first [withdraw the file](#withdraw-a-file) from the request and then +[add it again](create-and-submit-a-release-request.md#adding-files) with the new file type. + +## Move a file to a different group + +In order to move a file to a different file group, first [withdraw the file](#withdraw-a-file) +from the request and then [add it again](create-and-submit-a-release-request.md#adding-files) +with the new file group. + + +--- + +* Previous: [Respond to a returned request](respond-to-returned-request.md) +* Next: [Withdraw a release request](withdraw-request.md) diff --git a/docs/how-tos/index.md b/docs/how-tos/index.md new file mode 100644 index 00000000..91041b4c --- /dev/null +++ b/docs/how-tos/index.md @@ -0,0 +1,22 @@ +The how-to guides provide practical steps for working with Airlock. + +* [How to access and log in Airlock](access-airlock.md) + + +For researchers: + +* [View output files in a workspace](view-workspace-files.md) +* [Create and submit a release request](create-and-submit-a-release-request.md) +* [Respond to a returned request](respond-to-returned-request.md) +* [Edit a file on a request](edit-file-on-request.md) + * [Update a file](edit-file-on-request.md#update-a-file) + * [Withdraw a file](edit-file-on-request.md#withdraw-a-file) + * [Change a file's type](edit-file-on-request.md#change-a-file-type) + * [Move a file to a different group](edit-file-on-request.md#move-a-file-to-a-different-group) +* [Withdraw a request](withdraw-request.md) + + +For output checkers: + +* [Review a request](review-a-request.md) +* [Release files](release-files.md) diff --git a/docs/how-tos/release-files.md b/docs/how-tos/release-files.md new file mode 100644 index 00000000..623b74f6 --- /dev/null +++ b/docs/how-tos/release-files.md @@ -0,0 +1,24 @@ +When all output files in a release request have either been approved by two separate +output checkers or withdrawn, the request can be released to the jobs site. + +Navigate to the request overview page; a message will indicate that independent +review has been completed and the files can be released. + +![Ready to release request](../screenshots/ready_to_release.png) + +!!! Note + The option to release files is not enabled until all requested output files in a + release request have been approved. + + +Clicks the "Release Files" button to start the release process. + +The release request [transitions to the "Approved" state](../reference/request-states.md). Once all files have been uploaded to the jobs site, the +release request will move to the "Released" state. + +![Files released](../screenshots/files_released.png) + + +!!!info "Files excluded from release" + Supporting files and withdrawn output files will not be released. + diff --git a/docs/how-tos/respond-to-returned-request.md b/docs/how-tos/respond-to-returned-request.md new file mode 100644 index 00000000..0f9c74bd --- /dev/null +++ b/docs/how-tos/respond-to-returned-request.md @@ -0,0 +1,43 @@ +After output checkers have reviewed a release request, they may have questions, or they may +request changes to some of the output files. They do this by *commenting* on the request, and +then *returning* it to the researcher. You will receive a [notification](../explanation/notifications.md) +by email when a request has been returned to you. + +Access your release request; in the list of requests, you will see it indicated with the +status **Returned**. + +## File review status + +In the file browser view, you can see the reviewed status of each of your output files. + +In the tree view, files are coloured according to status. This request has one approved +file and two files with changes requested: + +![Tree for a returned request](../screenshots/returned_tree.png) + +## Comments + +Navigate to each of your file groups to view comments from the output checkers. You should add +comments to respond to any questions or requests for information. Note that your comments will +not be visible to output checkers until you have submitted the request again for review + +![Tree for a returned request](../screenshots/returned_request_comments.png) + + +## Responding to requests for changes to files + +You can opt to respond to a request to change a file by: + +1. [Withdrawing the file](edit-file-on-request.md#withdraw-a-file) (and potentially adding a different file or files) +2. Re-running a job and [updating the file](edit-file-on-request.md#update-a-file) +3. Leaving the file as is, and adding a comment to indicate why you have chosen to do so. + +## Re-submitting the request + +When you are happy that you have responded to feedback and questions, +[submit the request](create-and-submit-a-release-request.md#submit-the-request) again for review. + + +--- +* Previous: [Create and submit a release request](create-and-submit-a-release-request.md) +* Next: [Edit a file on a request](edit-file-on-request.md) diff --git a/docs/how-tos/review-a-request.md b/docs/how-tos/review-a-request.md new file mode 100644 index 00000000..18b8f8dc --- /dev/null +++ b/docs/how-tos/review-a-request.md @@ -0,0 +1,176 @@ +Two output checkers perform must perform an independent review of +each requested file. + +## View release requests that require action + +Use the navigation bar link to view all requests. + +![Requests index](../screenshots/requests_index.png) + +This page shows you any release requests that you have authored, and any +release requests that are currently active. + +!!! note + If you are not an output checker you will only see requests that you + have authored on this page. + + +### Outstanding requests awaiting review +These are requests that have been submitted and are awaiting or under review. +Each request also shows your progress in reviewing the files. + +### Requests returned for changes/questions +These requests are currently awaiting updates from the researcher, and do +not require any action from you as an output checker. + +Release requests are labelled by workspace name and author. Click on one to +view it. + +## View files requested for release + +The overview page for a release request displays the status of the +request, and some summary details about the files requested for release. + +It also shows a log of recent activity. + +This page also contains buttons for various actions that can be performed +on the request as a whole (submitting your review, returning the request to +the researcher etc.). In the example below, all buttons are disabled, as +this request has not yet been reviewed. + +![Request overview](../screenshots/request_overview.png) + +The files in a release request can be browsed in the file browser tree. The +[highlighted colour and icons](../reference/file-icons.md) illustrate the review state of the files. +This request has 3 output files, none of which have been reviewed yet, and +one supporting file, all in a file group called "my-group". + +![Request file tree](../screenshots/request_tree.png) + +## View context and controls for a file group + +Click on the file group in the tree to view the file group information. + +![File group](../screenshots/file_group.png) + +File group information contains the information about the context and controls +that the researcher has provided for these files. + +## Review files + +### View a file content +To review an individual file, click on the file in the tree to display its +content in the browser. + +![Request file page](../screenshots/file_review.png) + +The `More` dropdown also allows you to [view the file in alternative ways](../reference/view-files-alt.md), or to [view the source code](../reference/view-source-code.md) underlying +the file. You can also [download](../reference/downloading-files.md) if required. + +![More dropdown](../screenshots/more_dropdown_el_request_file.png) + +### View context, controls and comments +The context, controls and comments related to this file's file group can be +viewed from the file page by clicking on the Context button. + +![Context, controls and comments modal](../screenshots/context_modal.png) + +### Vote on a file + +Use the buttons at the top of the file content to submit your vote +for this file. Options are: + +* **Approve** — output meets disclosure requirements and is safe to be released +* **Request Changes** — output is not currently acceptable for release. + +After approving or requesting changes to a file, the +page will display your vote, as well as the overall decision for the +file. You can change or reset your vote in the same way. + +![Request file post-approval](../screenshots/file_approved.png) + +You will see the colours and icons change in the file tree to indicate the +files that you have voted on. + +![Request file post-approval](../screenshots/request_tree_post_voting.png) + + +### Add comments + +Comments can be added to each file group, to ask questions, or +provide information on why changes have been requested to any files. + +To add a comment, navigate to the file group by clicking on it in +the file browser. Enter your comment text and click Save. + +![Comment form](../screenshots/reviewed_request_comment_in_progress.png) + +You can choose to make a comment public and visible to everyone, or +private, visible only to other output checkers. In either case, comments are +hidden from the researcher until the request is returned (or approved/released). + +![Comments on submitted request](../screenshots/reviewed_request_comments.png) + +Comments that are created as private can be updated to public at a later stage. +This can be useful during the consolidation stage, if output checkers agree that +the comment contains a question to the researcher that they would like to ask +without revision. + +### Submit your independent review + +After you have reviewed all files, you must submit your review. Navigate to +the request overview page, and click the Submit review button, which will now +be enabled. + +![Submit independent review](../screenshots/submit_review.png) + +After your review has been submitted, the request status will change. + +![After independent review submitted](../screenshots/submitted_review.png) + + +### Consolidation + +Once two independent reviews have been submitted, the request moves into +"Reviewed" status. At this stage, output checkers are able to see the +combined decision on files, and comments that they have each made, and +can discuss and decide how to proceed with the request. + +At this stage, more comments can be added or the visibility of existing +comments can be changed. Output checkers should determine the set of +comments that they intend to return to the researcher (if any). + +## Progress the request to the next stage + +To progress the request, navigate to the request overview +page. Depending on the status of the file decisions, you will have the options to: + +### Return request to researcher + +If there are questions or changes have been requested, you will need to +return the request to the researcher. Click the Return request button from the +request overview page. + +### Reject request + +On rare occasions, there may be a request that contains data that must not +be released. In this case, you can reject the entire request. + +!!! warning + Rejecting a request cannot be undone. + +### Release files + +If all files have been approved, they can be [released](release-files.md) + +## Re-review of a request + +Once a request has been returned, researchers will receive a +[notification](../explanation/notifications.md), and can make changes, +respond to comments and re-submit the request for re-review. Review +of re-submitted requests follows the same process described above, until +the request is ready for release. + + +--- +* Next: [Release files](release-files.md) diff --git a/docs/how-tos/view-workspace-files.md b/docs/how-tos/view-workspace-files.md new file mode 100644 index 00000000..fbfc4804 --- /dev/null +++ b/docs/how-tos/view-workspace-files.md @@ -0,0 +1,29 @@ +To view files in a workspace, navigate to the Workspaces view using the links in the +navigation bar. This will show you a list of workspaces that you have access to, organised +by Project. + +![Workspaces index](../screenshots/workspaces_index.png) + +Click on a workspace to view its files. The landing page for a workspace shows some details of the workspace and project. On the left hand side the workspace files are displayed in a +browsable file tree. + +![Workspaces view](../screenshots/workspace_directory_view.png) + +Clicking on a directory will display a list of the files it contains, with some metadata +about the file, such as size, type and last modified date. + +To view a specific file, click on it in the directory view, or in the file tree. The contents +of the file will be displayed. + +![Workspace file view](../screenshots/workspace_file_view.png) + +From the file view, the `More` dropdown also allows you to [view the file in alternative ways](../reference/view-files-alt.md), or to [view the source code](../reference/view-source-code.md) underlying +the file. + +![More dropdown](../screenshots/more_dropdown_el.png) + + +--- + +* Previous: [Accessing Airlock](access-airlock.md) +* Next: [Create and submit a release request](create-and-submit-a-release-request.md) diff --git a/docs/how-tos/withdraw-request.md b/docs/how-tos/withdraw-request.md new file mode 100644 index 00000000..9f88cf61 --- /dev/null +++ b/docs/how-tos/withdraw-request.md @@ -0,0 +1,21 @@ +!!! warning + Withdrawing a request cannot be undone. + + +When a [release request is in Pending or Returned status](../explanation/workflow-and-permissions.md), +you have the option to withdraw the request. + +Navigate to the request overview by clicking the link in the header or by clicking on the +root of the file browser tree. + +Click on the "Withdraw request" button. + +![Withdraw request button](../screenshots/withdraw_request.png) + +You will need to confirm that you really want to do this before proceeding. + +![Withdraw request button](../screenshots/withdraw_request_modal.png) + + +--- +* Previous: [Edit a file on a request](edit-file-on-request.md) diff --git a/docs/index.md b/docs/index.md index 2ace9562..c9130776 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,48 +1,53 @@ -!!!warning "Work on Airlock is still in progress" - Airlock is a new tool for managing the output release request - and review process. While Airlock is in initial development stages, - a combination of the current and new (Airlock) process - will be maintained. - - ## What is OpenSAFELY Airlock? Airlock is a service running inside the secure environment to allow researchers to view output files, create release requests, and for output checkers to review and release these requests. -Researchers use the Airlock UI to view their workspace files, create a new release request, add their files to the release and submit them for review. Researchers can only have at most one open release request per workspace, but files can be organised by the researchers into logical groups when constructing the request. +Researchers use the Airlock UI to view their workspace files, create a new release request, add their files to the release request, provide context information and +description of applied statistical disclosure controls, and submit the files for review. -They can then either reject the request as a whole or release the files to the job-server (implicitly approving the request as a whole). +Output checkers review the files requested for release, and approve or request changes for +each one. Once a full review of all files has been completed by two output checkers and all +filees are approved, an output checker can release the files to the Jobs site. -Airlock allows us to enforce some safety controls and policies automatically. This includes things such as the number of required reviews, and ensuring that researchers who are also output checkers are not able to review their own requests. +Airlock allows us to enforce some safety controls and policies automatically. This includes things such as the number of required reviews, independence of reviews, and ensuring that researchers who are also output checkers are not able to review their own requests. ## Accessing Airlock - !!!warning "Airlock is only supported in Chrome :fontawesome-brands-chrome:" Please ensure you use Chrome when accessing Airlock. Features may not work as expected in other browsers. -To access the Airlock system: +To access the Airlock system, you will need to [obtain a single user token via the +Jobs site](https://docs.opensafely.org/jobs-site/#viewing-analysis-outputs-on-the-server), +which you can use to login in Airlock in Chrome in the relevant backend. + +For more details, see the section on [how to access Airlock](how-tos/access-airlock.md) + + +## Workflow and permission + +As a release request progresses through the construction, submission and review stages, the +actions that you are allowed to perform on a request or on files within a request changes. + +For example, a researcher creates and constructs a release request, and then submits it +for review. After submission, the researcher can no longer edit the request. Output checkers +now have access to the submitted request and can review and approve or request changes to +files. They may then return the request to the researcher with comments. At this stage the +request can now be edited by the researcher, and the output checkers no longer have +permission to change their decisions on files or to add comments. -1. [Obtain a Single Use Token via the - Jobs website](https://docs.opensafely.org/jobs-site/#viewing-analysis-outputs-on-the-server). -1. Log into Level 4 and navigate to Airlock in Chrome. Airlock is - accessed at `https://.backends.opensafely.org`. e.g. on - the TPP backend, go to `https://tpp.backends.opensafely.org`. -1. Log in using your GitHub username or email and the single use token. +Refer to the [reference documentation on Airlock workflow and permissions](explanation/workflow-and-permissions.md) for a more detailed description. -(Note: Airlock is not supported on browsers other than Chrome.) +## Using the documentation -## Viewing files +Airlock's documentation has three main sections: -Users can view medium privacy outputs from any workspace they have permission to -access (workspaces for which they have the Project Developer role) via Airlock if: +The [how-to guides](how-tos/index.md) provide practical steps for working with Airlock to create and process release requests. These are additionally subdivided into guides for researchers and guides for output checkers. -* They have access to log into the backend (they must be accessing Airlock - from a browser in the chosen backend) -* The backend has Airlock installed and running. +The [explanation](explanation/index.md) section provides background information about Airlock and its features. +The [reference](reference/index.md) section provides background information about working with Airlock. diff --git a/docs/reference/downloading-files.md b/docs/reference/downloading-files.md new file mode 100644 index 00000000..3725311c --- /dev/null +++ b/docs/reference/downloading-files.md @@ -0,0 +1,15 @@ +!!! note + + Downloading files is restricted to output checkers who are + reviewing a request. This option is not available for workspace + files, or for the author of a release request. + + +If possible, output checkers should not download files, and should +view them in the airlock browser, or via the [available alternative +methods](view-files-alt.md). + +However, if necessary, files on a release request can be downloaded +via the "More" dropdown in the file view. + +![More dropdown](../screenshots/more_dropdown_el_request_file.png) diff --git a/docs/reference/file-icons.md b/docs/reference/file-icons.md new file mode 100644 index 00000000..e86fb81e --- /dev/null +++ b/docs/reference/file-icons.md @@ -0,0 +1,59 @@ +Files in workspaces and requests are identified by different icons +and colours. + +## Workspace files + +A workspace file may: + +* be added to the current request and under review +* have been updated since it was added to the current request +* have already been released (and therefore cannot be added to the request) + +The icons and colours of the possible workspace file states are shown below: + +![Workspace file icons](../screenshots/workspace_file_icons.png) + + +## Release request files + +### During independent review + +During independent review, output checkers see only their own +progress on a request. + + A file on a release request may: + +* be approved by the logged-in output checker +* have changes requested by the logged-in output checker +* be pending review by the logged-in output checker +* have been withdrawn from the request +* be a supporting file (which will not be reviewed or released) + +The icons and colours of the possible request file states at this stage +are shown below: + +![Request file icons during independent review](../screenshots/request_independent_review_file_icons.png) + +During this stage, the researcher will not see the status of the file +reviews, and will see all requested output files as under review: + +![Request file icons during independent review - researcher's view](../screenshots/request_independent_review_researcher_file_icons.png) + + +### After independent review + +After both independent reviews have been submitted, output checkers see the +combined status of files on a request. Once the request has been returned to +the researcher, they will also see the combined status. + +A file on a reviewed release request may: + +* be approved by two independent output checkers +* have changes requested by two independent output checkers +* have conflicting reviews by two independent output checkers +* have been withdrawn from the request +* be a supporting file (which will not be reviewed or released) + +The icons and colours of the possible request file states are shown below: + +![Request file icons after independent review](../screenshots/request_reviewed_file_icons.png) diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 00000000..6a6a6e5a --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,8 @@ +The reference provides background knowledge for working with Airlock. + +* [Terms and definitions](terms-and-definitions.md) +* [File icons](file-icons.md) +* [Alternative ways to view files](view-files-alt.md) +* [Viewing underlying source code](view-source-code.md) +* [Downloading files](downloading-files.md) +* [Request state diagram](request-states.md) diff --git a/docs/reference/request-states.md b/docs/reference/request-states.md new file mode 100644 index 00000000..12d29253 --- /dev/null +++ b/docs/reference/request-states.md @@ -0,0 +1,20 @@ + +# State machine diagram for RequestStatus + +Generated from `BusinessLogicLayer.VALID_STATE_TRANSITIONS` by scripts/statemachine.py + +```mermaid +stateDiagram-v2 + [*] --> PENDING + PENDING --> SUBMITTED + PENDING --> WITHDRAWN + SUBMITTED --> PARTIALLY_REVIEWED + PARTIALLY_REVIEWED --> REVIEWED + REVIEWED --> APPROVED + REVIEWED --> REJECTED + REVIEWED --> RETURNED + RETURNED --> SUBMITTED + RETURNED --> WITHDRAWN + APPROVED --> RELEASED +``` + diff --git a/docs/reference/terms-and-definitions.md b/docs/reference/terms-and-definitions.md new file mode 100644 index 00000000..e089fac9 --- /dev/null +++ b/docs/reference/terms-and-definitions.md @@ -0,0 +1,83 @@ +## Workspace + +On the Jobs site, a workspace is linked to a GitHub repository in the OpenSAFELY organisation. Actions are run within the workspace, and generate outputs as defined in the repo's `project.yaml`. + +On Airlock, a Workspace represents the Jobs site workspace as a directory of files +generated from the jobs that have been run. + + +## Release request + +A term used to refer to a request to release one or more output files from a workspace to the Jobs site. + +Sometimes referred to as just "request". + +A request is also represented in Airlock as a directory of file groups and the files they contain. + + +## Output File + +Files of this type contain the data the researcher wishes to be released. These files will ultimately be released if approved. + + +## Supporting File + +A file that has been added to a release request for additional information or context. Files of this type contain supplementary data to support the review of output files in the release request, e.g. the underlying data used to generate a figure. These files will ultimately not be released. + +## File Group + +A collection of files that share the same context and disclosure control information. + +File groups allow the researcher to group the various files in a release request +into logical groups, in order to help the output checker understand the request. + +Supporting files should be placed in the same file group as the output file they support. + + +## Context +Contextual description of what data is contained in the output files within a file group +explaining e.g.: + +- why these files are requested for release +- variable descriptions +- description and count of the underlying sample of the population +- population size and degrees of freedom for regression outputs +- relationship to other data/tables which through combination may introduce secondary disclosive risks. + + +## Controls + +Description of statistical disclosure control measures (e.g. rounding/suppression) that have been +applied to the files within a file group. + +## Turn + +A stage of the release request process during which the request is considered to be "owned" +by either the researcher (author) or the reviewer (output checker). + +## Vote (on a request file) + +An individual output checker's review of a file. This can be Approved, +Request changes or Undecided. + +## Decision (on a request file) + +The combined decision on a file, taking into account all submitted +reviews from output checkers. This can be Approved or Request Changes +(if output checkers agree) or Conflicted (if output checkers do not +agree.) + +## Independent Review + +Each time a release request is submitted for review, it is initially reviewed independently by two output checkers. At this stage, output checkers are not aware of the status of other +reviews, and cannot see comments made by other output checker. + +## Consolidation + +After output checkers have both completed their independent review, there is a phase of +consolidation, where they can collaborate and determine the questions and feedback that +may be required from the researcher, prior to deciding whether this request is now ready +for release. + +At this stage, any Conflicted decision can be discussed and output +checkers can update their votes. diff --git a/docs/reference/view-files-alt.md b/docs/reference/view-files-alt.md new file mode 100644 index 00000000..4174ba27 --- /dev/null +++ b/docs/reference/view-files-alt.md @@ -0,0 +1,17 @@ +Workspace and request files can be viewed in the Airlock browser. Two alternative ways of viewing a +file are provided via the `More` dropdown in the file view. + +![More dropdown](../screenshots/more_dropdown.png) + + +## View a file in a new tab +Some files, especially tables, may be easier to view in their own window. + +From the file view, use the `More` dropdown to view a file in a new tab. This will open just the +file content, with more space for viewing. + + +## View a file in plain text +Files in the Airlock file viewer are formatted according to their file type. For example, CSV +files are displayed as a filterable and sortable table. If you wish, you can also view the +file content in its raw form, by selecting "View as plain text" in the `More` dropdown. diff --git a/docs/reference/view-source-code.md b/docs/reference/view-source-code.md new file mode 100644 index 00000000..d661edc9 --- /dev/null +++ b/docs/reference/view-source-code.md @@ -0,0 +1,13 @@ +You can view the code underlying a workspace or request file via the `More` dropdown in the file +view. + +![More dropdown](../screenshots/more_dropdown.png) + +Clicking on`View code` opens a new tab with a view of the files in the GitHub repo that the +workspace is connected to. + +![More dropdown](../screenshots/manual/code_view.png) + +!!! note + The version of the repo files corresponds to the state of the repo + *at the commit when the job was run*. diff --git a/docs/releasing.md b/docs/releasing.md deleted file mode 100644 index b1aa9ddb..00000000 --- a/docs/releasing.md +++ /dev/null @@ -1,14 +0,0 @@ -When all output files in a release request have either been approved by two separate -output checkers or withdrawn, the request can be released to the jobs site. - -An output checker navigates to the release request and clicks the "Release Files" -button to start the release process. This button is disabled until all files -in the release request have at least two approvals. The release request transitions -to the "Approved" state. Once all files have been uploaded to the jobs site, the -release request will move to the "Released" state. - -Once a release request has been released, it can no longer be updated. - -!!!info "Files excluded from release" - Supporting files and withdrawn output files will not be released. - diff --git a/docs/requesting-a-review.md b/docs/requesting-a-review.md deleted file mode 100644 index 96b1ca08..00000000 --- a/docs/requesting-a-review.md +++ /dev/null @@ -1,16 +0,0 @@ -## Submitting a request - -Once the researcher has finished working on the release request, the next step is to -submit it for a review by an output checker. Researchers should view the current request -and click "Submit For Review". - -The status of the release request will transition to "Submitted". - -A GitHub issue will be automatically created and output-checkers will be notified -in Slack of the new release request. - - -### Completing the release request form - -After submitting their request on Airlock, researchers complete [this form](https://docs.google.com/document/d/1uWRiFps6tDA2SpxSxf0C2G9mOVWMQ6TQ/edit) -and email it to datarelease@opensafely.org, with the subject "Airlock release request". diff --git a/docs/reviewing.md b/docs/reviewing.md deleted file mode 100644 index 13d691f0..00000000 --- a/docs/reviewing.md +++ /dev/null @@ -1,42 +0,0 @@ -Two output checkers independently review each file and record their decisions using the buttons within Airlock. -Whilst viewing a file in an Airlock release request, they have the option of: - -* **Approve** — output meets disclosure requirements and is safe to be released -* **Request Changes** — output is not currently acceptable for release. - -!!!warning "Retrictions for authors" - Airlock does not permit users to approve or request changes to files that are part of release requests they created themselves. - - -## Viewing release requests on Airlock - -1. After logging into Airlock, output checkers navigate to the Requests list to - view all open requests awaiting review. Note that if an output checker is the - author of a request, they will not see it in the outstanding requests list. -1. Output checkers identify the request by workspace name and requester username. A user - can have at most one active request per workspace. -1. Output checkers can view the files included in the request via the file - browser within Airlock. If required (e.g. in order to perform calculations - in a spreadsheet), output checkers can also downloaded files from Airlock. - -!!! warning "Downloading restrictions" - Please note that only request files (not workspace files) can be downloaded. - Downloading is permitted **only** for the purposes of output checking. - Request authors are not permitted to download files from their own requests, - even if they are also output checkers. - -## Dealing with discrepancies and requested changes to files - - - -Researchers should withdraw or update any files that have had changes requested in -order for their release request to progress. They can add further files, including -revised versions of problematic files, and then ask the output checkers for further -review. - -Files require approval by two output checkers in order to be released. If the two output checkers -do not agree on whether a file should be approved or changes should be requested, -and there is no way to satisfy the output checker who requests changes, it is possible -for a third output checker to supply the second approving review. diff --git a/docs/screenshots/add_file_button.png b/docs/screenshots/add_file_button.png new file mode 100644 index 00000000..6401bd87 Binary files /dev/null and b/docs/screenshots/add_file_button.png differ diff --git a/docs/screenshots/add_file_modal.png b/docs/screenshots/add_file_modal.png new file mode 100644 index 00000000..88f5cf53 Binary files /dev/null and b/docs/screenshots/add_file_modal.png differ diff --git a/docs/screenshots/changed_tree_file.png b/docs/screenshots/changed_tree_file.png new file mode 100644 index 00000000..8b67f2f7 Binary files /dev/null and b/docs/screenshots/changed_tree_file.png differ diff --git a/docs/screenshots/context_and_controls.png b/docs/screenshots/context_and_controls.png new file mode 100644 index 00000000..8fa8977a Binary files /dev/null and b/docs/screenshots/context_and_controls.png differ diff --git a/docs/screenshots/context_modal.png b/docs/screenshots/context_modal.png new file mode 100644 index 00000000..eb159444 Binary files /dev/null and b/docs/screenshots/context_modal.png differ diff --git a/docs/screenshots/file_approved.png b/docs/screenshots/file_approved.png new file mode 100644 index 00000000..fbe337db Binary files /dev/null and b/docs/screenshots/file_approved.png differ diff --git a/docs/screenshots/file_group.png b/docs/screenshots/file_group.png new file mode 100644 index 00000000..29b0b71a Binary files /dev/null and b/docs/screenshots/file_group.png differ diff --git a/docs/screenshots/file_review.png b/docs/screenshots/file_review.png new file mode 100644 index 00000000..7922bafb Binary files /dev/null and b/docs/screenshots/file_review.png differ diff --git a/docs/screenshots/file_update.png b/docs/screenshots/file_update.png new file mode 100644 index 00000000..6d853242 Binary files /dev/null and b/docs/screenshots/file_update.png differ diff --git a/docs/screenshots/file_update_modal copy.png b/docs/screenshots/file_update_modal copy.png new file mode 100644 index 00000000..f000ab64 Binary files /dev/null and b/docs/screenshots/file_update_modal copy.png differ diff --git a/docs/screenshots/file_update_modal.png b/docs/screenshots/file_update_modal.png new file mode 100644 index 00000000..3dc5a820 Binary files /dev/null and b/docs/screenshots/file_update_modal.png differ diff --git a/docs/screenshots/files_released.png b/docs/screenshots/files_released.png new file mode 100644 index 00000000..19385073 Binary files /dev/null and b/docs/screenshots/files_released.png differ diff --git a/docs/screenshots/login_form.png b/docs/screenshots/login_form.png new file mode 100644 index 00000000..775d2e0e Binary files /dev/null and b/docs/screenshots/login_form.png differ diff --git a/docs/screenshots/manual/code_view.png b/docs/screenshots/manual/code_view.png new file mode 100644 index 00000000..45eec42a Binary files /dev/null and b/docs/screenshots/manual/code_view.png differ diff --git a/docs/screenshots/manual/job-server-token-form.png b/docs/screenshots/manual/job-server-token-form.png new file mode 100644 index 00000000..c8c58ed1 Binary files /dev/null and b/docs/screenshots/manual/job-server-token-form.png differ diff --git a/docs/screenshots/manual/job-server-token.png b/docs/screenshots/manual/job-server-token.png new file mode 100644 index 00000000..5ee6d2e0 Binary files /dev/null and b/docs/screenshots/manual/job-server-token.png differ diff --git a/docs/screenshots/more_dropdown.png b/docs/screenshots/more_dropdown.png new file mode 100644 index 00000000..455e0ba1 Binary files /dev/null and b/docs/screenshots/more_dropdown.png differ diff --git a/docs/screenshots/more_dropdown_el.png b/docs/screenshots/more_dropdown_el.png new file mode 100644 index 00000000..8b538976 Binary files /dev/null and b/docs/screenshots/more_dropdown_el.png differ diff --git a/docs/screenshots/more_dropdown_el_request_file.png b/docs/screenshots/more_dropdown_el_request_file.png new file mode 100644 index 00000000..a122dd3f Binary files /dev/null and b/docs/screenshots/more_dropdown_el_request_file.png differ diff --git a/docs/screenshots/multiselect_update copy.png b/docs/screenshots/multiselect_update copy.png new file mode 100644 index 00000000..5ba2d871 Binary files /dev/null and b/docs/screenshots/multiselect_update copy.png differ diff --git a/docs/screenshots/multiselect_update.png b/docs/screenshots/multiselect_update.png new file mode 100644 index 00000000..d0bf49d8 Binary files /dev/null and b/docs/screenshots/multiselect_update.png differ diff --git a/docs/screenshots/ready_to_release.png b/docs/screenshots/ready_to_release.png new file mode 100644 index 00000000..a2613914 Binary files /dev/null and b/docs/screenshots/ready_to_release.png differ diff --git a/docs/screenshots/request_file_icons.png b/docs/screenshots/request_file_icons.png new file mode 100644 index 00000000..bb3411e4 Binary files /dev/null and b/docs/screenshots/request_file_icons.png differ diff --git a/docs/screenshots/request_independent_review_file_icons.png b/docs/screenshots/request_independent_review_file_icons.png new file mode 100644 index 00000000..fc551262 Binary files /dev/null and b/docs/screenshots/request_independent_review_file_icons.png differ diff --git a/docs/screenshots/request_independent_review_researcher_file_icons.png b/docs/screenshots/request_independent_review_researcher_file_icons.png new file mode 100644 index 00000000..add102aa Binary files /dev/null and b/docs/screenshots/request_independent_review_researcher_file_icons.png differ diff --git a/docs/screenshots/request_overview.png b/docs/screenshots/request_overview.png new file mode 100644 index 00000000..a5cc25cd Binary files /dev/null and b/docs/screenshots/request_overview.png differ diff --git a/docs/screenshots/request_reviewed_file_icons.png b/docs/screenshots/request_reviewed_file_icons.png new file mode 100644 index 00000000..4bef39bd Binary files /dev/null and b/docs/screenshots/request_reviewed_file_icons.png differ diff --git a/docs/screenshots/request_tree.png b/docs/screenshots/request_tree.png new file mode 100644 index 00000000..7b8751bf Binary files /dev/null and b/docs/screenshots/request_tree.png differ diff --git a/docs/screenshots/request_tree_post_voting.png b/docs/screenshots/request_tree_post_voting.png new file mode 100644 index 00000000..3cb8bb8a Binary files /dev/null and b/docs/screenshots/request_tree_post_voting.png differ diff --git a/docs/screenshots/requests_index.png b/docs/screenshots/requests_index.png new file mode 100644 index 00000000..b72f72b2 Binary files /dev/null and b/docs/screenshots/requests_index.png differ diff --git a/docs/screenshots/returned_request_comments.png b/docs/screenshots/returned_request_comments.png new file mode 100644 index 00000000..7b519107 Binary files /dev/null and b/docs/screenshots/returned_request_comments.png differ diff --git a/docs/screenshots/returned_tree.png b/docs/screenshots/returned_tree.png new file mode 100644 index 00000000..c887788d Binary files /dev/null and b/docs/screenshots/returned_tree.png differ diff --git a/docs/screenshots/reviewed_request_comment_in_progress.png b/docs/screenshots/reviewed_request_comment_in_progress.png new file mode 100644 index 00000000..7f829a8d Binary files /dev/null and b/docs/screenshots/reviewed_request_comment_in_progress.png differ diff --git a/docs/screenshots/reviewed_request_comments.png b/docs/screenshots/reviewed_request_comments.png new file mode 100644 index 00000000..ea3b44e4 Binary files /dev/null and b/docs/screenshots/reviewed_request_comments.png differ diff --git a/docs/screenshots/submit_request copy.png b/docs/screenshots/submit_request copy.png new file mode 100644 index 00000000..89d995d4 Binary files /dev/null and b/docs/screenshots/submit_request copy.png differ diff --git a/docs/screenshots/submit_request.png b/docs/screenshots/submit_request.png new file mode 100644 index 00000000..ca3eddf5 Binary files /dev/null and b/docs/screenshots/submit_request.png differ diff --git a/docs/screenshots/submit_review.png b/docs/screenshots/submit_review.png new file mode 100644 index 00000000..534bd680 Binary files /dev/null and b/docs/screenshots/submit_review.png differ diff --git a/docs/screenshots/submitted_request.png b/docs/screenshots/submitted_request.png new file mode 100644 index 00000000..809ae04d Binary files /dev/null and b/docs/screenshots/submitted_request.png differ diff --git a/docs/screenshots/submitted_review.png b/docs/screenshots/submitted_review.png new file mode 100644 index 00000000..ba48f6d1 Binary files /dev/null and b/docs/screenshots/submitted_review.png differ diff --git a/docs/screenshots/withdraw_request.png b/docs/screenshots/withdraw_request.png new file mode 100644 index 00000000..fc1d3b21 Binary files /dev/null and b/docs/screenshots/withdraw_request.png differ diff --git a/docs/screenshots/withdraw_request_modal copy.png b/docs/screenshots/withdraw_request_modal copy.png new file mode 100644 index 00000000..b4907d7d Binary files /dev/null and b/docs/screenshots/withdraw_request_modal copy.png differ diff --git a/docs/screenshots/withdraw_request_modal.png b/docs/screenshots/withdraw_request_modal.png new file mode 100644 index 00000000..eecbfe43 Binary files /dev/null and b/docs/screenshots/withdraw_request_modal.png differ diff --git a/docs/screenshots/withdrawn_file.png b/docs/screenshots/withdrawn_file.png new file mode 100644 index 00000000..f0c38891 Binary files /dev/null and b/docs/screenshots/withdrawn_file.png differ diff --git a/docs/screenshots/workspace_directory_content.png b/docs/screenshots/workspace_directory_content.png new file mode 100644 index 00000000..5defa1c9 Binary files /dev/null and b/docs/screenshots/workspace_directory_content.png differ diff --git a/docs/screenshots/workspace_directory_view.png b/docs/screenshots/workspace_directory_view.png new file mode 100644 index 00000000..5bd1429c Binary files /dev/null and b/docs/screenshots/workspace_directory_view.png differ diff --git a/docs/screenshots/workspace_file_icons.png b/docs/screenshots/workspace_file_icons.png new file mode 100644 index 00000000..90fa5a03 Binary files /dev/null and b/docs/screenshots/workspace_file_icons.png differ diff --git a/docs/screenshots/workspace_file_view.png b/docs/screenshots/workspace_file_view.png new file mode 100644 index 00000000..a246c582 Binary files /dev/null and b/docs/screenshots/workspace_file_view.png differ diff --git a/docs/screenshots/workspace_view.png b/docs/screenshots/workspace_view.png new file mode 100644 index 00000000..39be17a7 Binary files /dev/null and b/docs/screenshots/workspace_view.png differ diff --git a/docs/screenshots/workspaces_index.png b/docs/screenshots/workspaces_index.png new file mode 100644 index 00000000..23508216 Binary files /dev/null and b/docs/screenshots/workspaces_index.png differ diff --git a/dotenv-sample b/dotenv-sample index a1153c5f..279e7f45 100644 --- a/dotenv-sample +++ b/dotenv-sample @@ -33,3 +33,6 @@ OTEL_EXPORTER_CONSOLE=False # To send to honecomb in dev, create a token for the development and set it here. # OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=TOKEN34" + +# Uncomment to run the tests that take docs screenshots +# RUN_SCREENSHOT_TESTS=True diff --git a/justfile b/justfile index 5b2e832d..091b0a4a 100644 --- a/justfile +++ b/justfile @@ -110,7 +110,7 @@ check: devenv check "$BIN/djhtml --tabwidth 2 --check airlock/" check "docker run --rm -i ghcr.io/hadolint/hadolint:v2.12.0-alpine < docker/Dockerfile" check "find docker/ airlock/ job-server -name \*.sh -print0 | xargs -0 docker run --rm -v \"$PWD:/mnt\" koalaman/shellcheck:v0.9.0" - check "just state-diagram /tmp/airlock-states.md && diff -u /tmp/airlock-states.md docs/request-states.md" + check "just state-diagram /tmp/airlock-states.md && diff -u /tmp/airlock-states.md docs/reference/request-states.md" if [[ $failed > 0 ]]; then echo -en "\e[1;31m" diff --git a/mkdocs.yml b/mkdocs.yml index 328dc6a1..f4ca59e2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,11 +12,33 @@ docs_dir: docs site_dir: mkdocs_build nav: - - About Airlock: index.md - - Creating a release request: creating-a-release-request.md - - Requesting a review: requesting-a-review.md - - Reviewing the release request: reviewing.md - - Releasing files: releasing.md + - About: index.md + - How-to guides: + - how-tos/index.md + - How to access and log in Airlock: how-tos/access-airlock.md + - For researchers: + - View output files in a workspace: how-tos/view-workspace-files.md + - Create and submit a release request: how-tos/create-and-submit-a-release-request.md + - Respond to a returned request: how-tos/respond-to-returned-request.md + - Edit a file on a request: how-tos/edit-file-on-request.md + - Withdraw a request: how-tos/withdraw-request.md + - For output checkers: + - Review a request: how-tos/review-a-request.md + - Release files: how-tos/release-files.md + - Explanation: + - explanation/index.md + - Why Airlock?: explanation/why-airlock.md + - How does a workspace file differ from a request file?: explanation/workspace-vs-request-files.md + - Workflow and permissions: explanation/workflow-and-permissions.md + - Notifications: explanation/notifications.md + - Reference: + - reference/index.md + - reference/terms-and-definitions.md + - File icons and colours: reference/file-icons.md + - Alternative ways to view files: reference/view-files-alt.md + - Viewing underlying source code: reference/view-source-code.md + - Downloading files: reference/downloading-files.md + - Request state diagram: reference/request-states.md watch: - docs @@ -69,3 +91,9 @@ markdown_extensions: - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.snippets: + check_paths: true + base_path: # base paths will be checked in order for matching snippets + - docs + auto_append: + - airlock-includes/glossary.md diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 3358132c..7d262ad0 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -88,7 +88,7 @@ def researcher_user(live_server, context): "username": "test_researcher", "workspaces": { "test-dir1": { - "project_details": {"name": "Project 1", "ongoing": True}, + "project_details": {"name": "Test Project", "ongoing": True}, "archived": False, } }, @@ -133,7 +133,7 @@ def dev_users(tmp_path, settings): "workspaces": { "test-workspace": { "project_details": { - "name": "Project 1", + "name": "Test Project", "ongoing": True, }, "archived": False, diff --git a/tests/functional/test_code_pages.py b/tests/functional/test_code_pages.py index 8ee24ff5..1b791d66 100644 --- a/tests/functional/test_code_pages.py +++ b/tests/functional/test_code_pages.py @@ -55,6 +55,7 @@ def test_code_from_workspace(live_server, page, context): return_button = new_page.locator("#return-button") expect(new_page.locator("body")).to_contain_text("project.yaml") + expect(return_button).to_be_visible() expect(return_button).to_have_attribute("href", file_url) diff --git a/tests/functional/test_docs_screenshots.py b/tests/functional/test_docs_screenshots.py new file mode 100644 index 00000000..0c3cb532 --- /dev/null +++ b/tests/functional/test_docs_screenshots.py @@ -0,0 +1,573 @@ +import os +import re + +import pytest +from django.conf import settings +from playwright.sync_api import expect + +from airlock.business_logic import bll +from airlock.enums import RequestFileType, RequestStatus +from airlock.types import UrlPath +from tests import factories + +from .conftest import login_as_user +from .utils import screenshot_element_with_padding + + +def get_user_data(): + author_username = "researcher" + author_workspaces = ["my-workspace"] + user_dicts = { + "author": dict( + username=author_username, workspaces=author_workspaces, output_checker=False + ), + "checker1": dict(username="checker1", workspaces=[], output_checker=True), + "checker2": dict(username="checker2", workspaces=[], output_checker=True), + } + + author = factories.create_user( + username=author_username, + workspaces=author_workspaces, + output_checker=False, + ) + + return author, user_dicts + + +@pytest.mark.skipif( + os.getenv("RUN_SCREENSHOT_TESTS") is None, + reason="screenshot tests skipped; set RUN_SCREENSHOT_TESTS env variable", +) +def test_screenshot_from_creation_to_release( + page, live_server, context, release_files_stubber +): + author, user_dicts = get_user_data() + + # set up a workspace with files in a subdirectory + workspace = factories.create_workspace("my-workspace") + + factories.write_workspace_file( + workspace, + "outputs/file1.csv", + "Age Band,Mean\n0-20,10\n21-40,20\n41-60,30\n60+,40", + ) + factories.write_workspace_file( + workspace, + "outputs/file2.csv", + "Variable 1,Variable 2\nA,1\nB,2\nC,3\nD,4", + ) + factories.write_workspace_file( + workspace, + "outputs/summary.txt", + "A summary of the data for output.", + ) + + factories.write_workspace_file( + workspace, + "outputs/supporting.txt", + "The supporting content", + ) + + # Log in as a researcher + login_as_user(live_server, context, user_dicts["author"]) + page.goto(live_server.url) + + # workspaces index page + page.screenshot(path=settings.SCREENSHOT_DIR / "workspaces_index.png") + + # workspace view page + page.goto(live_server.url + workspace.get_url()) + page.screenshot(path=settings.SCREENSHOT_DIR / "workspace_view.png") + + # Directory view + page.goto(live_server.url + workspace.get_url(UrlPath("outputs"))) + # let the data table load + expect(page.locator("#customTable.datatable-table")).to_be_visible() + page.screenshot(path=settings.SCREENSHOT_DIR / "workspace_directory_view.png") + # Content only in directory view + content = page.locator("#selected-contents") + content.screenshot(path=settings.SCREENSHOT_DIR / "workspace_directory_content.png") + + # File view page + page.goto(live_server.url + workspace.get_url(UrlPath("outputs/file1.csv"))) + # wait briefly (100ms) for the table to load before screenshotting + page.wait_for_timeout(100) + page.screenshot(path=settings.SCREENSHOT_DIR / "workspace_file_view.png") + + # More dropdown + more_locator = page.locator("#file-button-more") + more_locator.click() + # Screenshot both the full page and the element; these will be used in different + # places in the docs + page.screenshot(path=settings.SCREENSHOT_DIR / "more_dropdown.png") + screenshot_element_with_padding( + page, + more_locator, + "more_dropdown_el.png", + extra={"x": -180, "width": 180, "height": 120}, + ) + # Close the more dropdown and unfocus + page.keyboard.press("Escape") + more_locator.blur() + + # Add file button + add_file_button = page.locator("button[value=add_files]") + content.screenshot(path=settings.SCREENSHOT_DIR / "add_file_button.png") + screenshot_element_with_padding( + page, content, "add_file_button.png", crop={"height": 0.25} + ) + + # Click to add file and fill in the form with a new group name + add_file_button.click() + page.locator("#id_new_filegroup").fill("my-group") + form_element = page.get_by_role("form") + screenshot_element_with_padding( + page, form_element, "add_file_modal.png", crop={"height": 0.5} + ) + + # create the release request outside of the browser so we can use its methods + # and avoid clicking through all the files to add them + release_request = factories.create_request_at_status( + workspace, + RequestStatus.PENDING, + author, + files=[ + factories.request_file(group="my-group", path="outputs/file1.csv"), + factories.request_file(group="my-group", path="outputs/file2.csv"), + factories.request_file(group="my-group", path="outputs/summary.txt"), + factories.request_file( + group="my-group", + path="outputs/supporting.txt", + filetype=RequestFileType.SUPPORTING, + ), + ], + ) + + page.goto(live_server.url + release_request.get_url(UrlPath("my-group"))) + # screenshot the tree + page.locator("#tree").screenshot(path=settings.SCREENSHOT_DIR / "request_tree.png") + + # Add context & controls to the filegroup + page.screenshot(path=settings.SCREENSHOT_DIR / "context_and_controls.png") + + context_input = page.locator("#id_context") + # context_input.click() + context_input.fill("These files describe data by age band.") + + controls_input = page.locator("#id_controls") + # controls_input.click() + controls_input.fill("Small numbers have been suppressed.") + # Save + page.locator("#edit-group-button").click() + + # Submit request + page.goto(live_server.url + release_request.get_url()) + page.locator("button[data-modal=submitRequest]").click() + page.screenshot(path=settings.SCREENSHOT_DIR / "submit_request.png") + page.locator("#submit-for-review-button").click() + page.screenshot(path=settings.SCREENSHOT_DIR / "submitted_request.png") + + def do_review(screenshot=True): + # Approve file1.csv + page.goto( + live_server.url + + release_request.get_url(UrlPath("my-group/outputs/file1.csv")) + ) + + if screenshot: + # Screenshot the request file page before voting + page.screenshot(path=settings.SCREENSHOT_DIR / "file_review.png") + + page.locator("#file-approve-button").click() + + if screenshot: + # More dropdown (includes download file option) + more_locator = page.locator("#file-button-more") + more_locator.click() + screenshot_element_with_padding( + page, + more_locator, + "more_dropdown_el_request_file.png", + extra={"x": -180, "width": 180, "height": 160}, + ) + + # Click to open the context modal + page.locator("button[data-modal=group-context]").click() + page.screenshot(path=settings.SCREENSHOT_DIR / "context_modal.png") + page.get_by_role("button", name="Close").click() + + if screenshot: + # Screenshot the request file page after voting + page.screenshot(path=settings.SCREENSHOT_DIR / "file_approved.png") + + # Request changes on file2.csv + page.goto( + live_server.url + + release_request.get_url(UrlPath("my-group/outputs/file2.csv")) + ) + + page.locator("#file-request-changes-button").click() + # Request changes on summary.txt + page.goto( + live_server.url + + release_request.get_url(UrlPath("my-group/outputs/summary.txt")) + ) + page.locator("#file-request-changes-button").click() + + if screenshot: + # screenshot the tree after voting + page.locator("#tree").screenshot( + path=settings.SCREENSHOT_DIR / "request_tree_post_voting.png" + ) + + # Submit independent review + page.goto(live_server.url + release_request.get_url()) + if screenshot: + page.screenshot(path=settings.SCREENSHOT_DIR / "submit_review.png") + + page.locator("#submit-review-button").click() + if screenshot: + page.screenshot(path=settings.SCREENSHOT_DIR / "submitted_review.png") + + # Login as output checker and visit pages + login_as_user(live_server, context, user_dicts["checker1"]) + # Requests index + page.goto(f"{live_server.url}/requests") + page.screenshot(path=settings.SCREENSHOT_DIR / "requests_index.png") + # Request view + page.goto(live_server.url + release_request.get_url()) + page.screenshot(path=settings.SCREENSHOT_DIR / "request_overview.png") + # File group + page.goto(live_server.url + release_request.get_url(UrlPath("my-group"))) + page.screenshot(path=settings.SCREENSHOT_DIR / "file_group.png") + + # Review as each output checker + do_review() + login_as_user(live_server, context, user_dicts["checker2"]) + do_review(screenshot=False) + + # Add private comment + page.goto(live_server.url + release_request.get_url(UrlPath("my-group"))) + comment_button = page.get_by_role("button", name=re.compile(r"^Comment")) + comment_input = page.locator("#id_comment") + + comment_input.fill("Please update file2.csv with more descriptive variable names") + page.get_by_test_id("c3").screenshot( + path=settings.SCREENSHOT_DIR / "reviewed_request_comment_in_progress.png" + ) + comment_button.click() + # Add public comment + public_visibility_radio = page.locator("input[name=visibility][value=PUBLIC]") + public_visibility_radio.check() + comment_input.fill("Is summmary.txt required for output?") + comment_button.click() + page.get_by_test_id("c3").screenshot( + path=settings.SCREENSHOT_DIR / "reviewed_request_comments.png" + ) + + # Return to researcher + page.goto(live_server.url + release_request.get_url()) + page.locator("#return-request-button").click() + + # Responding to returned request + login_as_user(live_server, context, user_dicts["author"]) + + # Screenshot tree for file review status + screenshot_element_with_padding(page, page.locator("#tree"), "returned_tree.png") + + # View comments + page.goto(live_server.url + release_request.get_url(UrlPath("my-group"))) + page.get_by_test_id("c3").screenshot( + path=settings.SCREENSHOT_DIR / "returned_request_comments.png" + ) + + # Withdraw a file after request returned + page.goto( + live_server.url + + release_request.get_url(UrlPath("my-group/outputs/summary.txt")) + ) + page.locator("#withdraw-file-button").click() + page.screenshot(path=settings.SCREENSHOT_DIR / "withdrawn_file.png") + + # Update a file after request returned + # change the file on disk + factories.write_workspace_file( + workspace, + "outputs/file2.csv", + contents="Category,Result\nA,1\nB,2\nC,3\nD,4", + ) + # multiselect view + page.goto(live_server.url + workspace.get_url(UrlPath("outputs"))) + page.screenshot(path=settings.SCREENSHOT_DIR / "multiselect_update.png") + # file view + page.goto(live_server.url + workspace.get_url(UrlPath("outputs/file2.csv"))) + # screenshot the tree icon and the page + page.get_by_role("link", name="file2.csv").screenshot( + path=settings.SCREENSHOT_DIR / "changed_tree_file.png" + ) + page.screenshot(path=settings.SCREENSHOT_DIR / "file_update.png") + page.locator("button[value=update_files]").click() + page.screenshot(path=settings.SCREENSHOT_DIR / "file_update_modal.png") + # Click the button to update the file in the release request + page.get_by_role("form").locator("#update-file-button").click() + + # resubmit + page.goto(live_server.url + release_request.get_url()) + page.locator("#submit-for-review-button").click() + + # checker 1 and 2 review, approve and release + def do_review_and_approve(username): + login_as_user(live_server, context, user_dicts[username]) + # file1.csv is already approved, summary.txt has been withdrawn. + # Approve file2.csv + page.goto( + live_server.url + + release_request.get_url(UrlPath("my-group/outputs/file2.csv")) + ) + page.locator("#file-approve-button").click() + # Submit independent review + page.goto(live_server.url + release_request.get_url()) + page.locator("#submit-review-button").click() + + for username in ["checker1", "checker2"]: + do_review_and_approve(username) + + # release + # Mock the responses from job-server + release_request = factories.refresh_release_request(release_request) + release_files_stubber(release_request) + page.goto(live_server.url + release_request.get_url()) + + page.screenshot(path=settings.SCREENSHOT_DIR / "ready_to_release.png") + page.locator("#release-files-button").click() + # Make sure we've waited for the files to be released + expect(page.locator("body")).to_contain_text( + "Files have been released to jobs.opensafely.org" + ) + page.screenshot(path=settings.SCREENSHOT_DIR / "files_released.png") + + +@pytest.mark.skipif( + os.getenv("RUN_SCREENSHOT_TESTS") is None, + reason="screenshot tests skipped; set RUN_SCREENSHOT_TESTS env variable", +) +def test_screenshot_withdraw_request(page, context, live_server): + author, user_dicts = get_user_data() + + release_request = factories.create_request_at_status( + "my-workspace", + author=author, + status=RequestStatus.RETURNED, + files=[factories.request_file(changes_requested=True)], + ) + + # Log in as author + login_as_user(live_server, context, user_dicts["author"]) + + # View submitted request + page.goto(live_server.url + release_request.get_url()) + page.screenshot(path=settings.SCREENSHOT_DIR / "withdraw_request.png") + + page.locator("[data-modal=withdrawRequest]").click() + page.screenshot(path=settings.SCREENSHOT_DIR / "withdraw_request_modal.png") + + page.locator("#withdraw-request-confirm").click() + + +@pytest.mark.skipif( + os.getenv("RUN_SCREENSHOT_TESTS") is None, + reason="screenshot tests skipped; set RUN_SCREENSHOT_TESTS env variable", +) +def test_screenshot_request_partially_reviewed_icons(page, context, live_server): + author, user_dicts = get_user_data() + checker1 = factories.create_user( + username="checker1", + workspaces=[], + output_checker=True, + ) + workspace = factories.create_workspace("my-workspace") + release_request = factories.create_request_at_status( + workspace, + author=author, + status=RequestStatus.SUBMITTED, + files=[ + factories.request_file( + path="approved.txt", + contents="approved", + approved=True, + checkers=[checker1], + ), + factories.request_file( + path="changes_requested.txt", + contents="changes", + changes_requested=True, + checkers=[checker1], + ), + factories.request_file(path="pending_review.txt", contents="pending"), + factories.request_file( + path="withdrawn.txt", + contents="withdrawn", + filetype=RequestFileType.WITHDRAWN, + ), + factories.request_file( + path="supporting.txt", + contents="supporting", + filetype=RequestFileType.SUPPORTING, + ), + ], + ) + + login_as_user(live_server, context, user_dicts["checker1"]) + + # View request + page.goto(live_server.url + release_request.get_url()) + + # screenshot the tree + page.locator("#tree").screenshot( + path=settings.SCREENSHOT_DIR / "request_independent_review_file_icons.png" + ) + + login_as_user(live_server, context, user_dicts["author"]) + + # View request + page.goto(live_server.url + release_request.get_url()) + + # screenshot the tree + page.locator("#tree").screenshot( + path=settings.SCREENSHOT_DIR + / "request_independent_review_researcher_file_icons.png" + ) + + +@pytest.mark.skipif( + os.getenv("RUN_SCREENSHOT_TESTS") is None, + reason="screenshot tests skipped; set RUN_SCREENSHOT_TESTS env variable", +) +def test_screenshot_request_reviewed_icons(page, context, live_server): + author, user_dicts = get_user_data() + checker1 = factories.create_user( + username="checker1", + workspaces=[], + output_checker=True, + ) + checker2 = factories.create_user( + username="checker2", + workspaces=[], + output_checker=True, + ) + workspace = factories.create_workspace("my-workspace") + release_request = factories.create_request_at_status( + workspace, + author=author, + status=RequestStatus.REVIEWED, + files=[ + factories.request_file( + path="approved.txt", + contents="approved", + approved=True, + checkers=[checker1, checker2], + ), + factories.request_file( + path="changes_requested.txt", + contents="changes", + changes_requested=True, + checkers=[checker1, checker2], + ), + factories.request_file( + path="conflicted.txt", + contents="conflicted", + approved=True, + checkers=[checker1, checker2], + ), + factories.request_file( + path="withdrawn.txt", + contents="withdrawn", + filetype=RequestFileType.WITHDRAWN, + ), + factories.request_file( + path="supporting.txt", + contents="supporting", + filetype=RequestFileType.SUPPORTING, + ), + ], + ) + # change one of the votes to changes requested on the conflicted file + conflicted_file = release_request.get_request_file_from_output_path( + "conflicted.txt" + ) + bll.request_changes_to_file(release_request, conflicted_file, checker1) + + login_as_user(live_server, context, user_dicts["checker1"]) + + # View request + page.goto(live_server.url + release_request.get_url()) + + # screenshot the tree + page.locator("#tree").screenshot( + path=settings.SCREENSHOT_DIR / "request_reviewed_file_icons.png" + ) + + +@pytest.mark.skipif( + os.getenv("RUN_SCREENSHOT_TESTS") is None, + reason="screenshot tests skipped; set RUN_SCREENSHOT_TESTS env variable", +) +def test_screenshot_workspace_icons(page, context, live_server): + author, user_dicts = get_user_data() + checker1 = factories.create_user( + username="checker1", + workspaces=[], + output_checker=True, + ) + checker2 = factories.create_user( + username="checker2", + workspaces=[], + output_checker=True, + ) + workspace = factories.create_workspace("my-workspace") + factories.write_workspace_file( + workspace, path="not_added_to_request.txt", contents="not added" + ) + factories.write_workspace_file( + workspace, path="already_released.txt", contents="released" + ) + factories.create_request_at_status( + workspace, + author=author, + status=RequestStatus.RELEASED, + files=[ + factories.request_file( + path="already_released.txt", contents="released", approved=True + ) + ], + ) + factories.create_request_at_status( + workspace, + author=author, + status=RequestStatus.SUBMITTED, + files=[ + factories.request_file(path="added_to_request.txt", contents="approved"), + factories.request_file( + path="updated.txt", + contents="updated", + changes_requested=True, + checkers=[checker1, checker2], + ), + ], + ) + + # update the contents of updated.txt on disk + factories.write_workspace_file( + workspace, + "updated.txt", + contents="new content", + ) + + login_as_user(live_server, context, user_dicts["author"]) + # View workspace + page.goto(live_server.url + workspace.get_url()) + + # screenshot the tree + page.locator("#tree").screenshot( + path=settings.SCREENSHOT_DIR / "workspace_file_icons.png" + ) diff --git a/tests/functional/test_e2e.py b/tests/functional/test_e2e.py index 539a288f..3ef982f5 100644 --- a/tests/functional/test_e2e.py +++ b/tests/functional/test_e2e.py @@ -145,7 +145,8 @@ def test_e2e_release_files( # Add file to request, with custom named group # Find the add file button and click on it to open the modal - find_and_click(page.locator("button[value=add_files]")) + add_file_button = page.locator("button[value=add_files]") + find_and_click(add_file_button) # Fill in the form with a new group name page.locator("#id_new_filegroup").fill("my-new-group") @@ -155,8 +156,10 @@ def test_e2e_release_files( page.locator("input[name=form-0-filetype][value=SUPPORTING]") ).not_to_be_checked() + form_element = page.get_by_role("form") + # Click the button to add the file to a release request - find_and_click(page.get_by_role("form").locator("#add-file-button")) + find_and_click(form_element.locator("#add-file-button")) expect(page).to_have_url( f"{live_server.url}/workspaces/view/test-workspace/subdir/file.txt" @@ -468,9 +471,10 @@ def test_e2e_update_file(page, live_server, dev_users, multiselect): """ # set up a returned file & request author = factories.create_user("researcher", ["test-workspace"], False) + path = "subdir/file.txt" - factories.create_request_at_status( + release_request = factories.create_request_at_status( "test-workspace", author=author, status=RequestStatus.RETURNED, @@ -482,10 +486,12 @@ def test_e2e_update_file(page, live_server, dev_users, multiselect): # Log in as researcher login_as(live_server, page, "researcher") + page.goto(live_server.url + release_request.get_url("default")) + workspace = bll.get_workspace("test-workspace", author) # change the file on disk - factories.write_workspace_file(workspace, path, contents="changed") + factories.write_workspace_file(workspace, path, contents="New file content.") if multiselect: page.goto(live_server.url + workspace.get_url(UrlPath("subdir/"))) @@ -556,6 +562,7 @@ def test_e2e_withdraw_and_readd_file(page, live_server, dev_users): # checkboxes otherwise the wrong thing gets selected expect(page.locator("#customTable.datatable-table")).to_be_visible() find_and_click(page.locator(f'input[name="selected"][value="{path1}"]')) + find_and_click(page.locator("button[value=add_files]")) find_and_click(page.get_by_role("form").locator("#add-file-button")) @@ -613,6 +620,7 @@ def test_e2e_withdraw_request(page, live_server, dev_users): page.goto(live_server.url + release_request.get_url()) find_and_click(page.locator("[data-modal=withdrawRequest]")) + find_and_click(page.locator("#withdraw-request-confirm")) expect(page.locator("body")).to_contain_text("Request has been withdrawn") diff --git a/tests/functional/test_login.py b/tests/functional/test_login.py index 6e1295a5..c1dc247e 100644 --- a/tests/functional/test_login.py +++ b/tests/functional/test_login.py @@ -3,6 +3,7 @@ from playwright.sync_api import expect from .conftest import login_as_user +from .utils import screenshot_element_with_padding @mock.patch("airlock.login_api.session.post", autospec=True) @@ -19,13 +20,20 @@ def test_login(requests_post, settings, page, live_server): page.goto(live_server.url + "/login/?next=/") page.locator("#id_user").fill("test_user") - page.locator("#id_token").fill("foo bar baz") - page.locator("button[type=submit]").click() + page.locator("#id_token").fill("dummy test token") + + # Scroll the button into view before screenshotting the form + submit_button = page.locator("button[type=submit]") + submit_button.scroll_into_view_if_needed() + login_form = page.get_by_test_id("loginform") + screenshot_element_with_padding(page, login_form, "login_form.png") + + submit_button.click() requests_post.assert_called_with( f"{settings.AIRLOCK_API_ENDPOINT}/releases/authenticate", headers={"Authorization": "test_api_token"}, - json={"user": "test_user", "token": "foo bar baz"}, + json={"user": "test_user", "token": "dummy test token"}, ) expect(page).to_have_url(live_server.url + "/workspaces/") diff --git a/tests/functional/test_request_pages.py b/tests/functional/test_request_pages.py index b6058de2..9f76d4a0 100644 --- a/tests/functional/test_request_pages.py +++ b/tests/functional/test_request_pages.py @@ -397,6 +397,7 @@ def test_request_returnable( expect(return_request_button).to_be_enabled() return_request_button.click() expect(return_request_button).not_to_be_visible() + elif login_as == "author": expect(return_request_button).not_to_be_visible() else: diff --git a/tests/functional/utils.py b/tests/functional/utils.py new file mode 100644 index 00000000..37a584d0 --- /dev/null +++ b/tests/functional/utils.py @@ -0,0 +1,39 @@ +from django.conf import settings + + +def screenshot_element_with_padding( + page, element_locator, filename, extra=None, crop=None +): + """ + Take a screenshot with 10px padding around an element. + + Playwright allows screenshotting of a specific element + (with element_locator.screenshot()) but it crops very close and makes + ugly screenshots for including in docs. + + extra: optional dict, to add additional padding around the + element (on top of the default 10px). + + crop: optional dict to crop height and/or width to a percentage of the + original (from top left corner). + """ + box = element_locator.bounding_box() + + clip = { + "x": box["x"] - 10, + "y": box["y"] - 10, + "width": box["width"] + 20, + "height": box["height"] + 20, + } + extra = extra or {} + for key, extra_padding in extra.items(): + clip[key] += extra_padding + + crop = crop or {} + for key, crop in crop.items(): + clip[key] *= crop + + page.screenshot( + path=settings.SCREENSHOT_DIR / filename, + clip=clip, + ) diff --git a/tests/integration/views/test_docs.py b/tests/integration/views/test_docs.py index b39e7ef1..a5ef55e2 100644 --- a/tests/integration/views/test_docs.py +++ b/tests/integration/views/test_docs.py @@ -9,7 +9,9 @@ [ "/docs/", "/docs/index.html", - "/docs/creating-a-release-request/", + "/docs/how-tos/", + "/docs/explanation/", + "/docs/reference/", "/docs/img/favicon.svg", ], )