Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add farm group #155

Merged
merged 2 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()

Check warning on line 30 in django_project/gap/admin/farm.py

View check run for this annotation

Codecov / codecov/patch

django_project/gap/admin/farm.py#L29-L30

Added lines #L29 - L30 were not covered by tests


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

Check warning on line 36 in django_project/gap/admin/farm.py

View check run for this annotation

Codecov / codecov/patch

django_project/gap/admin/farm.py#L36

Added line #L36 was not covered by tests


@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()

Check warning on line 54 in django_project/gap/admin/farm.py

View check run for this annotation

Codecov / codecov/patch

django_project/gap/admin/farm.py#L54

Added line #L54 was not covered by tests

def displayed_headers(self, obj: FarmGroup):
"""Display headers as a table."""
columns = "".join(

Check warning on line 58 in django_project/gap/admin/farm.py

View check run for this annotation

Codecov / codecov/patch

django_project/gap/admin/farm.py#L58

Added line #L58 was not covered by tests
f'<td style="padding: 10px; border: 1px solid gray">{header}</td>'
for header in obj.headers
)
return format_html(

Check warning on line 62 in django_project/gap/admin/farm.py

View check run for this annotation

Codecov / codecov/patch

django_project/gap/admin/farm.py#L62

Added line #L62 was not covered by tests
'<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 @@
"""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 @@
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 @@

# 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 @@
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 @@
# 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 @@
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 @@
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 @@
"""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 @@
).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('')

Check warning on line 575 in django_project/gap/models/crop_insight.py

View check run for this annotation

Codecov / codecov/patch

django_project/gap/models/crop_insight.py#L574-L575

Added lines #L574 - L575 were not covered by tests
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 @@
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
Loading