-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add priotization code and refactor api
- Loading branch information
Showing
6 changed files
with
197 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import hashlib | ||
import json | ||
import typing | ||
|
||
import django_filters | ||
from django.core.serializers.json import DjangoJSONEncoder | ||
|
||
from per.models import OpsLearningCacheResponse | ||
|
||
|
||
class OpslearningSummaryCacheHelper: | ||
@staticmethod | ||
def calculate_md5_str(string): | ||
hash_md5 = hashlib.md5() | ||
hash_md5.update(string) | ||
return hash_md5.hexdigest() | ||
|
||
@classmethod | ||
def generate_hash(cls, value: typing.Union[None, str, dict]) -> str: | ||
# TODO: Use OrderedDict | ||
if value is None: | ||
return "" | ||
hashable = None | ||
if isinstance(value, str): | ||
hashable = value | ||
elif isinstance(value, dict): | ||
hashable = json.dumps( | ||
value, | ||
sort_keys=True, | ||
indent=2, | ||
cls=DjangoJSONEncoder, | ||
).encode("utf-8") | ||
else: | ||
raise Exception(f"Invalid Type: {type(value)}") | ||
return cls.calculate_md5_str(hashable) | ||
|
||
@classmethod | ||
def get_or_create( | ||
self, | ||
request, | ||
filter_sets: typing.List[django_filters.FilterSet], | ||
): | ||
filter_data = { | ||
key: value | ||
for key, value in request.query_params.items() | ||
if key in [field for filter_set in filter_sets for field in filter_set.get_filters()] | ||
} | ||
hash_value = self.generate_hash(filter_data) | ||
# Check if the summary is already cached | ||
ops_learning_summary = OpsLearningCacheResponse.objects.filter(used_filters_hash=hash_value).first() | ||
if ops_learning_summary: | ||
return ops_learning_summary | ||
# TODO: Create a new summary based on the filters | ||
# returning a dummy object for now | ||
return OpsLearningCacheResponse.objects.first() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
from ast import literal_eval | ||
|
||
import pandas as pd | ||
from celery import shared_task | ||
from django.db.models import Exists, F, OuterRef | ||
|
||
from api.models import Country | ||
from country_plan.models import CountryPlan | ||
from per.models import FormPrioritization, Overview | ||
|
||
|
||
class OpsLearningSummaryTask: | ||
|
||
@classmethod | ||
def generate_regional_prioritization_list(self, df: pd.DataFrame): | ||
"""Generates a list of regional prioritizations from the given data.""" | ||
df_exploded = df.explode("components") | ||
regional_df = df_exploded.groupby(["region", "components"]).size().reset_index(name="count") | ||
regional_df = regional_df[regional_df["count"] > 2] | ||
regional_list = regional_df.groupby("region")["components"].apply(list).reset_index() | ||
return regional_list | ||
|
||
@classmethod | ||
def generate_global_prioritization_list(self, regional_df: pd.DataFrame): | ||
"""Generates a global prioritization list from regional data.""" | ||
global_df = regional_df.explode("components").groupby("components").size().reset_index(name="count") | ||
global_components = global_df[global_df["count"] > 2]["components"].tolist() | ||
global_list = {"global": global_components} | ||
return global_list | ||
|
||
@classmethod | ||
def generate_country_prioritization_list( | ||
self, regional_df: pd.DataFrame, global_components: list, prioritization_df: pd.DataFrame, country_df: pd.DataFrame | ||
): | ||
"""Generates a country-level prioritization list.""" | ||
regional_dict = dict(zip(regional_df["region"], regional_df["components"])) | ||
merged_df = country_df[["country", "region"]].merge(prioritization_df, on=["country", "region"], how="left") | ||
no_prioritization_df = merged_df[merged_df["components"].isna()] | ||
|
||
for index, row in no_prioritization_df.iterrows(): | ||
region_id = row["region"] | ||
components = regional_dict.get(region_id, global_components["global"]) | ||
no_prioritization_df.at[index, "components"] = components | ||
|
||
final_df = pd.concat([merged_df.dropna(subset=["components"]), no_prioritization_df]) | ||
final_df["components"] = final_df["components"].apply(lambda x: literal_eval(str(x))) | ||
final_df = final_df[["country", "components"]] | ||
return final_df | ||
|
||
@classmethod | ||
def generate_priotization_list(self): | ||
exclusion_list = [ | ||
"IFRC Africa", | ||
"IFRC Americas", | ||
"IFRC Asia-Pacific", | ||
"IFRC Europe", | ||
"IFRC Geneva", | ||
"IFRC MENA", | ||
"Benelux ERU", | ||
"ICRC", | ||
] | ||
|
||
# Get all countries | ||
country_qs = ( | ||
Country.objects.filter(is_deprecated=False, society_name__isnull=False) | ||
.exclude(name__in=exclusion_list) | ||
.annotate(has_country_plan=Exists(CountryPlan.objects.filter(country=OuterRef("pk"), is_publish=True))) | ||
.values("id") | ||
) | ||
country_df = pd.DataFrame(list(country_qs)) | ||
country_df = country_df.rename(columns={"id": "country"}, inplace=True) | ||
|
||
# Get all PER Overview | ||
per_overview_qs = Overview.objects.select_related("country").values( | ||
"id", | ||
"country_id", | ||
"country__region", | ||
"assessment_number", | ||
) | ||
per_overview_df = pd.DataFrame(list(per_overview_qs)) | ||
per_overview_df = per_overview_df.rename( | ||
columns={"id": "overview", "country_id": "country", "country__region": "region"}, inplace=True | ||
) | ||
|
||
# Get all PER Prioritization | ||
per_priotization_qs = ( | ||
FormPrioritization.objects.filter( | ||
is_draft=False, | ||
prioritized_action_responses__isnull=False, | ||
) | ||
.annotate( | ||
components=F("prioritized_action_responses__component"), | ||
) | ||
.values( | ||
"overview__country__id", | ||
"components", | ||
) | ||
) | ||
per_priotization_df = pd.DataFrame(list(per_priotization_qs)) | ||
per_priotization_df = per_priotization_df.merge( | ||
per_overview_df[["overview", "country", "region", "assessment_number"]], on="overview", how="left" | ||
) | ||
per_priotization_df = per_priotization_df.sort_values("assessment_number").drop_duplicates(subset="country", keep="last") | ||
per_priotization_df = per_priotization_df[["region", "country", "components"]] | ||
|
||
# Generate the prioritization list | ||
regional_list = self.generate_regional_prioritization_list(per_priotization_df) | ||
global_list = self.generate_global_prioritization_list(regional_list) | ||
country_list = self.generate_country_prioritization_list(regional_list, global_list, per_priotization_df, country_df) | ||
|
||
return regional_list, global_list, country_list | ||
|
||
|
||
@shared_task | ||
def generate_summary(filter_data, hash_value): | ||
regional_list, global_list, country_list = OpsLearningSummaryTask.generate_priotization_list() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters