Skip to content

Commit 4220e96

Browse files
authored
Merge pull request #95 from opensafely-core/filegroups-ui
Add support for adding a file to a new or existing file group on a request
2 parents b08b89d + 4104233 commit 4220e96

File tree

6 files changed

+234
-15
lines changed

6 files changed

+234
-15
lines changed

airlock/forms.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from django import forms
2+
3+
4+
class AddFileForm(forms.Form):
5+
filegroup = forms.ChoiceField(required=False)
6+
new_filegroup = forms.CharField(required=False)
7+
8+
def __init__(self, *args, **kwargs):
9+
release_request = kwargs.pop("release_request")
10+
super().__init__(*args, **kwargs)
11+
if release_request:
12+
self.filegroup_names = {group.name for group in release_request.filegroups}
13+
else:
14+
self.filegroup_names = set()
15+
group_choices = {
16+
(name, name) for name in self.filegroup_names if name != "default"
17+
}
18+
group_choices = [("default", "default"), *sorted(group_choices)]
19+
self.fields["filegroup"].choices = group_choices
20+
self.fields["new_filegroup"]
21+
22+
def clean_new_filegroup(self):
23+
new_filegroup = self.cleaned_data.get("new_filegroup").lower()
24+
if new_filegroup in [fg.lower() for fg in self.filegroup_names]:
25+
self.add_error(
26+
"new_filegroup",
27+
f"File group with name '{new_filegroup}' already exists",
28+
)
29+
else:
30+
return new_filegroup

airlock/templates/file_browser/index.html

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{% extends "base.html" %}
22

3+
{% load django_vite %}
4+
35
{% block full_width_content %}
46

57
{% fragment as action_button %}
@@ -65,7 +67,33 @@
6567
{% endfor %}
6668
{% endif %}
6769
{% /list_group %}
68-
{% /card %}
70+
71+
{% #card_footer no_container=False %}
72+
{% if context == "request" %}
73+
<div class="px-4">
74+
{% #button variant="primary-outline" type="button" data-modal="viewFileGroups" %}
75+
View File Groups
76+
{% /button %}
77+
</div>
78+
{% #modal id="viewFileGroups" %}
79+
{% #card container=True title="File Groups for this request" %}
80+
{% for group in release_request.filegroups %}
81+
{{ group.name}}
82+
{% #list_group %}
83+
{% for file in group.files %}
84+
{% #list_group_item %}
85+
{{ file.relpath }}
86+
{% /list_group_item %}
87+
{% endfor %}
88+
{% /list_group %}
89+
{% endfor %}
90+
{% #button variant="secondary" type="cancel" %}Close{% /button %}
91+
{% /card %}
92+
{% /modal %}
93+
{% endif %}
94+
{% /card_footer %}
95+
96+
{% /card %}
6997

7098
</div>
7199

@@ -91,14 +119,25 @@
91119
{% /card %}
92120

93121
{% else %}
94-
95122
{% fragment as add_button %}
96123
{% if context == "workspace" %}
97-
<form action="{{ request_file_url }}" method="POST">
98-
{% csrf_token %}
99-
<input type=hidden name="path" value="{{ path_item.relpath }}"/>
100-
{% #button type="submit" tooltip="Add this file to the current request, starting a new one if needed" variant="success" %}Add File to Request{% /button %}
101-
</form>
124+
{% #button variant="success" type="button" tooltip="Add this file to the current request, starting a new one if needed" data-modal="addRequestFile" id="add-file-button"%}
125+
Add File to Request
126+
{% /button %}
127+
{% #modal id="addRequestFile" %}
128+
{% #card container=True title="Add a file" %}
129+
<form action="{{ request_file_url }}" method="POST">
130+
{% csrf_token %}
131+
{% form_select class="w-full max-w-lg mx-auto" label="Select a file group" field=form.filegroup choices=form.filegroup.field.choices %}
132+
{% form_input class="w-full max-w-lg mx-auto" label="Or create a new file group" field=form.new_filegroup %}
133+
<input type=hidden name="path" value="{{ path_item.relpath }}"/>
134+
<div class="mt-2">
135+
{% #button type="submit" variant="success" %}Add File to Request{% /button %}
136+
{% #button variant="danger" type="cancel" %}Cancel{% /button %}
137+
</div>
138+
</form>
139+
{% /card %}
140+
{% /modal %}
102141
{% elif is_author %}
103142
<form action="" method="POST">
104143
{% csrf_token %}
@@ -133,3 +172,7 @@
133172
</div>
134173

135174
{% endblock full_width_content %}
175+
176+
{% block extra_js %}
177+
{% vite_asset "templates/_components/modal/modal.js" %}
178+
{% endblock %}

airlock/views.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from airlock import login_api
1515
from airlock.api import Status
1616
from airlock.file_browser_api import PathItem
17+
from airlock.forms import AddFileForm
1718
from local_db.api import LocalDBProvider
1819

1920

@@ -141,6 +142,9 @@ def workspace_view(request, workspace_name: str, path: str = ""):
141142
"workspace_add_file",
142143
kwargs={"workspace_name": workspace_name},
143144
),
145+
"form": AddFileForm(
146+
release_request=api.get_current_request(workspace_name, request.user)
147+
),
144148
},
145149
)
146150

@@ -155,14 +159,23 @@ def workspace_add_file_to_request(request, workspace_name):
155159
raise Http404()
156160

157161
release_request = api.get_current_request(workspace_name, request.user, create=True)
162+
form = AddFileForm(request.POST, release_request=release_request)
163+
if not form.is_valid():
164+
for error in form.errors.values():
165+
messages.error(request, error)
166+
return redirect(workspace.get_url_for_path(relpath))
167+
168+
group_name = request.POST.get("new_filegroup") or request.POST.get("filegroup")
158169
try:
159-
api.add_file_to_request(release_request, relpath, request.user)
170+
api.add_file_to_request(release_request, relpath, request.user, group_name)
160171
except api.APIException as err:
161172
# This exception is raised if the file has already been added
162173
# (to any group on the request)
163174
messages.error(request, str(err))
164175
else:
165-
messages.success(request, "File has been added to request")
176+
messages.success(
177+
request, f"File has been added to request (file group '{group_name}')"
178+
)
166179
# redirect to this just added file
167180
return redirect(release_request.get_url_for_path(relpath))
168181

tests/factories.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,7 @@ def write_request_file(request, path, contents=""):
4949

5050
def create_filegroup(release_request, group_name):
5151
return api._get_or_create_filegroupmetadata(release_request.id, group_name)
52+
53+
54+
def refresh_release_request(release_request):
55+
return api.get_release_request(release_request.id)

tests/integration/test_views.py

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
import requests
3+
from django.contrib import messages
34

45
from airlock.api import Status
56
from airlock.users import User
@@ -130,9 +131,9 @@ def test_workspace_request_file_creates(client_with_user, api):
130131
factories.write_workspace_file(workspace, "test/path.txt")
131132

132133
assert api.get_current_request(workspace.name, user) is None
133-
134134
response = client.post(
135-
"/workspaces/add-file-to-request/test1", data={"path": "test/path.txt"}
135+
"/workspaces/add-file-to-request/test1",
136+
data={"path": "test/path.txt", "filegroup": "default"},
136137
)
137138
assert response.status_code == 302
138139

@@ -153,7 +154,8 @@ def test_workspace_request_file_request_already_exists(client_with_user, api):
153154
assert release_request.filegroups == []
154155

155156
response = client.post(
156-
"/workspaces/add-file-to-request/test1", data={"path": "test/path.txt"}
157+
"/workspaces/add-file-to-request/test1",
158+
data={"path": "test/path.txt", "filegroup": "default"},
157159
)
158160
assert response.status_code == 302
159161
current_release_request = api.get_current_request(workspace.name, user)
@@ -164,6 +166,30 @@ def test_workspace_request_file_request_already_exists(client_with_user, api):
164166
assert str(filegroup.files[0].relpath) == "test/path.txt"
165167

166168

169+
def test_workspace_request_file_with_new_filegroup(client_with_user, api):
170+
client = client_with_user({"workspaces": ["test1"]})
171+
user = User.from_session(client.session)
172+
173+
workspace = factories.create_workspace("test1")
174+
factories.write_workspace_file(workspace, "test/path.txt")
175+
176+
assert api.get_current_request(workspace.name, user) is None
177+
response = client.post(
178+
"/workspaces/add-file-to-request/test1",
179+
data={
180+
"path": "test/path.txt",
181+
# new filegroup overrides a selected existing one (or the default)
182+
"filegroup": "default",
183+
"new_filegroup": "new_group",
184+
},
185+
)
186+
assert response.status_code == 302
187+
188+
release_request = api.get_current_request(workspace.name, user)
189+
filegroup = release_request.filegroups[0]
190+
assert filegroup.name == "new_group"
191+
192+
167193
def test_workspace_request_file_filegroup_already_exists(client_with_user, api):
168194
client = client_with_user({"workspaces": ["test1"]})
169195
user = User.from_session(client.session)
@@ -175,14 +201,18 @@ def test_workspace_request_file_filegroup_already_exists(client_with_user, api):
175201
filegroupmetadata = factories.create_filegroup(release_request, "default")
176202
assert not filegroupmetadata.request_files.exists()
177203

178-
client.post("/workspaces/add-file-to-request/test1", data={"path": "test/path.txt"})
204+
client.post(
205+
"/workspaces/add-file-to-request/test1",
206+
data={"path": "test/path.txt", "filegroup": "default"},
207+
)
179208

180209
assert filegroupmetadata.request_files.count() == 1
181210
assert str(filegroupmetadata.request_files.first().relpath) == "test/path.txt"
182211

183212
# Attempt to add the same file again
184213
response = client.post(
185-
"/workspaces/add-file-to-request/test1", data={"path": "test/path.txt"}
214+
"/workspaces/add-file-to-request/test1",
215+
data={"path": "test/path.txt", "filegroup": "default"},
186216
)
187217
assert response.status_code == 302
188218
# No new file created
@@ -195,12 +225,44 @@ def test_workspace_request_file_request_path_does_not_exist(client_with_user):
195225
factories.create_workspace("test1")
196226

197227
response = client.post(
198-
"/workspaces/add-file-to-request/test1", data={"path": "test/path.txt"}
228+
"/workspaces/add-file-to-request/test1",
229+
data={"path": "test/path.txt", "filegroup": "default"},
199230
)
200231

201232
assert response.status_code == 404
202233

203234

235+
def test_workspace_request_file_invalid_new_filegroup(client_with_user, api):
236+
client = client_with_user({"workspaces": ["test1"]})
237+
user = User.from_session(client.session)
238+
239+
workspace = factories.create_workspace("test1")
240+
factories.write_workspace_file(workspace, "test/path.txt")
241+
242+
release_request = factories.create_release_request(workspace, user)
243+
filegroupmetadata = factories.create_filegroup(release_request, "test_group")
244+
245+
response = client.post(
246+
"/workspaces/add-file-to-request/test1",
247+
data={
248+
"path": "test/path.txt",
249+
"filegroup": "default",
250+
"new_filegroup": "test_group",
251+
},
252+
follow=True,
253+
)
254+
255+
assert not filegroupmetadata.request_files.exists()
256+
# redirects to the workspace file again, with error messages
257+
assert response.request["PATH_INFO"] == workspace.get_url_for_path("test/path.txt")
258+
259+
all_messages = [msg for msg in response.context["messages"]]
260+
assert len(all_messages) == 1
261+
message = all_messages[0]
262+
assert message.level == messages.ERROR
263+
assert "already exists" in message.message
264+
265+
204266
def test_request_index_no_user(client):
205267
release_request = factories.create_release_request("workspace")
206268
response = client.get(f"/requests/view/{release_request.id}/")

tests/unit/test_forms.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import pytest
2+
3+
from airlock.forms import AddFileForm
4+
from tests import factories
5+
6+
7+
pytestmark = pytest.mark.django_db
8+
9+
10+
def test_add_file_form_no_release_request():
11+
form = AddFileForm(release_request=None)
12+
assert form.fields["filegroup"].choices == [("default", "default")]
13+
14+
15+
def test_add_file_form_empty_release_request():
16+
release_request = factories.create_release_request("workspace")
17+
form = AddFileForm(release_request=release_request)
18+
assert form.fields["filegroup"].choices == [("default", "default")]
19+
20+
21+
def test_add_file_form_filegroup_choices():
22+
release_request = factories.create_release_request("workspace")
23+
for group in ["b_group", "a_group"]:
24+
factories.create_filegroup(release_request, group)
25+
release_request = factories.refresh_release_request(release_request)
26+
27+
other_release_request = factories.create_release_request("workspace1")
28+
factories.create_filegroup(other_release_request, "other_group")
29+
30+
form = AddFileForm(release_request=release_request)
31+
# default group is always first, other choices are sorted
32+
assert form.fields["filegroup"].choices == [
33+
("default", "default"),
34+
("a_group", "a_group"),
35+
("b_group", "b_group"),
36+
]
37+
38+
39+
@pytest.mark.parametrize(
40+
"new_group_name,is_valid",
41+
[
42+
# Can create a new default group if one doesn't already exist
43+
("default", True),
44+
# Can't create a duplicate group
45+
("test", False),
46+
# Can't create a duplicate group, case insensitive
47+
("Test", False),
48+
# Can create a group with the same name as a group on another request
49+
("test 1", True),
50+
],
51+
)
52+
def test_add_file_form_new_filegroup(new_group_name, is_valid):
53+
release_request = factories.create_release_request("workspace")
54+
factories.create_filegroup(release_request, "test")
55+
release_request = factories.refresh_release_request(release_request)
56+
57+
other_release_request = factories.create_release_request("workspace1")
58+
factories.create_filegroup(other_release_request, "test 1")
59+
60+
data = {
61+
# a filegroup is always in the POST data, ignored if
62+
# new_filegroup also present
63+
"filegroup": "default",
64+
"new_filegroup": new_group_name,
65+
}
66+
form = AddFileForm(data, release_request=release_request)
67+
assert form.is_valid() == is_valid

0 commit comments

Comments
 (0)