Skip to content
This repository was archived by the owner on Aug 10, 2024. It is now read-only.

Commit f726f33

Browse files
authored
Requirements Patches, Workflow Concurrency, Error Messaging, and Test Coverage (#346)
* Add acceptance, production to deployment workflow * Install libcairo2-dev in dev container * Comment runtime and requirements * Revert "Add acceptance, production to deployment workflow" This reverts commit aadc44a. * Apply no-op requirement changes * Update python-dateutil to 2.8.2 * Update simplejson to 3.17.6 * Update django-admin-rangefilter to 0.8.8 * Update gunicorn to 20.1.0 * Update to psycopg2-binary 2.8.6 * Fix note about py * Update to python-decouple 3.4 * Only run initdb on an empty directory * Add test_poll_state * Fix formatting * Refactor deployment workflow * Add detailed error messages to views * Refactor _poll_state_response * Add err_msg parameter to error view * Refactor poll_state * Use valid PENDING state to updated percent * Revert "Use valid PENDING state to updated percent" This reverts commit 4c6df8c. * Revert "Refactor poll_state" This reverts commit e7287fd. * Fix line length * Revert "Install libcairo2-dev in dev container" This reverts commit f183dcf. * Reapply "Install libcairo2-dev in dev container" This reverts commit 0fe85ea.
1 parent eaa6e53 commit f726f33

File tree

9 files changed

+181
-69
lines changed

9 files changed

+181
-69
lines changed

.devcontainer/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM mcr.microsoft.com/devcontainers/python:3.9
22

33
RUN apt-get update
4-
RUN apt-get --yes install rabbitmq-server
4+
RUN apt-get --yes install libcairo2-dev rabbitmq-server
55
RUN mkdir --parents /usr/local/var/postgres
66
RUN chown vscode:vscode /usr/local/var/postgres

.github/workflows/deployment.yml

+41-7
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,45 @@ permissions:
88
contents: read
99

1010
jobs:
11-
commit:
11+
commit-format:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout the Git repository
16+
uses: actions/checkout@v3
17+
- name: Set up Python 3.9
18+
uses: actions/setup-python@v3
19+
with:
20+
python-version: "3.9"
21+
cache: "pip"
22+
cache-dependency-path: ./requirements-dev.txt
23+
- name: Install dependencies
24+
run: |
25+
make env-python
26+
make install-dev
27+
- name: Format with autopep8
28+
run: make format-check
29+
30+
commit-lint:
31+
runs-on: ubuntu-latest
32+
33+
steps:
34+
- name: Checkout the Git repository
35+
uses: actions/checkout@v3
36+
- name: Set up Python 3.9
37+
uses: actions/setup-python@v3
38+
with:
39+
python-version: "3.9"
40+
cache: "pip"
41+
cache-dependency-path: ./requirements-dev.txt
42+
- name: Install dependencies
43+
run: |
44+
make env-python
45+
make install-dev
46+
- name: Lint with ruff
47+
run: make lint-check
48+
49+
commit-test:
1250
runs-on: ubuntu-latest
1351

1452
env:
@@ -58,13 +96,9 @@ jobs:
5896
./requirements.txt
5997
- name: Install dependencies
6098
run: |
99+
make env-python
61100
make install
62-
- name: Format with autopep8
63-
run: |
64-
make format-check
65-
- name: Lint with ruff
66-
run: |
67-
make lint-check
101+
make install-dev
68102
- name: Test with unittest
69103
run: |
70104
make celery &

Makefile

+12-4
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ heroku:
1818

1919
.PHONY: install
2020
install:
21-
python3 -m venv venv
22-
pip install -U pip
2321
pip install -r requirements.txt
24-
pip install -r requirements-dev.txt
2522
make migrate
2623
make groups
2724
make static
2825

26+
.PHONY: install-dev
27+
install-dev:
28+
pip install -r requirements-dev.txt
29+
2930
.PHONY: static
3031
static:
3132
python3 manage.py collectstatic -c --no-input
@@ -45,6 +46,11 @@ else
4546
endif
4647
@echo "RabbitMQ Status: Online"
4748

49+
.PHONY: env-python
50+
env-python:
51+
python3 -m venv venv
52+
pip install -U pip
53+
4854
.PHONY: stopenv
4955
stopenv:
5056
ifeq ($(USER),vscode)
@@ -75,12 +81,14 @@ groups:
7581

7682
.PHONY: codespace
7783
codespace:
78-
initdb /usr/local/var/postgres
84+
find /usr/local/var/postgres -maxdepth 0 -empty -exec initdb {} \;
7985
sudo cp ./rabbitmq-devcontainer.conf /etc/rabbitmq/rabbitmq.conf
8086
make .env
8187
make .git/hooks/pre-commit
8288
make env
89+
make env-python
8390
make install
91+
make install-dev
8492
nohup bash -c 'make celery &'
8593

8694
.PHONY: test

app/admin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.forms import Textarea
55
from django.http import HttpRequest, HttpResponseRedirect
66
from django.utils import timezone as tz
7-
from rangefilter.filter import DateRangeFilter
7+
from rangefilter.filters import DateRangeFilter
88

99
from app.constants.str import (
1010
EMPTY_DONATION,

app/templates/app/PollState.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
}
6565

6666
function setErrorPath() {
67-
window.location.href = "/error";
67+
window.location.href = "/error?err_msg=Failed%20to%20process%20the%20file.";
6868
}
6969

7070
function poll() {

app/views/test_views.py

+43-11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
download_receipt,
1111
export_csv,
1212
import_csv,
13+
poll_state,
1314
)
1415

1516

@@ -69,30 +70,61 @@ def test_export_csv_post(self) -> None:
6970
self.assertEqual(first=response.status_code, second=302)
7071

7172
def test_download_receipt(self) -> None:
72-
request = self.request_factory.post(path="")
73-
request.user = self.user
74-
request.queryset = {}
73+
download_receipt_request = self.request_factory.post(path="")
74+
download_receipt_request.user = self.user
75+
download_receipt_request.queryset = {}
7576

76-
post_response = download_receipt(request=request)
77+
download_receipt_response = download_receipt(
78+
request=download_receipt_request)
7779

78-
response = self.client.get(path=post_response.url)
80+
get_receipt_response = self.client.get(
81+
path=download_receipt_response.url)
7982

80-
self.assertContains(response=response,
83+
self.assertContains(response=get_receipt_response,
8184
text="""<div class="progress">
8285
<div class="bar" role="progressbar"></div>
8386
</div>""",
8487
status_code=200, html=True)
8588

86-
location = post_response.get(header="Location")
89+
location = download_receipt_response.get(header="Location")
8790
parsed_url = urlparse(url=location)
8891
query = parse_qs(qs=parsed_url.query)
8992
task_id = query["job"]
90-
request = self.request_factory.get(path="", data={"task_id": task_id})
91-
request.user = self.user
93+
poll_state_request = self.request_factory.post(
94+
path="", data={"task_id": task_id})
95+
poll_state_request.user = self.user
96+
97+
poll_state_response = poll_state(request=poll_state_request)
98+
self.assertContains(
99+
response=poll_state_response,
100+
text="SUCCESS",
101+
status_code=200,
102+
html=True)
92103

93-
response = download_file(request=request)
94-
content_type = response["Content-Type"]
104+
download_file_request = self.request_factory.get(
105+
path="", data={"task_id": task_id})
106+
download_file_request.user = self.user
107+
108+
download_file_response = download_file(request=download_file_request)
109+
content_type = download_file_response["Content-Type"]
95110

96111
self.assertEqual(first=content_type, second="application/zip", msg=(
97112
"The content type of the receipt response was unexpected. "
98113
"Is Celery running with a results backend enabled?"))
114+
115+
def test_poll_state(self) -> None:
116+
request = self.request_factory.post(
117+
path="", data={"task_id": "test-task-id"})
118+
request.user = self.user
119+
120+
response = poll_state(request=request)
121+
122+
print(response.content)
123+
self.assertEqual(first=response.status_code, second=200)
124+
self.assertJSONEqual(
125+
raw=response.content,
126+
expected_data={
127+
"state": "PENDING",
128+
"process_percent": 0,
129+
"status": 200},
130+
)

app/views/views.py

+46-36
Original file line numberDiff line numberDiff line change
@@ -36,58 +36,58 @@ def get_analytics(request: HttpRequest):
3636
return render(request, "app/analytics.html", _context("Analytics"))
3737

3838

39-
def import_view_template(request, importer, filetype, required_permission):
40-
"""A importer view template
39+
@require_http_methods(["GET", "POST"])
40+
@login_required(login_url="/login")
41+
def import_csv(request: HttpRequest):
42+
"""A view to redirect after task queuing csv importer
4143
"""
42-
if not request.user.has_perm(required_permission):
43-
return _error(request, PERMISSION_DENIED)
44+
filetype = ".csv"
45+
46+
if not request.user.has_perm("app.can_import_historical"):
47+
return _error(request=request, err_msg=PERMISSION_DENIED)
4448

4549
res = HttpResponseRedirect("/")
4650

4751
if request.method == "GET":
48-
if "job" in request.GET:
49-
res = _poll_state_response(request, "import_csv")
50-
elif request.method == "POST":
52+
res = _poll_state_response(request, "import_csv")
53+
# POST is the only other valid method
54+
else:
5155
uploaded_file = request.FILES.get("uploaded_file", None)
5256
if uploaded_file and uploaded_file.name.endswith(filetype):
5357
raw_file = uploaded_file.read()
5458
decoded_file = str(raw_file, 'utf-8-sig',
5559
errors='ignore').splitlines()
56-
job = importer.s(decoded_file).delay()
60+
job = historical_data_importer.s(decoded_file).delay()
5761
res = HttpResponseRedirect(f"{reverse('import_csv')}?job={job.id}")
5862
else:
59-
res = _error(request)
60-
return res
61-
63+
res = _error(
64+
request=request,
65+
err_msg="Uploaded file {uploaded_file.name} is not a "
66+
"{filetype} file.")
6267

63-
@require_http_methods(["GET", "POST"])
64-
@login_required(login_url="/login")
65-
def import_csv(request: HttpRequest):
66-
"""A view to redirect after task queuing csv importer
67-
"""
68-
return import_view_template(
69-
request, historical_data_importer, ".csv", "app.can_import_historical")
68+
return res
7069

7170

7271
@require_http_methods(["GET", "POST"])
7372
@login_required(login_url="/login")
7473
def export_csv(request: HttpRequest):
7574
"""Queue CSV exporter then redirect to poll state"""
7675
if not request.user.has_perm('app.can_export_data'):
77-
return _error(request, PERMISSION_DENIED)
76+
return _error(request=request, err_msg=PERMISSION_DENIED)
7877

7978
res = HttpResponseRedirect("/")
8079

8180
if request.method == "GET":
82-
if "job" in request.GET:
83-
return _poll_state_response(request, "export_csv")
84-
elif request.method == "POST":
81+
res = _poll_state_response(request, "export_csv")
82+
# POST is the only other valid method
83+
else:
8584
export_name = request.POST.get("export_name", "export")
8685
queryset = request.queryset if hasattr(request, 'queryset') \
8786
else Item.objects.all()
8887
rows = serializers.serialize("json", queryset)
8988
job = exporter.s(export_name, rows, len(queryset)).delay()
9089
res = HttpResponseRedirect(f"{reverse('export_csv')}?job={job.id}")
90+
9191
return res
9292

9393

@@ -98,14 +98,12 @@ def download_receipt(request: HttpRequest):
9898
Takes request from admin which contains request.queryset
9999
"""
100100
if not request.user.has_perm('app.generate_tax_receipt'):
101-
return _error(request, PERMISSION_DENIED)
102-
103-
res = _error(request)
101+
return _error(request=request, err_msg=PERMISSION_DENIED)
104102

105103
if request.method == "GET":
106-
if "job" in request.GET:
107-
res = _poll_state_response(request, "download_receipt")
108-
elif request.method == "POST":
104+
res = _poll_state_response(request, "download_receipt")
105+
# POST is the only other valid method
106+
else:
109107
queryset = serializers.serialize("json", request.queryset)
110108
job = receiptor.s(queryset, len(request.queryset)).delay()
111109
res = HttpResponseRedirect(
@@ -119,7 +117,9 @@ def poll_state(request: HttpRequest):
119117
"""A view to report the progress to the user"""
120118
task_id = request.POST.get("task_id", None)
121119
if task_id is None:
122-
return _error(request)
120+
return _error(
121+
request=request,
122+
err_msg="The task_id query parameter of the request was omitted.")
123123

124124
task = AsyncResult(task_id)
125125
res = JsonResponse(_poll_state(PENDING, 0, 200))
@@ -153,15 +153,19 @@ def download_file(request: HttpRequest):
153153
except TimeoutError:
154154
print(f"{task} {task_name} failed #{attempts}")
155155
if (attempts >= ATTEMPT_LIMIT):
156-
return _error(request, "Download exceeded max attempts")
156+
return _error(
157+
request=request,
158+
err_msg="Download exceeded max attempts")
157159
return result
158160
except Exception as e:
159-
return _error(request, "Something went wrong.", e)
161+
return _error(request=request, err_msg=f"Failed to download file: {e}")
160162

161163

162-
def error(request):
164+
def error(request: HttpRequest):
163165
"""Error page"""
164-
return _error(request)
166+
err_msg = request.GET.get("err_msg", "Something went wrong.")
167+
168+
return _error(request=request, err_msg=err_msg)
165169

166170

167171
"""
@@ -170,10 +174,17 @@ def error(request):
170174

171175

172176
def _poll_state_response(request: HttpRequest, task_name):
177+
job = request.GET.get("job", None)
178+
if job is None:
179+
return _error(
180+
request=request,
181+
err_msg="The job query parameter of the request was omitted.")
182+
173183
context = _context("Poll State", {
174-
"task_id": request.GET["job"],
184+
"task_id": job,
175185
"task_name": task_name
176186
})
187+
177188
return render(request, "app/PollState.html", context)
178189

179190

@@ -186,8 +197,7 @@ def _context(title, override={}):
186197
return context
187198

188199

189-
def _error(request: HttpRequest, err_msg="Something went wrong.", e=None):
190-
# logger.exception(e)
200+
def _error(request: HttpRequest, err_msg="Something went wrong."):
191201
return render(request, "app/error.html", _context(err_msg))
192202

193203

0 commit comments

Comments
 (0)