Skip to content

Commit

Permalink
Add farm group (#155)
Browse files Browse the repository at this point in the history
* Add farm group

* Fix tests
  • Loading branch information
meomancer committed Sep 20, 2024
1 parent 26d8b57 commit 38960bc
Show file tree
Hide file tree
Showing 13 changed files with 432 additions and 60 deletions.
2 changes: 1 addition & 1 deletion django_project/_version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.1
0.0.2
7 changes: 1 addition & 6 deletions django_project/gap/admin/crop_insight.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,12 @@ class CropInsightRequestAdmin(admin.ModelAdmin):
"""Admin for CropInsightRequest."""

list_display = (
'requested_at', 'farm_count', 'file_url', 'last_task_status',
'requested_at', 'farm_group', 'file_url', 'last_task_status',
'background_tasks'
)
filter_horizontal = ('farms',)
actions = (generate_insight_report_action,)
readonly_fields = ('file',)

def farm_count(self, obj: CropInsightRequest):
"""Return farm list."""
return obj.farms.count()

def file_url(self, obj):
"""Return file url."""
if obj.file:
Expand Down
58 changes: 57 additions & 1 deletion django_project/gap/admin/farm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,68 @@
"""

from django.contrib import admin, messages
from django.utils.html import format_html

from core.admin import AbstractDefinitionAdmin
from gap.models import (
FarmCategory, FarmRSVPStatus, Farm
)
from gap.tasks.crop_insight import generate_spw
from gap.models.farm_group import FarmGroup, FarmGroupCropInsightField
from gap.tasks.crop_insight import generate_spw, generate_crop_plan


class FarmGroupCropInsightFieldInline(admin.TabularInline):
"""Inline list for model output in FarmGroupCropInsightField."""

model = FarmGroupCropInsightField
extra = 0


@admin.action(description='Recreate fields')
def recreate_farm_group_fields(modeladmin, request, queryset):
"""Recreate farm group fields."""
for group in queryset.all():
group.prepare_fields()


@admin.action(description='Run crop insight')
def run_crop_insight(modeladmin, request, queryset):
"""Run crop insight."""
generate_crop_plan.delay()


@admin.register(FarmGroup)
class FarmGroupAdmin(AbstractDefinitionAdmin):
"""FarmGroup admin."""

list_display = (
'name', 'description', 'farm_count'
)

filter_horizontal = ('farms', 'users')
inlines = (FarmGroupCropInsightFieldInline,)
actions = (recreate_farm_group_fields, run_crop_insight)
readonly_fields = ('displayed_headers',)

def farm_count(self, obj: FarmGroup):
"""Return farm list."""
return obj.farms.count()

def displayed_headers(self, obj: FarmGroup):
"""Display headers as a table."""
columns = "".join(
f'<td style="padding: 10px; border: 1px solid gray">{header}</td>'
for header in obj.headers
)
return format_html(
'<div style="width:1000px; overflow:auto;">'
' <table>'
f" <thead><tr>{columns}</tr>"
' </table>'
'</div>'
)

displayed_headers.allow_tags = True


@admin.action(description='Generate farms spw')
Expand Down
10 changes: 10 additions & 0 deletions django_project/gap/factories/farm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from gap.models import (
FarmCategory, FarmRSVPStatus, Farm
)
from gap.models.farm_group import FarmGroup


class FarmCategoryFactory(DjangoModelFactory):
Expand Down Expand Up @@ -51,3 +52,12 @@ class Meta: # noqa
category = factory.SubFactory(FarmCategoryFactory)
crop = factory.SubFactory('gap.factories.crop_insight.CropFactory')
phone_number = '123-456-7936'


class FarmGroupFactory(DjangoModelFactory):
"""Factory class for FarmGroup model."""

class Meta: # noqa
model = FarmGroup

name = factory.Sequence(lambda n: f'name-{n}')
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 4.2.7 on 2024-09-20 05:51

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('gap', '0019_grid'),
]

operations = [
migrations.CreateModel(
name='FarmGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=512)),
('description', models.TextField(blank=True, null=True)),
('farms', models.ManyToManyField(to='gap.farm')),
('users', models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['name'],
'abstract': False,
},
),
migrations.RemoveField(
model_name='cropinsightrequest',
name='farms',
),
migrations.CreateModel(
name='FarmGroupCropInsightField',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('field', models.CharField(max_length=256)),
('column_number', models.PositiveSmallIntegerField(default=1, help_text='Column number on the csv. It is start from 1')),
('label', models.CharField(blank=True, help_text='Change the field name to a other label. Keep it empty if does not want to override', max_length=256, null=True)),
('active', models.BooleanField(default=True, help_text='Check if the field is active and included on the csv')),
('farm_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gap.farmgroup')),
],
options={
'ordering': ['column_number'],
},
),
migrations.AddField(
model_name='cropinsightrequest',
name='farm_group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='gap.farmgroup'),
),
]
87 changes: 58 additions & 29 deletions django_project/gap/models/crop_insight.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
.. note:: Models
"""

import json
import uuid
from datetime import date, timedelta

Expand All @@ -16,10 +15,10 @@
from django.core.mail import EmailMessage
from django.utils import timezone

from core.group_email_receiver import crop_plan_receiver
from core.models.background_task import BackgroundTask, TaskStatus
from core.models.common import Definition
from gap.models import Farm
from gap.models.farm import Farm
from gap.models.farm_group import FarmGroup
from gap.models.lookup import RainfallClassification
from gap.models.measurement import DatasetAttribute
from gap.models.preferences import Preferences
Expand Down Expand Up @@ -263,7 +262,7 @@ class CropPlanData:
"""The report model for the Insight Request Report."""

@staticmethod
def default_fields():
def forecast_default_fields():
"""Return shortterm default fields."""
from gap.providers.tio import tomorrowio_shortterm_forecast_dataset

Expand All @@ -279,6 +278,18 @@ def default_fields():
forecast_fields.append('rainAccumulationType')
return forecast_fields

@staticmethod
def default_fields():
"""Return default fields for Farm Plan Data."""
return [
'farmID',
'phoneNumber',
'latitude',
'longitude',
'SPWTopMessage',
'SPWDescription',
]

def __init__(
self, farm: Farm, generated_date: date, forecast_days: int = 13,
forecast_fields: list = None
Expand Down Expand Up @@ -322,7 +333,7 @@ def __init__(

# Make default forecast_fields
if not forecast_fields:
forecast_fields = CropPlanData.default_fields()
forecast_fields = CropPlanData.forecast_default_fields()

self.forecast = self.forecast.filter(
dataset_attribute__source__in=forecast_fields
Expand All @@ -331,6 +342,19 @@ def __init__(
self.forecast_fields = forecast_fields
self.forecast_days = forecast_days

@staticmethod
def forecast_key(day_n, field):
"""Return key for forecast field."""
return f'day{day_n}_{field}'

@staticmethod
def forecast_fields_used():
"""Return list of forecast fields that being used by crop insight."""
return [
'rainAccumulationSum', 'precipitationProbability',
'rainAccumulationType'
]

@property
def data(self) -> dict:
"""Return the data."""
Expand Down Expand Up @@ -365,7 +389,7 @@ def data(self) -> dict:
# Short term forecast data
for idx in range(self.forecast_days):
for field in self.forecast_fields:
output[f'day{idx + 1}_{field}'] = ''
output[CropPlanData.forecast_key(idx + 1, field)] = ''

first_date = None
if self.forecast.first():
Expand All @@ -376,15 +400,17 @@ def data(self) -> dict:
for data in self.forecast:
var = data.dataset_attribute.source
day_n = (data.value_date - first_date).days + 1
output[f'day{day_n}_{var}'] = data.value
output[CropPlanData.forecast_key(day_n, var)] = data.value

if (var == 'rainAccumulationSum' and
'rainAccumulationType' in self.forecast_fields):
# we get the rain type
_class = RainfallClassification.classify(data.value)
if _class:
output[
f'day{day_n}_rainAccumulationType'
CropPlanData.forecast_key(
day_n, 'rainAccumulationType'
)
] = _class.name
return output

Expand All @@ -402,7 +428,9 @@ class CropInsightRequest(models.Model):
default=timezone.now,
help_text='The time when the request is made'
)
farms = models.ManyToManyField(Farm)
farm_group = models.ForeignKey(
FarmGroup, null=True, blank=True, on_delete=models.CASCADE
)
file = models.FileField(
upload_to=ingestor_file_path,
null=True, blank=True
Expand Down Expand Up @@ -500,21 +528,28 @@ def title(self) -> str:
"""Return the title of the request."""
east_africa_timezone = Preferences.east_africa_timezone()
east_africa_time = self.requested_at.astimezone(east_africa_timezone)
group = ''
if self.farm_group:
group = f' {self.farm_group} -'
return (
"GAP - Crop Plan Generator Results - "
f"GAP - Crop Plan Generator Results -{group} "
f"{east_africa_time.strftime('%A-%d-%m-%Y')} "
f"({east_africa_timezone})"
)

def _generate_report(self):
"""Generate reports."""
from spw.generator.crop_insight import CropInsightFarmGenerator
output = []

# If farm is empty, put empty farm
farms = self.farms.all()
if not farms.count():
farms = [Farm()]
farms = []
if self.farm_group:
farms = self.farm_group.farms.all()

output = [
self.farm_group.headers
]
fields = self.farm_group.fields

# Get farms
for farm in farms:
Expand All @@ -532,21 +567,19 @@ def _generate_report(self):
).data

# Create header
if len(output) == 0:
output.append(list(data.keys()))
output.append([val for key, val in data.items()])
row_data = []
for field in fields:
try:
row_data.append(data[field.field])
except KeyError:
row_data.append('')
output.append(row_data)

# Render csv
self.update_note('Generate CSV')
csv_content = ''

# Replace header
output[0] = json.loads(
json.dumps(output[0]).
replace('rainAccumulationSum', 'mm').
replace('rainAccumulationType', 'Type').
replace('precipitationProbability', 'Chance')
)
# Save to csv
for row in output:
csv_content += ','.join(map(str, row)) + '\n'
content_file = ContentFile(csv_content)
Expand All @@ -566,11 +599,7 @@ def _generate_report(self):
Best regards
''',
from_email=settings.DEFAULT_FROM_EMAIL,
to=[
email for email in
crop_plan_receiver().values_list('email', flat=True)
if email
]
to=self.farm_group.email_recipients()
)
email.attach(
f'{self.unique_id}.csv',
Expand Down
Loading

0 comments on commit 38960bc

Please sign in to comment.