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

Local Units Edit Process Flow Update #2346

Merged
merged 19 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6e14afc
Add new fields and Snapshot model for localunit (#2331)
susilnem Dec 2, 2024
8997f7e
LocalUnit: Create, Update, Revert, Latest changes Apis (#2336)
susilnem Dec 10, 2024
8a0c922
Add API to deprecate local unit (#2340)
Rup-Narayan-Rajbanshi Dec 12, 2024
55f3dfa
Add permission for the region level validator and change location_jso…
susilnem Dec 11, 2024
489095b
Add location_json field for location coordinate
susilnem Dec 12, 2024
697fa69
Add admin panel for localunit snapshot and restrict mutation
susilnem Dec 12, 2024
32af3d4
Fix typing issue on previous_data (JsonField) (#2344)
susilnem Dec 17, 2024
b8cc17d
Add Validators level check in permission (#2345)
susilnem Dec 17, 2024
b9c096e
Change field name in option api and add exclude deprecated local unit…
susilnem Dec 18, 2024
75b6419
Remove filter on latest change request api (#2348)
susilnem Dec 18, 2024
3982877
Validate the local unit on revert (#2350)
susilnem Dec 19, 2024
137b967
Fix issue on deprecare reason overview (#2351)
susilnem Dec 20, 2024
22102bc
Set readonly fields on LocalUnit Admin panel (#2353)
susilnem Dec 26, 2024
22443ab
Email Implementation on local units (#2357)
susilnem Dec 31, 2024
e714c32
Add additionalenv in configmap (#2363)
susilnem Jan 2, 2025
e81b2a4
Add local unit local branch name in email context (#2368)
susilnem Jan 3, 2025
714b46b
Change created_at auto_now to auto_now_add (#2378)
susilnem Jan 7, 2025
50c8566
Add start_date field in MicroAppealSerializer (#2381)
susilnem Jan 13, 2025
fd194cb
Fix status code and message (#2382)
susilnem Jan 13, 2025
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
5 changes: 5 additions & 0 deletions deploy/helm/ifrcgo-helm/templates/config/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ data:
DJANGO_READ_ONLY: {{ .Values.env.DJANGO_READ_ONLY | quote }}
SENTRY_SAMPLE_RATE: {{ .Values.env.SENTRY_SAMPLE_RATE | quote }}
SENTRY_DSN: {{ .Values.env.SENTRY_DSN | quote }}

# Additional configs
{{- range $name, $value := .Values.envAdditional }}
{{ $name }}: {{ $value | quote }}
{{- end }}
5 changes: 5 additions & 0 deletions deploy/helm/ifrcgo-helm/templates/config/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,8 @@ stringData:
AZURE_OPENAI_DEPLOYMENT_NAME: "{{ .Values.env.AZURE_OPENAI_DEPLOYMENT_NAME}}"
AZURE_OPENAI_ENDPOINT: "{{ .Values.env.AZURE_OPENAI_ENDPOINT}}"
AZURE_OPENAI_API_KEY: "{{ .Values.env.AZURE_OPENAI_API_KEY}}"

# Additional secrets
{{- range $name, $value := .Values.secretsAdditional }}
{{ $name }}: {{ $value | quote }}
{{- end }}
14 changes: 14 additions & 0 deletions deploy/helm/ifrcgo-helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,24 @@ env:
AZURE_OPENAI_ENDPOINT: ''
AZURE_OPENAI_API_KEY: ''

# NOTE: Used to pass additional configs to api/worker containers
# NOTE: Not used by azure vault
envAdditional:
# Additional configs
# EXAMPLE: MY_CONFIG: "my-value"

secrets:
API_TLS_CRT: ''
API_TLS_KEY: ''
API_ADDITIONAL_DOMAIN_TLS_CRT: ''
API_ADDITIONAL_DOMAIN_TLS_KEY: ''

# NOTE: Used to pass additional secrets to api/worker containers
# NOTE: Not used by azure vault
secretsAdditional:
# Additional secrets
# EXAMPLE: MY_SECRET: "my-secret-value"

api:
domain: "go-staging.ifrc.org"
tls:
Expand Down Expand Up @@ -155,6 +167,8 @@ cronjobs:
schedule: '0 0 * * 0'
- command: 'ingest_icrc'
schedule: '0 3 * * 0'
- command: 'notify_validators'
schedule: '0 0 * * *'


elasticsearch:
Expand Down
37 changes: 37 additions & 0 deletions local_units/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from django.core.exceptions import ValidationError
from reversion_compare.admin import CompareVersionAdmin

from dref.admin import ReadOnlyMixin

from .models import (
Affiliation,
BloodService,
Expand All @@ -14,6 +16,7 @@
HealthData,
HospitalType,
LocalUnit,
LocalUnitChangeRequest,
LocalUnitLevel,
LocalUnitType,
PrimaryHCC,
Expand Down Expand Up @@ -49,6 +52,10 @@ class LocalUnitAdmin(CompareVersionAdmin, admin.OSMGeoAdmin):
"level",
"health",
)
readonly_fields = (
"validated",
"is_locked",
)
list_filter = (
AutocompleteFilterFactory("Country", "country"),
AutocompleteFilterFactory("Type", "type"),
Expand All @@ -64,6 +71,36 @@ def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)


@admin.register(LocalUnitChangeRequest)
class LocalUnitChangeRequestAdmin(ReadOnlyMixin, admin.ModelAdmin):
autocomplete_fields = (
"local_unit",
"triggered_by",
)
search_fields = (
"local_unit__id",
"local_unit__english_branch_name",
"local_unit__local_branch_name",
)
list_filter = ("status",)
list_display = (
"local_unit",
"status",
"current_validator",
)
ordering = ("id",)

def get_queryset(self, request):
return (
super()
.get_queryset(request)
.select_related(
"local_unit",
"triggered_by",
)
)


@admin.register(DelegationOffice)
class DelegationOfficeAdmin(admin.OSMGeoAdmin):
search_fields = ("name", "city", "country__name")
Expand Down
33 changes: 33 additions & 0 deletions local_units/dev_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from django.http import HttpResponse
from django.template import loader
from rest_framework import permissions
from rest_framework.views import APIView


class LocalUnitsEmailPreview(APIView):
permission_classes = [permissions.IsAuthenticated]

def get(self, request):
type_param = request.GET.get("type")
param_types = {"new", "update", "validate", "revert", "deprecate", "regional_validator", "global_validator"}

if type_param not in param_types:
return HttpResponse(f"Invalid type parameter. Please use one of the following values: {', '.join(param_types)}.")

context_mapping = {
"new": {"new_local_unit": True, "validator_email": "Test Validator", "full_name": "Test User"},
"update": {"update_local_unit": True, "validator_email": "Test Validator", "full_name": "Test User"},
"validate": {"validate_success": True, "full_name": "Test User"},
"revert": {"revert_reason": "Test Reason", "full_name": "Test User"},
"deprecate": {"deprecate_local_unit": True, "deprecate_reason": "Test Deprecate Reason", "full_name": "Test User"},
"regional_validator": {"is_validator_regional_admin": True, "full_name": "Regional User"},
"global_validator": {"is_validator_global_admin": True, "full_name": "Global User"},
}

context = context_mapping.get(type_param)
if context is None:
return HttpResponse("No context found for the email preview.")

context["local_branch_name"] = "Test Local Branch"
template = loader.get_template("email/local_units/local_unit.html")
return HttpResponse(template.render(context, request))
7 changes: 7 additions & 0 deletions local_units/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from . import models

enum_register = {
"deprecate_reason": models.LocalUnit.DeprecateReason,
"validation_status": models.LocalUnitChangeRequest.Status,
"validators": models.Validator,
}
1 change: 1 addition & 0 deletions local_units/filterset.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Meta:
"type__code",
"draft",
"validated",
"is_locked",
)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import logging

from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand

from local_units.models import LocalUnit

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = "Create standard local unit global validator permission class and group"

def handle(self, *args, **options):
logger.info("Creating/Updating permissions/groups for local unit global validator")
print("- Creating/Updating permissions/groups for local unit global validator")
codename = "local_unit_global_validator"
content_type = ContentType.objects.get_for_model(LocalUnit)
permission, created = Permission.objects.get_or_create(
codename=codename,
name="Local Unit Global Validator",
content_type=content_type,
)

# If it's a new permission, create a group for it
group, created = Group.objects.get_or_create(name="Local Unit Global Validators")
group.permissions.add(permission)
logger.info("Local unit global validator permission and group created")
85 changes: 85 additions & 0 deletions local_units/management/commands/notify_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from datetime import timedelta

from django.core.management.base import BaseCommand
from django.template.loader import render_to_string
from django.utils import timezone
from sentry_sdk.crons import monitor

from local_units.models import LocalUnit, Validator
from local_units.utils import (
get_email_context,
get_global_validators,
get_region_admins,
)
from main.sentry import SentryMonitor
from notifications.notification import send_notification


class Command(BaseCommand):
help = "Notify validators for the pending local units in different period of time"

@monitor(monitor_slug=SentryMonitor.NOTIFY_VALIDATORS)
def handle(self, *args, **options):
self.stdout.write(self.style.NOTICE("Notifying the validators..."))

# Regional Validators: 14 days
queryset_for_regional_validators = LocalUnit.objects.filter(
validated=False,
is_deprecated=False,
last_sent_validator_type=Validator.LOCAL,
created_at__lte=timezone.now() - timedelta(days=14),
)

# Global Validators: 28 days
queryset_for_global_validators = LocalUnit.objects.filter(
validated=False,
is_deprecated=False,
last_sent_validator_type=Validator.REGIONAL,
created_at__lte=timezone.now() - timedelta(days=28),
)

for local_unit in queryset_for_regional_validators:
self.stdout.write(self.style.NOTICE(f"Notifying regional validators for local unit pk:({local_unit.id})"))
email_context = get_email_context(local_unit)
email_context["is_validator_regional_admin"] = True
email_subject = "Action Required: Local Unit Pending Validation"
email_type = "Local Unit"

for region_admin_validator in get_region_admins(local_unit):
try:
email_context["full_name"] = region_admin_validator.get_full_name()
email_body = render_to_string("email/local_units/local_unit.html", email_context)
send_notification(email_subject, region_admin_validator.email, email_body, email_type)
local_unit.last_sent_validator_type = Validator.REGIONAL
local_unit.save(update_fields=["last_sent_validator_type"])
except Exception as e:
self.stdout.write(
self.style.WARNING(
f"Failed to notify regional validator {region_admin_validator.get_full_name()} for local unit pk:({local_unit.id}): {e}" # noqa
)
)
continue

for local_unit in queryset_for_global_validators:
self.stdout.write(self.style.NOTICE(f"Notifying global validators for local unit pk:({local_unit.id})"))
email_context = get_email_context(local_unit)
email_context["is_validator_global_admin"] = True
email_subject = "Action Required: Local Unit Pending Validation"
email_type = "Local Unit"

for global_validator in get_global_validators():
try:
email_context["full_name"] = global_validator.get_full_name()
email_body = render_to_string("email/local_units/local_unit.html", email_context)
send_notification(email_subject, global_validator.email, email_body, email_type)
local_unit.last_sent_validator_type = Validator.GLOBAL
local_unit.save(update_fields=["last_sent_validator_type"])
except Exception as e:
self.stdout.write(
self.style.WARNING(
f"Failed to notify global validator {global_validator.get_full_name()} for local unit pk:({local_unit.id}): {e}" # noqa
)
)
continue

self.stdout.write(self.style.SUCCESS("Successfully sent the notifications to the validators"))
101 changes: 101 additions & 0 deletions local_units/migrations/0018_localunit_deprecated_reason_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Generated by Django 4.2.16 on 2024-12-11 09:28

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


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("local_units", "0017_alter_healthdata_other_medical_heal"),
]

operations = [
migrations.AddField(
model_name="localunit",
name="deprecated_reason",
field=models.IntegerField(
blank=True,
choices=[
(1, "Non-existent local unit"),
(2, "Incorrectly added local unit"),
(3, "Security concerns"),
(4, "Other"),
],
null=True,
verbose_name="deprecated reason",
),
),
migrations.AddField(
model_name="localunit",
name="deprecated_reason_overview",
field=models.TextField(blank=True, null=True, verbose_name="Explain the reason why the local unit is being deleted"),
),
migrations.AddField(
model_name="localunit",
name="is_deprecated",
field=models.BooleanField(default=False, verbose_name="Is deprecated?"),
),
migrations.AddField(
model_name="localunit",
name="is_locked",
field=models.BooleanField(default=False, verbose_name="Is locked?"),
),
migrations.CreateModel(
name="LocalUnitChangeRequest",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("previous_data", models.JSONField(default=dict, verbose_name="Previous data")),
(
"status",
models.IntegerField(
choices=[(1, "Pending"), (2, "Approved"), (3, "Revert")], default=1, verbose_name="status"
),
),
(
"current_validator",
models.IntegerField(
choices=[(1, "Local"), (2, "Regional"), (3, "Global")], default=1, verbose_name="Current validator"
),
),
("triggered_at", models.DateTimeField(auto_now_add=True, verbose_name="Triggered at")),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated at")),
("rejected_data", models.JSONField(default=dict, verbose_name="Rejected data")),
("rejected_reason", models.TextField(blank=True, null=True, verbose_name="Rejected reason")),
(
"local_unit",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="local_unit_change_request",
to="local_units.localunit",
verbose_name="Local Unit",
),
),
(
"triggered_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="tiggered_by_local_unit",
to=settings.AUTH_USER_MODEL,
verbose_name="triggered by",
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="updated_by_local_unit",
to=settings.AUTH_USER_MODEL,
verbose_name="updated by",
),
),
],
options={
"ordering": ("id",),
},
),
]
Loading
Loading