Skip to content

Commit 552468e

Browse files
authored
Feature/satellite image (#14)
1 parent 03017b3 commit 552468e

16 files changed

Lines changed: 339 additions & 610 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,13 @@ Response is uuid of generated PDF file.
277277
Response is uuid of generated PDF file.
278278

279279
<h2>Pytest</h2>
280-
Pytest can be run on the same machine the service has been deployed to by moving into the tests dir and running:
280+
Pytest can be run on the same machine the service has been deployed to by moving into the app dir and running:
281281

282282
```
283-
pytest tests_.py
283+
pytest
284284
```
285285

286-
This will run the tests and return success values for each api tested in the terminal.
286+
This will run all tests and return success values for each api tested in the terminal.
287287

288288
<h3>These tests will NOT result in generated .pdf files.</h3>
289289

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
[pytest]
22
env_files =
33
testing.env
4+
pythonpath = .
5+
File renamed without changes.

app/tests/unit/__init__.py

Whitespace-only changes.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from unittest import TestCase
2+
from unittest.mock import patch, MagicMock
3+
4+
from fastapi import HTTPException
5+
import os
6+
7+
REQUIRED_ENV_VARS = {
8+
"REPORTING_GATEKEEPER_USERNAME": "mock_user",
9+
"REPORTING_GATEKEEPER_PASSWORD": "mock_pass",
10+
"REPORTING_BACKEND_CORS_ORIGINS": '["*"]',
11+
"REPORTING_POSTGRES_USER": "mock_pg_user",
12+
"REPORTING_POSTGRES_PASSWORD": "mock_pg_pass",
13+
"REPORTING_POSTGRES_DB": "mock_db",
14+
"REPORTING_POSTGRES_HOST": "mock_host",
15+
"REPORTING_POSTGRES_PORT": "5432",
16+
"REPORTING_SERVICE_NAME": "mock_service",
17+
"REPORTING_SERVICE_PORT": "8000",
18+
"REPORTING_GATEKEEPER_BASE_URL": "http://mock.gatekeeper",
19+
"JWT_ACCESS_TOKEN_EXPIRATION_TIME": "3600",
20+
"JWT_SIGNING_KEY": "mock_jwt_secret",
21+
"PDF_DIRECTORY": "/",
22+
"REPORTING_USING_GATEKEEPER": "True"
23+
}
24+
25+
# TestReportAPI Class that will be used for unit test of report endpoints
26+
class TestReportAPI(TestCase):
27+
28+
CORRECT_TOKEN = 'eyJhbGciOiJIUzI1NiJ9.eyJJc3N1ZXIiOiJJc3N1ZXIifQ.HLkw6rgYSwcv0sE69OKiNQFvHoo-6VqlxC5nKuMmftg'
29+
WRONG_TOKEN = "ayJhbGciOiJIUzI1NiJ9.eyJJc3N1ZXIiOiJJc3N1ZXIifQ.HLkw6rgYSwcv0sE69OKiNQFvHoo-6VqlxC5nKuMmftg"
30+
BASE_URL = "/api/v1/openagri-report"
31+
32+
@staticmethod
33+
def user_login(token):
34+
if token == TestReportAPI.CORRECT_TOKEN:
35+
return ""
36+
else:
37+
raise HTTPException(status_code=401, detail='Not Auth!')
38+
39+
40+
def patch(self, obj, attr, value = None):
41+
if value is None:
42+
value = MagicMock()
43+
patcher = patch.object(obj, attr, value)
44+
self.addCleanup(patcher.stop)
45+
return patcher.start()
46+
47+
48+
def setUp(self):
49+
for k, v in REQUIRED_ENV_VARS.items():
50+
os.environ[k] = v
51+
52+
super().setUpClass()
53+
from main import app
54+
from api.api_v1.endpoints import report
55+
from fastapi.testclient import TestClient
56+
from api.deps import get_current_user
57+
58+
app.dependency_overrides[get_current_user] = TestReportAPI.user_login
59+
self.patch(
60+
report,
61+
"decode_jwt_token",
62+
MagicMock(return_value={"user_id": "123"})
63+
)
64+
self.client=TestClient(app)
65+
66+
67+
def test_get_report_endpoint_not_auth(self):
68+
response = self.client.get(f"{TestReportAPI.BASE_URL}/123/", headers={"X-Token": "OK"},
69+
params={"token": TestReportAPI.WRONG_TOKEN})
70+
assert response.status_code == 401
71+
72+
def test_get_report_endpoint_auth_in_progress(self):
73+
response = self.client.get(f"{TestReportAPI.BASE_URL}/123/", headers={"X-Token": "OK"},
74+
params={"token": TestReportAPI.CORRECT_TOKEN})
75+
assert response.status_code == 202
76+
77+
def test_get_report_endpoint_success(self):
78+
from api.api_v1.endpoints import report
79+
from fastapi import Response
80+
self.patch(
81+
report,
82+
"FileResponse",
83+
MagicMock(return_value=Response(content=b"PDF", media_type="application/pdf"))
84+
)
85+
86+
self.patch(
87+
report.os.path,
88+
"exists",
89+
MagicMock(return_value=True)
90+
)
91+
response = self.client.get(f"{TestReportAPI.BASE_URL}/123/", headers={"X-Token": "OK"},
92+
params={"token": TestReportAPI.CORRECT_TOKEN})
93+
assert response.status_code == 200

app/utils/farm_calendar_report.py

Lines changed: 50 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
decode_dates_filters,
1717
get_parcel_info,
1818
get_farm_operation_data,
19-
FarmInfo,
19+
FarmInfo, display_pdf_parcel_details,
2020
)
2121
from utils.json_handler import make_get_request
2222
from geopy.geocoders import Nominatim
@@ -55,7 +55,7 @@ def __init__(
5555

5656

5757
def create_farm_calendar_pdf(
58-
calendar_data: FarmCalendarData, token: dict[str, str]
58+
calendar_data: FarmCalendarData, token: dict[str, str], parcel_id: str | None = None
5959
) -> EX:
6060
"""Create PDF report from farm calendar data"""
6161
pdf = EX()
@@ -85,6 +85,11 @@ def create_farm_calendar_pdf(
8585
pdf.line(pdf.l_margin, y_position, line_end_x, y_position)
8686
pdf.ln(5)
8787

88+
parcel_defined = False
89+
if parcel_id and len(calendar_data.operations) > 1:
90+
display_pdf_parcel_details(pdf, parcel_id, geolocator, token)
91+
parcel_defined = True
92+
8893
if len(calendar_data.operations) == 1:
8994
operation = calendar_data.operations[0]
9095

@@ -175,8 +180,9 @@ def create_farm_calendar_pdf(
175180
ag = operation.responsibleAgent if operation.responsibleAgent else ""
176181
pdf.multi_cell(0, 8, ag, ln=True, fill=True, align="L")
177182

178-
pdf.set_font("FreeSerif", "B", 10)
179-
pdf.cell(40, 8, "Initial Materials:")
183+
pdf.set_font("FreeSerif", "B", 12)
184+
pdf.set_x(15)
185+
pdf.cell(40, 8, "Initial Materials")
180186
pdf.ln(15)
181187
if calendar_data.materials:
182188
with pdf.table(text_align="CENTER", padding=0.5, v_align=VAlign.M) as table:
@@ -204,10 +210,11 @@ def create_farm_calendar_pdf(
204210

205211
if len(calendar_data.operations) > 1:
206212
calendar_data.operations.sort(key=lambda x: x.hasStartDatetime)
213+
pdf.set_font("FreeSerif", "B", 12)
214+
pdf.set_x(15)
215+
pdf.cell(0, 10, "Operations", ln=True, align='L')
216+
pdf.ln(5)
207217
with pdf.table(text_align="CENTER", padding=0.5) as table:
208-
pdf.set_font("FreeSerif", "B", 12)
209-
pdf.cell(0, 10, "Operations", ln=True)
210-
pdf.ln(5)
211218

212219
row = table.row()
213220
pdf.set_font("FreeSerif", "B", 10)
@@ -217,8 +224,9 @@ def create_farm_calendar_pdf(
217224
row.cell("End")
218225
row.cell("Agent")
219226
row.cell("Machinery IDs")
220-
row.cell("Parcel")
221-
row.cell("Farm")
227+
if not parcel_defined:
228+
row.cell("Parcel")
229+
row.cell("Farm")
222230
row.cell("Compost Pile")
223231
row.cell("Responsible Agent")
224232
pdf.set_font("FreeSerif", "", 9)
@@ -275,9 +283,10 @@ def create_farm_calendar_pdf(
275283
address = parcel_data.address
276284

277285
row.cell(f"{machinery_ids}")
278-
row.cell(address)
279-
farm_local = f"Name: {farm.name} | Municipality: {farm.municipality}"
280-
row.cell(farm_local)
286+
if not parcel_defined:
287+
row.cell(address)
288+
farm_local = f"Name: {farm.name} | Municipality: {farm.municipality}"
289+
row.cell(farm_local)
281290
operation = calendar_data.operations[0]
282291
cp = (
283292
operation.isOperatedOn.get("@id").split(":")[3]
@@ -300,6 +309,7 @@ def create_farm_calendar_pdf(
300309
pdf.set_fill_color(0, 255, 255)
301310

302311
pdf.set_font("FreeSerif", "B", 12)
312+
pdf.set_x(15)
303313
pdf.cell(0, 10, "Data Table", ln=True)
304314
pdf.ln(5)
305315
types = {
@@ -440,44 +450,44 @@ def process_farm_calendar_data(
440450
if not operation_id:
441451
# Check for generic response
442452
if calendar_activity_type:
443-
params["name"] = calendar_activity_type
444-
farm_activity_type_info = make_get_request(
445-
url=f'{settings.REPORTING_FARMCALENDAR_BASE_URL}{settings.REPORTING_FARMCALENDAR_URLS["activity_types"]}',
446-
token=token,
447-
params=params,
448-
)
449-
450-
del params["name"]
451-
452-
if farm_activity_type_info:
453-
params["activity_type"] = farm_activity_type_info[0][
454-
"@id"
455-
].split(":")[3]
456-
decode_dates_filters(params, from_date, to_date)
457-
observations = make_get_request(
458-
url=obs_url,
453+
if calendar_activity_type.strip() != "Compost Operation":
454+
params["name"] = calendar_activity_type
455+
farm_activity_type_info = make_get_request(
456+
url=f'{settings.REPORTING_FARMCALENDAR_BASE_URL}{settings.REPORTING_FARMCALENDAR_URLS["activity_types"]}',
459457
token=token,
460458
params=params,
461459
)
462460

461+
del params["name"]
462+
if farm_activity_type_info:
463+
464+
params["activity_type"] = farm_activity_type_info[0][
465+
"@id"
466+
].split(":")[3]
467+
decode_dates_filters(params, from_date, to_date)
468+
observations = make_get_request(
469+
url=obs_url,
470+
token=token,
471+
params=params,
472+
)
473+
else:
463474
if parcel_id:
464475
params["parcel"] = parcel_id
465476
operations = make_get_request(
466477
url=operation_url,
467478
token=token,
468479
params=params,
469480
)
470-
471-
del params["activity_type"]
472-
for o in operations:
473-
id = o["@id"].split(":")[3]
474-
get_farm_operation_data(
475-
id=id,
476-
materials=materials,
477-
params=params,
478-
observations=observations,
479-
token=token,
480-
)
481+
if operations:
482+
for o in operations:
483+
id = o["@id"].split(":")[3]
484+
get_farm_operation_data(
485+
id=id,
486+
materials=materials,
487+
params=params,
488+
observations=observations,
489+
token=token,
490+
)
481491

482492
else:
483493
operation_url = f"{operation_url}{operation_id}/"
@@ -555,7 +565,7 @@ def process_farm_calendar_data(
555565
materials=[],
556566
)
557567

558-
pdf = create_farm_calendar_pdf(calendar_data, token)
568+
pdf = create_farm_calendar_pdf(calendar_data, token, parcel_id)
559569
pdf_dir = f"{settings.PDF_DIRECTORY}{pdf_file_name}"
560570
os.makedirs(os.path.dirname(f"{pdf_dir}.pdf"), exist_ok=True)
561571
pdf.output(f"{pdf_dir}.pdf")

app/utils/generate_aggregation_data.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pandas as pd
55
import matplotlib
6-
6+
from core.config import settings
77
from utils import get_pesticide
88

99
matplotlib.use("Agg")
@@ -14,8 +14,10 @@
1414

1515

1616
def get_pest_from_obj(pt: CropProtectionOperation, token: dict | str):
17-
pest_id = pt.usesPesticide.get("@id", None) if pt.usesPesticide else None
1817
pest_name = ""
18+
if not settings.REPORTING_USING_GATEKEEPER:
19+
return pest_name
20+
pest_id = pt.usesPesticide.get("@id", None) if pt.usesPesticide else None
1921
if pest_id:
2022
pest_id = pest_id.split(":")[3]
2123
pest = get_pesticide(pest_id, token)

0 commit comments

Comments
 (0)