UPDATE
This commit is contained in:
@@ -1,3 +1,15 @@
|
|||||||
|
try:
|
||||||
|
import pymysql
|
||||||
|
except ImportError: # pragma: no cover - optional fallback when mysqlclient is unavailable
|
||||||
|
pymysql = None
|
||||||
|
else: # pragma: no cover - import side effect
|
||||||
|
# Django 5's MySQL backend checks the mysqlclient version string during import.
|
||||||
|
# PyMySQL exposes a legacy compatibility version, so override it before installing
|
||||||
|
# the MySQLdb shim.
|
||||||
|
pymysql.version_info = (2, 2, 1, "final", 0)
|
||||||
|
pymysql.__version__ = "2.2.1"
|
||||||
|
pymysql.install_as_MySQLdb()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .celery import app as celery_app
|
from .celery import app as celery_app
|
||||||
except ImportError: # pragma: no cover - fallback for test environments
|
except ImportError: # pragma: no cover - fallback for test environments
|
||||||
|
|||||||
+39
-20
@@ -12,7 +12,21 @@ load_dotenv()
|
|||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
LOG_DIR = Path(os.environ.get("LOG_DIR", BASE_DIR / "logs"))
|
LOG_DIR = Path(os.environ.get("LOG_DIR", BASE_DIR / "logs"))
|
||||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
def _can_use_file_logging(log_dir: Path) -> bool:
|
||||||
|
try:
|
||||||
|
log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
probe_file = log_dir / ".write_test"
|
||||||
|
with probe_file.open("a", encoding="utf-8"):
|
||||||
|
pass
|
||||||
|
probe_file.unlink(missing_ok=True)
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
FILE_LOGGING_ENABLED = _can_use_file_logging(LOG_DIR)
|
||||||
|
|
||||||
SECRET_KEY = os.environ.get("SECRET_KEY", "django-insecure-dev-only")
|
SECRET_KEY = os.environ.get("SECRET_KEY", "django-insecure-dev-only")
|
||||||
DEBUG = os.environ.get("DEBUG", "0") == "1"
|
DEBUG = os.environ.get("DEBUG", "0") == "1"
|
||||||
@@ -192,29 +206,34 @@ LOGGING = {
|
|||||||
"class": "logging.StreamHandler",
|
"class": "logging.StreamHandler",
|
||||||
"formatter": "standard",
|
"formatter": "standard",
|
||||||
},
|
},
|
||||||
"file": {
|
},
|
||||||
|
"loggers": {
|
||||||
|
"django": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": os.environ.get("DJANGO_LOG_LEVEL", "INFO"),
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"rag": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": os.environ.get("RAG_LOG_LEVEL", "INFO"),
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": os.environ.get("ROOT_LOG_LEVEL", "INFO"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if FILE_LOGGING_ENABLED:
|
||||||
|
LOGGING["handlers"]["file"] = {
|
||||||
"class": "logging.handlers.TimedRotatingFileHandler",
|
"class": "logging.handlers.TimedRotatingFileHandler",
|
||||||
"filename": str(LOG_DIR / "app.log"),
|
"filename": str(LOG_DIR / "app.log"),
|
||||||
"when": "midnight",
|
"when": "midnight",
|
||||||
"backupCount": 14,
|
"backupCount": 14,
|
||||||
"encoding": "utf-8",
|
"encoding": "utf-8",
|
||||||
"formatter": "standard",
|
"formatter": "standard",
|
||||||
},
|
|
||||||
},
|
|
||||||
"loggers": {
|
|
||||||
"django": {
|
|
||||||
"handlers": ["console", "file"],
|
|
||||||
"level": os.environ.get("DJANGO_LOG_LEVEL", "INFO"),
|
|
||||||
"propagate": False,
|
|
||||||
},
|
|
||||||
"rag": {
|
|
||||||
"handlers": ["console", "file"],
|
|
||||||
"level": os.environ.get("RAG_LOG_LEVEL", "INFO"),
|
|
||||||
"propagate": False,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"handlers": ["console", "file"],
|
|
||||||
"level": os.environ.get("ROOT_LOG_LEVEL", "INFO"),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
LOGGING["loggers"]["django"]["handlers"].append("file")
|
||||||
|
LOGGING["loggers"]["rag"]["handlers"].append("file")
|
||||||
|
LOGGING["root"]["handlers"].append("file")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from types import SimpleNamespace
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from crop_simulation.models import SimulationRun, SimulationScenario
|
from crop_simulation.models import SimulationRun, SimulationScenario
|
||||||
@@ -206,9 +207,11 @@ class ReportingAndAiJourneyTests(IntegrationAPITestCase):
|
|||||||
broken_simulation_service = SimpleNamespace(
|
broken_simulation_service = SimpleNamespace(
|
||||||
get_water_stress=lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("simulation offline"))
|
get_water_stress=lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("simulation offline"))
|
||||||
)
|
)
|
||||||
with patch(
|
crop_simulation_app = apps.get_app_config("crop_simulation")
|
||||||
"irrigation.indicators.apps.get_app_config",
|
with patch.object(
|
||||||
return_value=SimpleNamespace(get_water_stress_service=lambda: broken_simulation_service),
|
crop_simulation_app,
|
||||||
|
"get_water_stress_service",
|
||||||
|
return_value=broken_simulation_service,
|
||||||
):
|
):
|
||||||
water_stress_response = self.client.post(
|
water_stress_response = self.client.post(
|
||||||
"/api/irrigation/water-stress/",
|
"/api/irrigation/water-stress/",
|
||||||
@@ -509,12 +512,22 @@ class ReportingAndAiJourneyTests(IntegrationAPITestCase):
|
|||||||
"supportingMetrics": {"biomass": 12.1},
|
"supportingMetrics": {"biomass": 12.1},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
with patch(
|
crop_simulation_app = apps.get_app_config("crop_simulation")
|
||||||
"crop_simulation.views.apps.get_app_config",
|
with (
|
||||||
return_value=SimpleNamespace(
|
patch.object(
|
||||||
get_current_farm_chart_simulator=lambda: current_chart_service,
|
crop_simulation_app,
|
||||||
get_harvest_prediction_service=lambda: harvest_service,
|
"get_current_farm_chart_simulator",
|
||||||
get_yield_prediction_service=lambda: yield_service,
|
return_value=current_chart_service,
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
crop_simulation_app,
|
||||||
|
"get_harvest_prediction_service",
|
||||||
|
return_value=harvest_service,
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
crop_simulation_app,
|
||||||
|
"get_yield_prediction_service",
|
||||||
|
return_value=yield_service,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
current_chart_response = self.client.post(
|
current_chart_response = self.client.post(
|
||||||
@@ -539,11 +552,15 @@ class ReportingAndAiJourneyTests(IntegrationAPITestCase):
|
|||||||
task_state: dict[str, object] = {}
|
task_state: dict[str, object] = {}
|
||||||
|
|
||||||
def growth_delay_stub(payload):
|
def growth_delay_stub(payload):
|
||||||
|
serializable_payload = {
|
||||||
|
**payload,
|
||||||
|
"farm_uuid": str(payload["farm_uuid"]) if payload.get("farm_uuid") else None,
|
||||||
|
}
|
||||||
scenario = SimulationScenario.objects.create(
|
scenario = SimulationScenario.objects.create(
|
||||||
name=f"integration-{payload['plant_name']}",
|
name=f"integration-{payload['plant_name']}",
|
||||||
scenario_type=SimulationScenario.ScenarioType.SINGLE,
|
scenario_type=SimulationScenario.ScenarioType.SINGLE,
|
||||||
status=SimulationScenario.Status.SUCCESS,
|
status=SimulationScenario.Status.SUCCESS,
|
||||||
input_payload=payload,
|
input_payload=serializable_payload,
|
||||||
result_payload={"engine": "stub"},
|
result_payload={"engine": "stub"},
|
||||||
)
|
)
|
||||||
SimulationRun.objects.create(
|
SimulationRun.objects.create(
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("location_data", "0006_remove_soillocation_ideal_sensor_profile"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="NdviObservation",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("observation_date", models.DateField(db_index=True)),
|
||||||
|
("mean_ndvi", models.FloatField()),
|
||||||
|
("ndvi_map", models.JSONField(blank=True, default=dict)),
|
||||||
|
("vegetation_health_class", models.CharField(max_length=64)),
|
||||||
|
("satellite_source", models.CharField(default="sentinel-2", max_length=64)),
|
||||||
|
("cloud_cover", models.FloatField(blank=True, null=True)),
|
||||||
|
("metadata", models.JSONField(blank=True, default=dict)),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
(
|
||||||
|
"location",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="ndvi_observations",
|
||||||
|
to="location_data.soillocation",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "NDVI Observation",
|
||||||
|
"verbose_name_plural": "NDVI Observations",
|
||||||
|
"db_table": "dashboard_data_ndviobservation",
|
||||||
|
"ordering": ["-observation_date", "-created_at"],
|
||||||
|
"constraints": [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=("location", "observation_date", "satellite_source"),
|
||||||
|
name="ndvi_unique_location_date_source",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user