diff --git a/.gitignore b/.gitignore
index f6629ff9..10ee1d9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ db.sqlite3
# Editor directories and files
.idea
+.editorconfig
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -24,3 +25,6 @@ __pycache__/
*$py.class
.vscode
+
+# Static files
+staticfiles/
\ No newline at end of file
diff --git a/authentication/admin.py b/authentication/admin.py
index 28ede927..5714dc1c 100644
--- a/authentication/admin.py
+++ b/authentication/admin.py
@@ -1,27 +1,26 @@
from django.contrib import admin
-from django.contrib.auth.admin import UserAdmin
+from django.contrib.auth.models import Group
+from django.contrib.auth.admin import GroupAdmin
+from rest_framework.authtoken.models import TokenProxy
+from rest_framework.authtoken.admin import TokenAdmin
+from unfold.admin import ModelAdmin
from .models import User
-class CustomUserAdmin(UserAdmin):
+class CustomUserAdmin(ModelAdmin):
+ search_fields = ("email",)
ordering = ('email_or_cell', 'email')
list_display = ('email_or_cell', 'is_staff')
- list_filter = ('is_staff',)
- fieldsets = (
- (None, {'fields': ('email_or_cell', 'password')}),
- ('Personal info', {'fields': ('email', 'cell')}),
- ('Permissions', {'fields': ('is_staff', 'tcpa_consent', 'groups')}),
- )
- # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
- # overrides get_fieldsets to use this attribute when creating a user.
- add_fieldsets = (
- (None, {
- 'classes': ('wide',),
- 'fields': ('email_or_cell', 'password1', 'password2', 'email', 'cell', 'tcpa_consent', 'is_staff'),
- }),
- )
+class CustomGroupAdmin(ModelAdmin, GroupAdmin):
+ pass
+class CustomTokenAdmin(ModelAdmin, TokenAdmin):
+ pass
admin.site.register(User, CustomUserAdmin)
+admin.site.unregister(Group)
+admin.site.register(Group, CustomGroupAdmin)
+admin.site.unregister(TokenProxy)
+admin.site.register(TokenProxy, CustomTokenAdmin)
\ No newline at end of file
diff --git a/authentication/views.py b/authentication/views.py
index f9fefbb3..ab72bbe8 100644
--- a/authentication/views.py
+++ b/authentication/views.py
@@ -5,23 +5,23 @@
from rest_framework import viewsets, permissions, mixins
from rest_framework.response import Response
from authentication.serializers import UserSerializer, UserOffersSerializer
-from sentry_sdk import capture_message
+from sentry_sdk import capture_exception
from integrations.services.hubspot.integration import update_send_offers_hubspot, upsert_user_hubspot
import uuid
-class UserViewSet(mixins.UpdateModelMixin,
- viewsets.GenericViewSet):
+class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
- queryset = User.objects.all().order_by('-email_or_cell')
+
+ queryset = User.objects.all().order_by("-email_or_cell")
serializer_class = UserSerializer
permission_classes = [permissions.DjangoModelPermissions]
def update(self, request, pk=None):
if pk is None:
- return Response('Must have an associated screen', status=400)
+ return Response("Must have an associated screen", status=400)
screen = Screen.objects.get(uuid=pk)
user = screen.user
if user:
@@ -43,11 +43,8 @@ def update(self, request, pk=None):
try:
upsert_user_to_hubspot(screen, screen.user)
- except Exception:
- capture_message(
- 'HubSpot upsert failed',
- level='warning',
- )
+ except Exception as e:
+ capture_exception(e, level="warning")
return Response("Invalid Email", status=400)
return Response(status=204)
@@ -65,14 +62,13 @@ def upsert_user_to_hubspot(screen, user):
hubspot_id = upsert_user_hubspot(user, screen=screen)
if hubspot_id:
- random_id = str(uuid.uuid4()).replace('-', '')
+ random_id = str(uuid.uuid4()).replace("-", "")
user.external_id = hubspot_id
- user.email_or_cell = f'{hubspot_id}+{random_id}@myfriendben.org'
+ user.email_or_cell = f"{hubspot_id}+{random_id}@myfriendben.org"
user.first_name = None
user.last_name = None
user.cell = None
user.email = None
user.save()
else:
- raise Exception('Failed to upsert user')
-
+ raise Exception("Failed to upsert user")
diff --git a/benefits/settings.py b/benefits/settings.py
index 058f6a11..e4ba34ed 100644
--- a/benefits/settings.py
+++ b/benefits/settings.py
@@ -15,6 +15,7 @@
from decouple import config
from pathlib import Path
+from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
@@ -62,6 +63,13 @@
# Application definition
INSTALLED_APPS = [
+ "unfold",
+ "unfold.contrib.filters", # optional, if special filters are needed
+ "unfold.contrib.forms", # optional, if special form elements are needed
+ "unfold.contrib.import_export", # optional, if django-import-export package is used
+ "unfold.contrib.guardian", # optional, if django-guardian package is used
+ # optional, if django-simple-history package is used
+ "unfold.contrib.simple_history",
'authentication.apps.AuthConfig',
'corsheaders',
'screener.apps.ScreenerConfig',
@@ -193,7 +201,8 @@
),
'default': {
'fallbacks': ['en-us'], # defaults to PARLER_DEFAULT_LANGUAGE_CODE
- 'hide_untranslated': True, # the default; let .active_translations() return fallbacks too.
+ # the default; let .active_translations() return fallbacks too.
+ 'hide_untranslated': True,
},
}
@@ -221,3 +230,79 @@
)
django_heroku.settings(locals())
+
+
+# UNFOLD SETTINGS
+UNFOLD = {
+ "SITE_HEADER": _("MFB Admin"),
+ "SITE_TITLE": _("MFB Admin"),
+ 'APP_NAME': 'Benefits',
+ 'APP_VERSION': '1.0.0',
+ 'APP_DESCRIPTION': 'Benefits is a Django application that helps people find and apply for benefits.',
+ "SIDEBAR": {
+ "show_search": True,
+ "show_all_applications": True,
+ "navigation": [
+ {
+ "title": _("Navigation"),
+ "items": [
+ {
+ "title": _("Programs"),
+ "icon": "other_admission",
+ "link": reverse_lazy("admin:programs_program_changelist"),
+ },
+ {
+ "title": _("Urgent Needs"),
+ "icon": "breaking_news",
+ "link": reverse_lazy("admin:programs_urgentneed_changelist"),
+ },
+ {
+ "title": _("Navigators"),
+ "icon": "near_me",
+ "link": reverse_lazy("admin:programs_navigator_changelist"),
+ },
+ {
+ "title": _("Configurations"),
+ "icon": "tune",
+ "link": reverse_lazy("admin:configuration_configuration_changelist"),
+ },
+ {
+ "title": _("Translations"),
+ "icon": "translate",
+ "link": reverse_lazy("admin:translations_translation_changelist"),
+ },
+ ]
+ },
+ {
+ "separator": True,
+ "items": [
+ {
+ "title": _("Dashboard"),
+ "icon": "dashboard",
+ "link": reverse_lazy("admin:index"),
+ },
+ {
+ "title": _("Translations API"),
+ "icon": "settings",
+ "link": reverse_lazy("translations_api_url"),
+ },
+ {
+ "title": _("Users"),
+ "icon": "person",
+ "link": reverse_lazy("admin:authentication_user_changelist"),
+ },
+ {
+ "title": _("Groups"),
+ "icon": "group",
+ "link": reverse_lazy("admin:auth_group_changelist"),
+ },
+ {
+ "title": _("Tokens"),
+ "icon": "key",
+ "link": reverse_lazy("admin:authtoken_tokenproxy_changelist"),
+ },
+ ],
+ },
+ ],
+ }
+}
diff --git a/configuration/admin.py b/configuration/admin.py
index 8374284d..8db196be 100644
--- a/configuration/admin.py
+++ b/configuration/admin.py
@@ -1,7 +1,10 @@
from django.contrib import admin
+from unfold.admin import ModelAdmin
from .models import Configuration
-class ConfigurationAdmin(admin.ModelAdmin):
+
+class ConfigurationAdmin(ModelAdmin):
search_fields = ('name',)
-admin.site.register(Configuration, ConfigurationAdmin)
\ No newline at end of file
+
+admin.site.register(Configuration, ConfigurationAdmin)
diff --git a/integrations/services/hubspot/integration.py b/integrations/services/hubspot/integration.py
index 2f792d4d..628a91c7 100644
--- a/integrations/services/hubspot/integration.py
+++ b/integrations/services/hubspot/integration.py
@@ -4,6 +4,9 @@
from decouple import config
import json
import re
+from authentication.models import User
+from screener.models import Screen
+from sentry_sdk import capture_exception, capture_message
def upsert_user_hubspot(user, screen=None):
@@ -16,8 +19,8 @@ def upsert_user_hubspot(user, screen=None):
def update_send_offers_hubspot(external_id, send_offers, send_updates):
hubspot = Hubspot()
contact = {
- 'ab01___send_offers': send_offers,
- 'ab01___send_updates': send_updates,
+ "ab01___send_offers": send_offers,
+ "ab01___send_updates": send_updates,
}
try:
hubspot.update_contact(external_id, contact)
@@ -25,9 +28,11 @@ def update_send_offers_hubspot(external_id, send_offers, send_updates):
print(e)
-class Hubspot():
+class Hubspot:
+ MAX_HOUSEHOLD_SIZE = 8
+
def __init__(self):
- self.api_client = HubSpot(access_token=config('HUBSPOT'))
+ self.api_client = HubSpot(access_token=config("HUBSPOT"))
# Hubspot has no insert or update option in their latest API, so the code
# below first attempts to create a contact. If there is already a contact
@@ -40,33 +45,30 @@ def upsert_contact(self, contact):
contact_id = api_response.id
except ApiException as e:
http_body = json.loads(e.body)
- if http_body['category'] == 'CONFLICT':
+ print(http_body)
+ if http_body["category"] == "CONFLICT":
try:
contact_id = self.get_conflict_contact_id(e)
self.update_contact(contact_id, contact)
except ApiException as f:
- print(f)
+ capture_exception(f)
return False
else:
+ capture_exception(e)
return False
return contact_id
def create_contact(self, user):
- simple_public_object_input = SimplePublicObjectInput(
- properties=user
- )
+ simple_public_object_input = SimplePublicObjectInput(properties=user)
api_response = self.api_client.crm.contacts.basic_api.create(
simple_public_object_input=simple_public_object_input
)
return api_response
def update_contact(self, contact_id, user):
- simple_public_object_input = SimplePublicObjectInput(
- properties=user
- )
+ simple_public_object_input = SimplePublicObjectInput(properties=user)
api_response = self.api_client.crm.contacts.basic_api.update(
- contact_id,
- simple_public_object_input=simple_public_object_input
+ contact_id, simple_public_object_input=simple_public_object_input
)
return api_response
@@ -83,38 +85,48 @@ def get_conflict_contact_id(self, e):
http_body = json.loads(e.body)
# strip everything out of the error message except the contact id
# https://community.hubspot.com/t5/APIs-Integrations/Contacts-v3-contact-exists-error/m-p/364629
- contact_id = re.sub('[^0-9]', '', http_body['message'])
+ contact_id = re.sub("[^0-9]", "", http_body["message"])
return contact_id
- def mfb_user_to_hubspot_contact(self, user, screen=None):
+ def mfb_user_to_hubspot_contact(self, user: User, screen: Screen = None):
contact = {
- 'email': user.email,
- 'firstname': user.first_name,
- 'lastname': user.last_name,
- 'phone': str(user.cell),
- 'benefits_screener_id': user.id,
- 'ab01___send_offers': user.send_offers,
- 'ab01___send_updates': user.send_updates,
- 'ab01___tcpa_consent_to_contact': user.tcpa_consent,
- 'hs_language': user.language_code,
- 'ab01___screener_id': None,
- 'ab01___screener_uuid': None,
- 'ab01___1st_mfb_completion_date': user.date_joined.date().isoformat(),
- 'full_name': f'{user.first_name} {user.last_name}'
+ "email": user.email,
+ "firstname": user.first_name,
+ "lastname": user.last_name,
+ "phone": str(user.cell),
+ "benefits_screener_id": user.id,
+ "ab01___send_offers": user.send_offers,
+ "ab01___send_updates": user.send_updates,
+ "ab01___tcpa_consent_to_contact": user.tcpa_consent,
+ "hs_language": user.language_code,
+ "ab01___1st_mfb_completion_date": user.date_joined.date().isoformat(),
+ "full_name": f"{user.first_name} {user.last_name}",
}
if screen:
- contact['ab01___screener_id'] = screen.id
- contact['ab01___uuid'] = str(screen.uuid)
+ contact["ab01___screener_id"] = screen.id
+ contact["ab01___uuid"] = str(screen.uuid)
+ contact["ab01___county"] = screen.county
+ contact["ab01___number_of_household_members"] = screen.household_size
+
+ members = screen.household_members.all()
+ if len(members) > self.MAX_HOUSEHOLD_SIZE:
+ capture_message(f"screen has more than {self.MAX_HOUSEHOLD_SIZE} household members", level="error")
+
+ for i, member in enumerate(members):
+ if i >= self.MAX_HOUSEHOLD_SIZE:
+ break
+
+ contact[f"ab01___hhm{i + 1}_age"] = member.age
return contact
def format_email_new_benefit(self, user, num_benefits, value_benefits):
contact = {
- 'id': user.external_id,
- 'properties': {
- 'ab01___number_of_new_benefits': int(num_benefits),
- 'ab01___new_benefit_total_value': int(value_benefits),
- }
+ "id": user.external_id,
+ "properties": {
+ "ab01___number_of_new_benefits": int(num_benefits),
+ "ab01___new_benefit_total_value": int(value_benefits),
+ },
}
return contact
diff --git a/integrations/util/cache.py b/integrations/util/cache.py
index a340fb3e..ca61ad37 100644
--- a/integrations/util/cache.py
+++ b/integrations/util/cache.py
@@ -1,8 +1,8 @@
-from sentry_sdk import capture_message
+from sentry_sdk import capture_exception
import datetime
-class Cache():
+class Cache:
expire_time = 0
default = 0
@@ -17,8 +17,8 @@ def _update_cache(self):
try:
self.data = self.update()
self.last_update = datetime.datetime.now()
- except Exception:
- capture_message(f'Failed to update {self.__class__.__name__}', level='warning')
+ except Exception as e:
+ capture_exception(e, level="warning")
def should_update(self):
return datetime.datetime.now() > self.last_update + datetime.timedelta(seconds=self.expire_time)
diff --git a/programs/admin.py b/programs/admin.py
index 8fdf1b8a..77d63c3d 100644
--- a/programs/admin.py
+++ b/programs/admin.py
@@ -1,4 +1,7 @@
from django.contrib import admin
+from django.urls import reverse
+from django.utils.html import format_html
+from unfold.admin import ModelAdmin
from .models import (
LegalStatus,
Program,
@@ -14,50 +17,186 @@
)
-class ProgramAdmin(admin.ModelAdmin):
- search_fields = ('name_abbreviated',)
+class ProgramAdmin(ModelAdmin):
+ search_fields = ("name__translations__text",)
+ list_display = ["get_str", "name_abbreviated", "active", "action_buttons"]
+ filter_horizontal = ('legal_status_required', 'documents',)
+
+ def get_str(self, obj):
+ return str(obj) if str(obj).strip() else 'unnamed'
+
+ get_str.admin_order_field = "name"
+ get_str.short_description = "Program"
+
+ def action_buttons(self, obj):
+ name = obj.name
+ description = obj.description
+ description_short = obj.description_short
+ learn_more_link = obj.learn_more_link
+ apply_button_link = obj.apply_button_link
+ category = obj.category
+ estimated_delivery_time = obj.estimated_delivery_time
+ estimated_application_time = obj.estimated_application_time
+ value_type = obj.value_type
+ warning = obj.warning
+ website_description = obj.website_description
+
+ return format_html(
+ """
+
+ """,
+ reverse("translation_admin_url", args=[name.id]),
+ reverse("translation_admin_url", args=[description.id]),
+ reverse("translation_admin_url", args=[description_short.id]),
+ reverse("translation_admin_url", args=[category.id]),
+ reverse("translation_admin_url", args=[learn_more_link.id]),
+ reverse("translation_admin_url", args=[apply_button_link.id]),
+ reverse("translation_admin_url", args=[
+ estimated_delivery_time.id]),
+ reverse("translation_admin_url", args=[
+ estimated_application_time.id]),
+ reverse("translation_admin_url", args=[value_type.id]),
+ reverse("translation_admin_url", args=[warning.id]),
+ reverse("translation_admin_url", args=[website_description.id]),
+ )
+
+ action_buttons.short_description = "Translate:"
+ action_buttons.allow_tags = True
+
+
+class LegalStatusAdmin(ModelAdmin):
+ search_fields = ("status",)
+
+
+class NavigatorCountiesAdmin(ModelAdmin):
+ search_fields = ("name",)
+
+
+class NavigatorAdmin(ModelAdmin):
+ search_fields = ("name__translations__text",)
+ list_display = ["get_str", "external_name", "action_buttons"]
+ filter_horizontal = ('program', 'counties',)
+
+ def get_str(self, obj):
+ return str(obj) if str(obj).strip() else 'unnamed'
+
+ get_str.admin_order_field = "name"
+ get_str.short_description = "Navigator"
+
+ def action_buttons(self, obj):
+ name = obj.name
+ email = obj.email
+ assistance_link = obj.assistance_link
+ description = obj.description
+
+ return format_html(
+ """
+
+ """,
+ reverse("translation_admin_url", args=[name.id]),
+ reverse("translation_admin_url", args=[email.id]),
+ reverse("translation_admin_url", args=[assistance_link.id]),
+ reverse("translation_admin_url", args=[description.id]),
+ )
+
+ action_buttons.short_description = "Translate:"
+ action_buttons.allow_tags = True
+
+
+class UrgentNeedAdmin(ModelAdmin):
+ search_fields = ("name__translations__text",)
+ list_display = ["get_str", "external_name", "active", "action_buttons"]
+ filter_horizontal = ('type_short', 'functions',)
+
+ def get_str(self, obj):
+ return str(obj) if str(obj).strip() else 'unnamed'
+
+ get_str.admin_order_field = "name"
+ get_str.short_description = "Urgent Need"
+
+ def action_buttons(self, obj):
+ name = obj.name
+ description = obj.description
+ link = obj.link
+ type = obj.type
+ warning = obj.warning
+ website_description = obj.website_description
+
+ return format_html(
+ """
+
+ """,
+ reverse("translation_admin_url", args=[name.id]),
+ reverse("translation_admin_url", args=[description.id]),
+ reverse("translation_admin_url", args=[link.id]),
+ reverse("translation_admin_url", args=[type.id]),
+ reverse("translation_admin_url", args=[warning.id]),
+ reverse("translation_admin_url", args=[website_description.id]),
+ )
+
+ action_buttons.short_description = "Translate:"
+ action_buttons.allow_tags = True
+
+class UrgentNeedCategoryAdmin(ModelAdmin):
+ search_fields = ("name",)
+ fields = ("name",)
-class LegalStatusAdmin(admin.ModelAdmin):
- search_fields = ('status',)
-
-class NavigatorCountiesAdmin(admin.ModelAdmin):
- search_fields = ('name',)
-
-
-class NavigatorAdmin(admin.ModelAdmin):
- search_fields = ('translations__name',)
-
-
-class UrgentNeedAdmin(admin.ModelAdmin):
- search_fields = ('translations__name',)
-
-
-class UrgentNeedCategoryAdmin(admin.ModelAdmin):
- search_fields = ('name',)
- fields = ('name',)
-
-
-class UrgentNeedFunctionAdmin(admin.ModelAdmin):
- search_fields = ('name',)
- fields = ('name',)
-
-
-class FederalPovertyLimitAdmin(admin.ModelAdmin):
- search_fields = ('year',)
-
-
-class DocumentAdmin(admin.ModelAdmin):
- search_fields = ('name',)
-
-
-class ReferrerAdmin(admin.ModelAdmin):
- search_fields = ('referrer_code',)
-
-
-class WebHookFunctionsAdmin(admin.ModelAdmin):
- search_fields = ('name',)
+class UrgentNeedFunctionAdmin(ModelAdmin):
+ search_fields = ("name",)
+ fields = ("name",)
+
+
+class FederalPovertyLimitAdmin(ModelAdmin):
+ search_fields = ("year",)
+
+
+class DocumentAdmin(ModelAdmin):
+ search_fields = ("external_name",)
+
+
+class ReferrerAdmin(ModelAdmin):
+ search_fields = ("referrer_code",)
+ filter_horizontal = ('webhook_functions',
+ 'primary_navigators', 'remove_programs',)
+
+
+class WebHookFunctionsAdmin(ModelAdmin):
+ search_fields = ("name",)
admin.site.register(LegalStatus, LegalStatusAdmin)
diff --git a/programs/migrations/0072_program_website_description.py b/programs/migrations/0072_program_website_description.py
new file mode 100644
index 00000000..518fa67f
--- /dev/null
+++ b/programs/migrations/0072_program_website_description.py
@@ -0,0 +1,25 @@
+# Generated by Django 4.2.11 on 2024-05-21 17:26
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("translations", "0004_translation_no_auto"),
+ ("programs", "0071_alter_program_estimated_value"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="program",
+ name="website_description",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="program_website_description",
+ to="translations.translation",
+ ),
+ ),
+ ]
diff --git a/programs/migrations/0073_auto_20240521_1727.py b/programs/migrations/0073_auto_20240521_1727.py
new file mode 100644
index 00000000..a784a87f
--- /dev/null
+++ b/programs/migrations/0073_auto_20240521_1727.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.11 on 2024-05-21 17:27
+from django.db import migrations
+
+
+def add_website_description(apps, schema_editor):
+ Program = apps.get_model("programs", "Program")
+ Translation = apps.get_model("translations", "Translation")
+
+ for program in Program.objects.all():
+ translation = Translation.objects.add_translation(
+ f"programs.{program.name_abbreviated}-{program.id}_website_description", "[PLACEHOLDER]"
+ )
+ Program.objects.filter(pk=program.id).update(website_description=translation.id)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("programs", "0072_program_website_description"),
+ ("translations", "0004_translation_no_auto"),
+ ]
+
+ operations = [migrations.RunPython(add_website_description, migrations.RunPython.noop)]
diff --git a/programs/migrations/0074_alter_program_website_description.py b/programs/migrations/0074_alter_program_website_description.py
new file mode 100644
index 00000000..7d2f3556
--- /dev/null
+++ b/programs/migrations/0074_alter_program_website_description.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.11 on 2024-05-21 17:31
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("translations", "0004_translation_no_auto"),
+ ("programs", "0073_auto_20240521_1727"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="program",
+ name="website_description",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="program_website_description",
+ to="translations.translation",
+ ),
+ ),
+ ]
diff --git a/programs/migrations/0075_urgentneed_website_description.py b/programs/migrations/0075_urgentneed_website_description.py
new file mode 100644
index 00000000..e9ad6368
--- /dev/null
+++ b/programs/migrations/0075_urgentneed_website_description.py
@@ -0,0 +1,25 @@
+# Generated by Django 4.2.11 on 2024-05-21 20:54
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("translations", "0004_translation_no_auto"),
+ ("programs", "0074_alter_program_website_description"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="urgentneed",
+ name="website_description",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="urgent_website_description",
+ to="translations.translation",
+ ),
+ ),
+ ]
diff --git a/programs/migrations/0076_auto_20240521_2055.py b/programs/migrations/0076_auto_20240521_2055.py
new file mode 100644
index 00000000..60ffa57f
--- /dev/null
+++ b/programs/migrations/0076_auto_20240521_2055.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.2.11 on 2024-05-21 20:55
+import uuid
+from django.db import migrations
+
+
+def add_estimated_value(apps, schema_editor):
+ UrgentNeed = apps.get_model("programs", "UrgentNeed")
+ Translation = apps.get_model("translations", "Translation")
+
+ for urgent_need in UrgentNeed.objects.all():
+ translation = Translation.objects.add_translation(
+ f"urgent_need.{urgent_need.external_name or uuid.uuid4()}-{urgent_need.id}_website_description",
+ "[PLACEHOLDER]",
+ )
+ UrgentNeed.objects.filter(pk=urgent_need.id).update(website_description=translation.id)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("programs", "0075_urgentneed_website_description"),
+ ("translations", "0004_translation_no_auto"),
+ ]
+
+ operations = [migrations.RunPython(add_estimated_value, migrations.RunPython.noop)]
diff --git a/programs/migrations/0077_alter_urgentneed_website_description.py b/programs/migrations/0077_alter_urgentneed_website_description.py
new file mode 100644
index 00000000..24c403f3
--- /dev/null
+++ b/programs/migrations/0077_alter_urgentneed_website_description.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.11 on 2024-05-21 21:02
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("translations", "0004_translation_no_auto"),
+ ("programs", "0076_auto_20240521_2055"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="urgentneed",
+ name="website_description",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="urgent_website_description",
+ to="translations.translation",
+ ),
+ ),
+ ]
diff --git a/programs/models.py b/programs/models.py
index 1a5f69ea..ed553084 100644
--- a/programs/models.py
+++ b/programs/models.py
@@ -34,34 +34,33 @@ def __str__(self):
class LegalStatus(models.Model):
status = models.CharField(max_length=256)
- parent = models.ForeignKey('self', related_name='children', blank=True, null=True, on_delete=models.SET_NULL)
+ parent = models.ForeignKey(
+ "self", related_name="children", blank=True, null=True, on_delete=models.SET_NULL)
def __str__(self):
return self.status
class DocumentManager(models.Manager):
- translated_fields = ('text',)
+ translated_fields = ("text",)
def new_document(self, external_name):
translation = Translation.objects.add_translation(
- f'document.{external_name}_temporary_key', ''
- )
+ f"document.{external_name}_temporary_key")
- document = self.create(
- external_name=external_name,
- text=translation
- )
+ document = self.create(external_name=external_name, text=translation)
- translation.label = f'document.{external_name}_{document.id}'
+ translation.label = f"document.{external_name}_{document.id}"
translation.save()
return document
class Document(models.Model):
- external_name = models.CharField(max_length=120, blank=True, null=True, unique=True)
- text = models.ForeignKey(Translation, related_name='documents', blank=False, null=False, on_delete=models.PROTECT)
+ external_name = models.CharField(
+ max_length=120, blank=True, null=True, unique=True)
+ text = models.ForeignKey(Translation, related_name="documents",
+ blank=False, null=False, on_delete=models.PROTECT)
objects = DocumentManager()
@@ -71,28 +70,34 @@ def __str__(self) -> str:
class ProgramManager(models.Manager):
translated_fields = (
- 'description_short',
- 'name',
- 'description',
- 'learn_more_link',
- 'apply_button_link',
- 'value_type',
- 'estimated_delivery_time',
- 'estimated_application_time',
- 'category',
- 'warning',
- 'estimated_value',
+ "description_short",
+ "name",
+ "description",
+ "learn_more_link",
+ "apply_button_link",
+ "value_type",
+ "estimated_delivery_time",
+ "estimated_application_time",
+ "category",
+ "warning",
+ "estimated_value",
+ "website_description",
)
def new_program(self, name_abbreviated):
translations = {}
for field in self.translated_fields:
translations[field] = Translation.objects.add_translation(
- f'program.{name_abbreviated}_temporary_key-{field}', ''
+ f"program.{name_abbreviated}_temporary_key-{field}"
)
+ # try to set the external_name to the name_abbreviated
+ external_name_exists = self.filter(
+ external_name=name_abbreviated).count() > 0
+
program = self.create(
name_abbreviated=name_abbreviated,
+ external_name=name_abbreviated if not external_name_exists else None,
fpl=None,
active=False,
low_confidence=False,
@@ -100,7 +105,7 @@ def new_program(self, name_abbreviated):
)
for [field, translation] in translations.items():
- translation.label = f'program.{name_abbreviated}_{program.id}-{field}'
+ translation.label = f"program.{name_abbreviated}_{program.id}-{field}"
translation.save()
return program
@@ -111,76 +116,65 @@ def new_program(self, name_abbreviated):
# logic for eligibility and value is stored.
class Program(models.Model):
name_abbreviated = models.CharField(max_length=120)
- external_name = models.CharField(max_length=120, blank=True, null=True, unique=True)
- legal_status_required = models.ManyToManyField(LegalStatus, related_name='programs', blank=True)
- documents = models.ManyToManyField(Document, related_name='program_documents', blank=True)
+ external_name = models.CharField(
+ max_length=120, blank=True, null=True, unique=True)
+ legal_status_required = models.ManyToManyField(
+ LegalStatus, related_name="programs", blank=True)
+ documents = models.ManyToManyField(
+ Document, related_name="program_documents", blank=True)
active = models.BooleanField(blank=True, default=True)
low_confidence = models.BooleanField(blank=True, null=False, default=False)
- fpl = models.ForeignKey(FederalPoveryLimit, related_name='fpl', blank=True, null=True, on_delete=models.SET_NULL)
+ fpl = models.ForeignKey(FederalPoveryLimit, related_name="fpl",
+ blank=True, null=True, on_delete=models.SET_NULL)
description_short = models.ForeignKey(
- Translation,
- related_name='program_description_short',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="program_description_short", blank=False, null=False, on_delete=models.PROTECT
+ )
name = models.ForeignKey(
- Translation,
- related_name='program_name',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="program_name", blank=False, null=False, on_delete=models.PROTECT
+ )
description = models.ForeignKey(
- Translation,
- related_name='program_description',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="program_description", blank=False, null=False, on_delete=models.PROTECT
+ )
learn_more_link = models.ForeignKey(
- Translation,
- related_name='program_learn_more_link',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="program_learn_more_link", blank=False, null=False, on_delete=models.PROTECT
+ )
apply_button_link = models.ForeignKey(
- Translation,
- related_name='program_apply_button_link',
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="program_apply_button_link", null=False, on_delete=models.PROTECT
+ )
value_type = models.ForeignKey(
- Translation,
- related_name='program_value_type',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="program_value_type", blank=False, null=False, on_delete=models.PROTECT
+ )
estimated_delivery_time = models.ForeignKey(
- Translation,
- related_name='program_estimated_delivery_time',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="program_estimated_delivery_time", blank=False, null=False, on_delete=models.PROTECT
+ )
estimated_application_time = models.ForeignKey(
Translation,
- related_name='program_estimated_application_time',
+ related_name="program_estimated_application_time",
blank=False,
null=False,
- on_delete=models.PROTECT)
+ on_delete=models.PROTECT,
+ )
category = models.ForeignKey(
+ Translation, related_name="program_category", blank=False, null=False, on_delete=models.PROTECT
+ )
+ warning = models.ForeignKey(
Translation,
- related_name='program_category',
+ related_name="program_warning",
blank=False,
null=False,
- on_delete=models.PROTECT)
- warning = models.ForeignKey(
+ on_delete=models.PROTECT,
+ )
+ estimated_value = models.ForeignKey(
Translation,
- related_name='program_warning',
+ related_name="program_estimated_value",
blank=False,
null=False,
on_delete=models.PROTECT,
)
- estimated_value = models.ForeignKey(
+ website_description = models.ForeignKey(
Translation,
- related_name='program_estimated_value',
+ related_name="program_website_description",
blank=False,
null=False,
on_delete=models.PROTECT,
@@ -228,7 +222,7 @@ class UrgentNeedCategory(models.Model):
name = models.CharField(max_length=120)
class Meta:
- verbose_name_plural = 'Urgent Need Categories'
+ verbose_name_plural = "Urgent Need Categories"
def __str__(self):
return self.name
@@ -236,70 +230,66 @@ def __str__(self):
class UrgentNeedManager(models.Manager):
translated_fields = (
- 'name',
- 'description',
- 'link',
- 'type',
- 'warning',
+ "name",
+ "description",
+ "link",
+ "type",
+ "warning",
+ "website_description",
)
def new_urgent_need(self, name, phone_number):
translations = {}
for field in self.translated_fields:
- translations[field] = Translation.objects.add_translation(f'urgent_need.{name}_temporary_key-{field}', '')
+ translations[field] = Translation.objects.add_translation(
+ f"urgent_need.{name}_temporary_key-{field}")
+
+ # try to set the external_name to the name
+ external_name_exists = self.filter(external_name=name).count() > 0
urgent_need = self.create(
phone_number=phone_number,
+ external_name=name if not external_name_exists else None,
active=False,
low_confidence=False,
**translations,
)
for [field, translation] in translations.items():
- translation.label = f'urgent_need.{name}_{urgent_need.id}-{field}'
+ translation.label = f"urgent_need.{name}_{urgent_need.id}-{field}"
translation.save()
return urgent_need
class UrgentNeed(models.Model):
- external_name = models.CharField(max_length=120, blank=True, null=True, unique=True)
+ external_name = models.CharField(
+ max_length=120, blank=True, null=True, unique=True)
phone_number = PhoneNumberField(blank=True, null=True)
- type_short = models.ManyToManyField(UrgentNeedCategory, related_name='urgent_needs')
+ type_short = models.ManyToManyField(
+ UrgentNeedCategory, related_name="urgent_needs")
active = models.BooleanField(blank=True, null=False, default=True)
low_confidence = models.BooleanField(blank=True, null=False, default=False)
- functions = models.ManyToManyField(UrgentNeedFunction, related_name='function', blank=True)
+ functions = models.ManyToManyField(
+ UrgentNeedFunction, related_name="function", blank=True)
name = models.ForeignKey(
- Translation,
- related_name='urgent_need_name',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="urgent_need_name", blank=False, null=False, on_delete=models.PROTECT
+ )
description = models.ForeignKey(
- Translation,
- related_name='urgent_need_description',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="urgent_need_description", blank=False, null=False, on_delete=models.PROTECT
+ )
link = models.ForeignKey(
- Translation,
- related_name='urgent_need_link',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="urgent_need_link", blank=False, null=False, on_delete=models.PROTECT
+ )
type = models.ForeignKey(
- Translation,
- related_name='urgent_need_type',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="urgent_need_type", blank=False, null=False, on_delete=models.PROTECT
+ )
warning = models.ForeignKey(
- Translation,
- related_name='urgent_need_warning',
- blank=False,
- null=False,
- on_delete=models.PROTECT
+ Translation, related_name="urgent_need_warning", blank=False, null=False, on_delete=models.PROTECT
+ )
+ website_description = models.ForeignKey(
+ Translation, related_name="urgent_website_description", blank=False, null=False, on_delete=models.PROTECT
)
objects = UrgentNeedManager()
@@ -317,59 +307,55 @@ def __str__(self) -> str:
class NavigatorManager(models.Manager):
translated_fields = (
- 'name',
- 'email',
- 'assistance_link',
- 'description',
+ "name",
+ "email",
+ "assistance_link",
+ "description",
)
def new_navigator(self, name, phone_number):
translations = {}
for field in self.translated_fields:
- translations[field] = Translation.objects.add_translation(f'navigator.{name}_temporary_key-{field}', '')
+ translations[field] = Translation.objects.add_translation(
+ f"navigator.{name}_temporary_key-{field}")
+
+ # try to set the external_name to the name
+ external_name_exists = self.filter(external_name=name).count() > 0
navigator = self.create(
phone_number=phone_number,
+ external_name=name if not external_name_exists else None,
**translations,
)
for [field, translation] in translations.items():
- translation.label = f'navigator.{name}_{navigator.id}-{field}'
+ translation.label = f"navigator.{name}_{navigator.id}-{field}"
translation.save()
return navigator
class Navigator(models.Model):
- program = models.ManyToManyField(Program, related_name='navigator', blank=True)
- external_name = models.CharField(max_length=120, blank=True, null=True, unique=True)
+ program = models.ManyToManyField(
+ Program, related_name="navigator", blank=True)
+ external_name = models.CharField(
+ max_length=120, blank=True, null=True, unique=True)
phone_number = PhoneNumberField(blank=True, null=True)
- counties = models.ManyToManyField(NavigatorCounty, related_name='navigator', blank=True)
+ counties = models.ManyToManyField(
+ NavigatorCounty, related_name="navigator", blank=True)
name = models.ForeignKey(
- Translation,
- related_name='navigator_name',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="navigator_name", blank=False, null=False, on_delete=models.PROTECT
+ )
email = models.ForeignKey(
- Translation,
- related_name='navigator_email',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="navigator_email", blank=False, null=False, on_delete=models.PROTECT
+ )
assistance_link = models.ForeignKey(
- Translation,
- related_name='navigator_assistance_link',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="navigator_assistance_link", blank=False, null=False, on_delete=models.PROTECT
+ )
description = models.ForeignKey(
- Translation,
- related_name='navigator_name_description',
- blank=False,
- null=False,
- on_delete=models.PROTECT)
+ Translation, related_name="navigator_name_description", blank=False, null=False, on_delete=models.PROTECT
+ )
objects = NavigatorManager()
@@ -387,9 +373,12 @@ def __str__(self):
class Referrer(models.Model):
referrer_code = models.CharField(max_length=64, unique=True)
webhook_url = models.CharField(max_length=320, blank=True, null=True)
- webhook_functions = models.ManyToManyField(WebHookFunction, related_name='web_hook', blank=True)
- primary_navigators = models.ManyToManyField(Navigator, related_name='primary_navigators', blank=True)
- remove_programs = models.ManyToManyField(Program, related_name='removed_programs', blank=True)
+ webhook_functions = models.ManyToManyField(
+ WebHookFunction, related_name="web_hook", blank=True)
+ primary_navigators = models.ManyToManyField(
+ Navigator, related_name="primary_navigators", blank=True)
+ remove_programs = models.ManyToManyField(
+ Program, related_name="removed_programs", blank=True)
def __str__(self):
return self.referrer_code
diff --git a/programs/programs/co/pe/member.py b/programs/programs/co/pe/member.py
index 0258cdc8..f1276202 100644
--- a/programs/programs/co/pe/member.py
+++ b/programs/programs/co/pe/member.py
@@ -61,11 +61,12 @@ class Chp(PolicyEngineMembersCalculator):
def value(self):
total = 0
- for pkey, pvalue in self.get_data().items():
- if not self.in_tax_unit(pkey):
+ for member in self.screen.household_members.all():
+ if not self.in_tax_unit(member.id):
continue
- if pvalue['co_chp_eligible'][self.pe_period] > 0 and self.screen.has_insurance_types(('none',)):
+ chp_eligible = self.sim.value(self.pe_category, str(member.id), 'co_chp_eligible', self.pe_period) > 0
+ if chp_eligible and self.screen.has_insurance_types(('none',)):
total += self.amount
return total
diff --git a/programs/programs/co/pe/tax.py b/programs/programs/co/pe/tax.py
index a500674e..53e4aa62 100644
--- a/programs/programs/co/pe/tax.py
+++ b/programs/programs/co/pe/tax.py
@@ -38,4 +38,4 @@ def value(self):
multiplier = band['percent']
break
- return self.get_data()[self.pe_name][self.pe_period] * multiplier
+ return self.get_variable() * multiplier
diff --git a/programs/programs/federal/pe/member.py b/programs/programs/federal/pe/member.py
index a68d4735..e6d842fb 100644
--- a/programs/programs/federal/pe/member.py
+++ b/programs/programs/federal/pe/member.py
@@ -23,9 +23,10 @@ class Wic(PolicyEngineMembersCalculator):
def value(self):
total = 0
- for _, pvalue in self.get_data().items():
- if pvalue[self.pe_name][self.pe_period] > 0:
- total += self.wic_categories[pvalue['wic_category'][self.pe_period]] * 12
+ for member in self.screen.household_members.all():
+ if self.get_member_variable(member.id) > 0:
+ wic_category = self.sim.value('people', str(member.id), 'wic_category', self.pe_period)
+ total += self.wic_categories[wic_category] * 12
return total
@@ -67,18 +68,29 @@ def _value_by_age(self, age: int):
def value(self):
total = 0
- for pkey, pvalue in self.get_data().items():
- # Skip any members that are not in the tax unit.
- if not self.in_tax_unit(pkey):
- continue
+ members = self.screen.household_members.all()
- if pvalue[self.pe_name][self.pe_period] <= 0:
+ for member in members:
+ if self.get_member_variable(member.id) <= 0:
continue
- total += self._value_by_age(pvalue['age'][self.pe_period])
+ # here we need to adjust for children as policy engine
+ # just uses the average which skews very high for adults and
+ # aged adults
+
+ if self._get_age(member.id) <= 18:
+ medicaid_estimated_value = self.child_medicaid_average
+ elif self._get_age(member.id) > 18 and self._get_age(member.id) < 65:
+ medicaid_estimated_value = self.adult_medicaid_average
+ elif self._get_age(member.id) >= 65:
+ medicaid_estimated_value = self.aged_medicaid_average
+ else:
+ medicaid_estimated_value = 0
+
+ total += medicaid_estimated_value
in_wic_demographic = False
- for member in self.screen.household_members.all():
+ for member in members:
if member.pregnant is True or member.age <= 5:
in_wic_demographic = True
if total == 0 and in_wic_demographic:
@@ -89,6 +101,9 @@ def value(self):
return total
+ def _get_age(self, member_id: int) -> int:
+ return self.sim.value(self.pe_category, str(member_id), 'age', self.pe_period)
+
class PellGrant(PolicyEngineMembersCalculator):
pe_name = 'pell_grant'
diff --git a/programs/programs/federal/pe/spm.py b/programs/programs/federal/pe/spm.py
index ec430ff7..c312d267 100644
--- a/programs/programs/federal/pe/spm.py
+++ b/programs/programs/federal/pe/spm.py
@@ -25,7 +25,7 @@ class Snap(PolicyEngineSpmCalulator):
pe_output_period = SNAP_PERIOD
def value(self):
- return int(self.get_data()[self.pe_name][self.pe_output_period]) * 12
+ return int(self.sim.value(self.pe_category, self.pe_sub_category, self.pe_name, self.pe_output_period)) * 12
class SchoolLunch(PolicyEngineSpmCalulator):
@@ -39,8 +39,8 @@ def value(self):
total = 0
num_children = self.screen.num_children(3, 18)
- if self.get_data()[self.pe_name][self.pe_period] > 0 and num_children > 0:
- if self.get_data()['school_meal_tier'][self.pe_period] != 'PAID':
+ if self.get_variable() > 0 and num_children > 0:
+ if self.sim.value(self.pe_category, self.pe_sub_category, 'school_meal_tier', self.pe_period) != 'PAID':
total = SchoolLunch.amount * num_children
return total
diff --git a/programs/programs/policyengine/calculators/base.py b/programs/programs/policyengine/calculators/base.py
index 8f3999bf..1eaea971 100644
--- a/programs/programs/policyengine/calculators/base.py
+++ b/programs/programs/policyengine/calculators/base.py
@@ -4,6 +4,7 @@
from .dependencies.base import PolicyEngineScreenInput
from typing import List
from .constants import YEAR, PREVIOUS_YEAR
+from ..engines import Sim
class PolicyEngineCalulator(ProgramCalculator):
@@ -19,9 +20,9 @@ class PolicyEngineCalulator(ProgramCalculator):
pe_sub_category = ''
pe_period = YEAR
- def __init__(self, screen: Screen, pe_data):
+ def __init__(self, screen: Screen, sim: Sim):
self.screen = screen
- self.pe_data = pe_data
+ self.sim = sim
def eligible(self) -> Eligibility:
e = Eligibility()
@@ -31,13 +32,13 @@ def eligible(self) -> Eligibility:
return e
def value(self):
- return self.get_data()[self.pe_name][self.pe_period]
+ return int(self.get_variable())
- def get_data(self):
+ def get_variable(self):
'''
- Return Policy Engine dictionary of the program category and subcategory
+ Return value of the default variable
'''
- return self.pe_data[self.pe_category][self.pe_sub_category]
+ return self.sim.value(self.pe_category, self.pe_sub_category, self.pe_name, self.pe_period)
@classmethod
def can_calc(cls, missing_dependencies: Dependencies):
@@ -47,37 +48,40 @@ def can_calc(cls, missing_dependencies: Dependencies):
return True
+
class PolicyEngineTaxUnitCalulator(PolicyEngineCalulator):
pe_category = 'tax_units'
pe_sub_category = 'tax_unit'
tax_unit_dependent = True
pe_period = PREVIOUS_YEAR
+
class PolicyEngineSpmCalulator(PolicyEngineCalulator):
pe_category = 'spm_units'
pe_sub_category = 'spm_unit'
+
class PolicyEngineMembersCalculator(PolicyEngineCalulator):
tax_unit_dependent = True
pe_category = 'people'
def value(self):
total = 0
- for pkey, pvalue in self.get_data().items():
+ for member in self.screen.household_members.all():
# The following programs use income from the tax unit,
# so we want to skip any members that are not in the tax unit.
- if not self.in_tax_unit(pkey) and self.tax_unit_dependent:
+ if not self.in_tax_unit(member.id) and self.tax_unit_dependent:
continue
- pe_value = pvalue[self.pe_name][self.pe_period]
+ pe_value = self.get_member_variable(member.id)
total += pe_value
return total
- def in_tax_unit(self, member_id) -> bool:
- return str(member_id) in self.pe_data['tax_units']['tax_unit']['members']
+ def in_tax_unit(self, member_id: int) -> bool:
+ return str(member_id) in self.sim.members('tax_units', 'tax_unit')
- def get_data(self):
- return self.pe_data[self.pe_category]
+ def get_member_variable(self, member_id: int):
+ return self.sim.value(self.pe_category, str(member_id), self.pe_name, self.pe_period)
diff --git a/programs/programs/policyengine/engines.py b/programs/programs/policyengine/engines.py
new file mode 100644
index 00000000..4e117681
--- /dev/null
+++ b/programs/programs/policyengine/engines.py
@@ -0,0 +1,114 @@
+from integrations.util.cache import Cache
+from decouple import config
+import requests
+
+
+class Sim:
+ method_name = ""
+
+ def __init__(self, data) -> None:
+ self.data = data
+
+ def value(self, unit, sub_unit, variable, period):
+ """
+ Calculate variable at the period
+ """
+ raise NotImplementedError
+
+ def members(self, unit, sub_unit):
+ """
+ Return a list of the members in the sub unit
+ """
+ raise NotImplementedError
+
+
+class ApiSim(Sim):
+ method_name = "Policy Engine API"
+ pe_url = "https://api.policyengine.org/us/calculate"
+
+ def __init__(self, data) -> None:
+ response = requests.post(self.pe_url, json=data)
+ self.data = response.json()["result"]
+
+ def value(self, unit, sub_unit, variable, period):
+ return self.data[unit][sub_unit][variable][period]
+
+ def members(self, unit, sub_unit):
+ return self.data[unit][sub_unit]["members"]
+
+
+class PolicyEngineBearerTokenCache(Cache):
+ expire_time = 60 * 60 * 24 * 29
+ default = ""
+ client_id: str = config("POLICY_ENGINE_CLIENT_ID", "")
+ client_secret: str = config("POLICY_ENGINE_CLIENT_SECRET", "")
+ domain = "https://policyengine.uk.auth0.com"
+ endpoint = "/oauth/token"
+
+ def update(self):
+ # https://policyengine.org/us/api#fetch_token
+
+ if self.client_id == "" or self.client_secret == "":
+ raise Exception("client id or secret not configured")
+
+ payload = {
+ "client_id": self.client_id,
+ "client_secret": self.client_secret,
+ "grant_type": "client_credentials",
+ "audience": "https://household.api.policyengine.org",
+ }
+
+ res = requests.post(self.domain + self.endpoint, json=payload)
+
+ return res.json()["access_token"]
+
+
+class PrivateApiSim(ApiSim):
+ method_name = "Private Policy Engine API"
+ token = PolicyEngineBearerTokenCache()
+ pe_url = "https://household.api.policyengine.org/us/calculate"
+
+ def __init__(self, data) -> None:
+ token = self.token.fetch()
+
+ headers = {
+ "Authorization": f"Bearer {token}",
+ }
+
+ res = requests.post(self.pe_url, json=data, headers=headers)
+
+ self.data = res.json()["result"]
+
+
+# NOTE: Code to run Policy Engine locally. This is currently too CPU expensive to run in production.
+# Requires the Policy Engine package to be installed and imported.
+#
+# class LocalSim(Sim):
+# method_name = 'local package'
+#
+# def __init__(self, data) -> None:
+# self.household = data['household']
+#
+# self.entity_map = {}
+# for entity in self.household.keys():
+# group_map = {}
+#
+# for i, group in enumerate(self.household[entity].keys()):
+# group_map[group] = i
+#
+# self.entity_map[entity] = group_map
+#
+# self.sim = Simulation(situation=self.household)
+#
+# def value(self, unit, sub_unit, variable, period):
+# data = self.sim.calculate(variable, period)
+#
+# index = self.entity_map[unit][sub_unit]
+#
+# return data[index]
+#
+# def members(self, unit, sub_unit):
+# return self.household[unit][sub_unit]['members']
+
+
+pe_engines: list[Sim] = [PrivateApiSim, ApiSim]
diff --git a/programs/programs/policyengine/policy_engine.py b/programs/programs/policyengine/policy_engine.py
index c9c425db..5e5593da 100644
--- a/programs/programs/policyengine/policy_engine.py
+++ b/programs/programs/policyengine/policy_engine.py
@@ -1,21 +1,17 @@
from screener.models import HouseholdMember, Screen
-from .calculators import all_calculators, PolicyEngineCalulator
+from .calculators import PolicyEngineCalulator
from programs.programs.calc import Eligibility
from programs.util import Dependencies
from .calculators.dependencies.base import DependencyError
from typing import List
-import requests
-from .calculators.dependencies.member import (
- TaxUnitDependentDependency,
- TaxUnitHeadDependency,
- TaxUnitSpouseDependency,
-)
+from sentry_sdk import capture_exception, capture_message
+from .engines import Sim, pe_engines
def calc_pe_eligibility(
- screen: Screen,
- missing_fields: Dependencies,
- calculators: dict[str, type[PolicyEngineCalulator]],
+ screen: Screen,
+ missing_fields: Dependencies,
+ calculators: dict[str, type[PolicyEngineCalulator]],
) -> dict[str, Eligibility]:
valid_programs: dict[str, type[PolicyEngineCalulator]] = {}
@@ -28,37 +24,34 @@ def calc_pe_eligibility(
if len(valid_programs.values()) == 0 or len(screen.household_members.all()) == 0:
return {}
- data = policy_engine_calculate(pe_input(screen, valid_programs.values()))['result']
+ input_data = pe_input(screen, valid_programs.values())
+ for Method in pe_engines:
+ try:
+ return all_eligibility(Method(input_data), valid_programs, screen)
+ except Exception as e:
+ capture_exception(e, level="warning", message="")
+ capture_message(f"Failed to calculate eligibility with the {Method.method_name} method", level="warning")
+
+ raise Exception("Failed to calculate Policy Engine eligibility")
+
+
+def all_eligibility(method: Sim, valid_programs: dict[str, type[PolicyEngineCalulator]], screen: Screen):
all_eligibility: dict[str, Eligibility] = {}
- has_non_tax_unit_members = screen.has_members_ouside_of_tax_unit()
for name_abbr, Calculator in valid_programs.items():
- calc = Calculator(screen, data)
+ calc = Calculator(screen, method)
e = calc.eligible()
e.value = calc.value()
-
- if Calculator.tax_unit_dependent and has_non_tax_unit_members:
- e.multiple_tax_units = True
-
all_eligibility[name_abbr] = e.to_dict()
return all_eligibility
-def policy_engine_calculate(data):
- response = requests.post(
- "https://api.policyengine.org/us/calculate",
- json=data
- )
- data = response.json()
- return data
-
-
def pe_input(screen: Screen, programs: List[type[PolicyEngineCalulator]]):
- '''
+ """
Generate Policy Engine API request from the list of programs.
- '''
+ """
raw_input = {
"household": {
"people": {},
@@ -67,22 +60,14 @@ def pe_input(screen: Screen, programs: List[type[PolicyEngineCalulator]]):
"members": [],
}
},
- "families": {
- "family": {
- "members": []
- }
- },
- "households": {
- "household": {
- "members": []
- }
- },
+ "families": {"family": {"members": []}},
+ "households": {"household": {"members": []}},
"spm_units": {
"spm_unit": {
"members": [],
}
},
- "marital_units": {}
+ "marital_units": {},
}
}
members: list[HouseholdMember] = screen.household_members.all()
@@ -90,15 +75,15 @@ def pe_input(screen: Screen, programs: List[type[PolicyEngineCalulator]]):
for member in members:
member_id = str(member.id)
- household = raw_input['household']
+ household = raw_input["household"]
- household['families']['family']['members'].append(member_id)
- household['households']['household']['members'].append(member_id)
- household['spm_units']['spm_unit']['members'].append(member_id)
- household['people'][member_id] = {}
+ household["families"]["family"]["members"].append(member_id)
+ household["households"]["household"]["members"].append(member_id)
+ household["spm_units"]["spm_unit"]["members"].append(member_id)
+ household["people"][member_id] = {}
if member.is_in_tax_unit():
- household['tax_units']['tax_unit']['members'].append(member_id)
+ household["tax_units"]["tax_unit"]["members"].append(member_id)
already_added = set()
for member_1, member_2 in relationship_map.items():
@@ -106,14 +91,14 @@ def pe_input(screen: Screen, programs: List[type[PolicyEngineCalulator]]):
continue
marital_unit = (str(member_1), str(member_2))
- raw_input['household']['marital_units']['-'.join(marital_unit)] = {'members': marital_unit}
+ raw_input["household"]["marital_units"]["-".join(marital_unit)] = {"members": marital_unit}
already_added.add(member_1)
already_added.add(member_2)
for Program in programs:
for Data in Program.pe_inputs + Program.pe_outputs:
period = Program.pe_period
- if hasattr(Program, 'pe_output_period') and Data in Program.pe_outputs:
+ if hasattr(Program, "pe_output_period") and Data in Program.pe_outputs:
period = Program.pe_output_period
if not Data.member:
diff --git a/programs/programs/urgent_needs/urgent_need_functions.py b/programs/programs/urgent_needs/urgent_need_functions.py
index 0cd44e0a..65c9a0a4 100644
--- a/programs/programs/urgent_needs/urgent_need_functions.py
+++ b/programs/programs/urgent_needs/urgent_need_functions.py
@@ -103,7 +103,7 @@ def eligible(cls, screen: Screen):
'''
Return True if someone is younger than 18
'''
- return screen.num_children(child_relationship=['all']) >= 1
+ return screen.num_children(child_relationship=['all']) > 0
class BiaFoodDelivery(UrgentNeedFunction):
@@ -216,7 +216,7 @@ def eligible(cls, screen: Screen):
is_income_eligible = (
screen.calc_gross_income('yearly', ['all']) < fpl[screen.household_size]
)
- is_age_eligible = screen.num_adults(age_max=60)
+ is_age_eligible = screen.num_adults(age_max=60) > 0
return is_income_eligible or is_age_eligible
@@ -258,7 +258,7 @@ def eligible(cls, screen: Screen):
'''
Return True if the household has a child aged 0-5 and lives in an eligible county
'''
- is_age_eligible = screen.num_children(age_max=5)
+ is_age_eligible = screen.num_children(age_max=5) > 0
eligible_counties = [
'Adams County',
'Alamosa County',
@@ -286,4 +286,96 @@ def eligible(cls, screen: Screen):
'Weld County',
]
- return is_age_eligible and screen.county in eligible_counties
\ No newline at end of file
+ return is_age_eligible and screen.county in eligible_counties
+
+
+class EarlyChildhoodMentalHealthSupport(UrgentNeedFunction):
+ dependencies = ['age']
+ max_age = 5
+
+ @classmethod
+ def eligible(cls, screen: Screen):
+ '''
+ Return True if the householdh as a child aged 0-5
+ '''
+ return screen.num_children(age_max=cls.max_age) > 0
+
+
+class ParentsOfPreschoolYoungsters(UrgentNeedFunction):
+ dependencies = ['age', 'county']
+ counties = [
+ 'Adams County',
+ 'Alamosa County',
+ 'Arapahoe County',
+ 'Costilla County',
+ 'Crowley County',
+ 'Denver County',
+ 'Dolores County',
+ 'Jefferson County',
+ 'Montezuma County',
+ 'Otero County',
+ 'Pueblo County',
+ 'Saguache County',
+ 'Weld County',
+ ]
+ min_age = 2
+ max_age = 5
+
+ @classmethod
+ def eligible(cls, screen: Screen):
+ '''
+ Return True if a child is between 2 and 5 and lives in an eligible county
+ '''
+ age_eligible = screen.num_children(age_min=cls.min_age, age_max=cls.max_age) > 0
+ county_eligible = screen.county in cls.counties
+
+ return age_eligible and county_eligible
+
+
+class ParentsAsTeacher(UrgentNeedFunction):
+ dependencies = ['age', 'county']
+ counties = [
+ 'Adams County',
+ 'Alamosa County',
+ 'Arapahoe County',
+ 'Bent County',
+ 'Boulder County',
+ 'Conejos County',
+ 'Costilla County',
+ 'Crowley County',
+ 'Delta County',
+ 'Denver County',
+ 'Dolores County',
+ 'El Paso County',
+ 'Fremont County',
+ 'Gunnison County',
+ 'Huerfano County',
+ 'Jefferson County',
+ 'La Plata County',
+ 'Larimer County',
+ 'Las Animas County',
+ 'Mesa County',
+ 'Montezuma County',
+ 'Montrose County',
+ 'Morgan County',
+ 'Otero County',
+ 'Ouray County',
+ 'Park County',
+ 'Pueblo County',
+ 'Routt County',
+ 'Saguache County',
+ 'San Miguel County',
+ 'Teller County',
+ ]
+ max_age = 5
+
+ @classmethod
+ def eligible(cls, screen: Screen):
+ '''
+ Return True if there is a child younger than 5 and lives in an eligible county
+ '''
+ age_eligible = screen.num_children(age_max=cls.max_age) > 0
+ county_eligible = screen.county in cls.counties
+
+ return age_eligible and county_eligible
+
diff --git a/programs/serializers.py b/programs/serializers.py
index 420f1335..841ea9ce 100644
--- a/programs/serializers.py
+++ b/programs/serializers.py
@@ -1,20 +1,29 @@
from programs.models import Program, UrgentNeed, Navigator
from rest_framework import serializers
+from translations.serializers import ModelTranslationSerializer, TranslationSerializer
class NavigatorAPISerializer(serializers.ModelSerializer):
class Meta:
model = Navigator
- fields = '__all__'
+ fields = "__all__"
class ProgramSerializer(serializers.ModelSerializer):
+ name = ModelTranslationSerializer()
+ website_description = ModelTranslationSerializer()
+ category = ModelTranslationSerializer()
+
class Meta:
model = Program
- fields = '__all__'
+ fields = ("id", "name", "website_description", "category")
class UrgentNeedAPISerializer(serializers.ModelSerializer):
+ name = ModelTranslationSerializer()
+ website_description = ModelTranslationSerializer()
+ type = ModelTranslationSerializer()
+
class Meta:
model = UrgentNeed
- fields = '__all__'
+ fields = ("id", "name", "website_description", "type")
diff --git a/programs/views.py b/programs/views.py
index fbd38a66..8d16cfc5 100644
--- a/programs/views.py
+++ b/programs/views.py
@@ -4,32 +4,31 @@
from programs.serializers import ProgramSerializer, NavigatorAPISerializer, UrgentNeedAPISerializer
-class ProgramViewSet(mixins.RetrieveModelMixin,
- viewsets.GenericViewSet):
+class ProgramViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
API endpoint that allows programs to be viewed or edited.
"""
- queryset = Program.objects.all()
+
+ queryset = Program.objects.filter(active=True)
serializer_class = ProgramSerializer
permission_classes = [permissions.IsAuthenticated]
- # filterset_fields = ['legal_status_required', 'value_type']
-class NavigatorViewSet(mixins.RetrieveModelMixin,
- viewsets.GenericViewSet):
+class NavigatorViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""
API endpoint that allows programs to be viewed or edited.
"""
+
queryset = Navigator.objects.all()
serializer_class = NavigatorAPISerializer
permission_classes = [permissions.IsAuthenticated]
-class UrgentNeedViewSet(mixins.RetrieveModelMixin,
- viewsets.GenericViewSet):
+class UrgentNeedViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
API endpoint that allows programs to be viewed or edited.
"""
- queryset = UrgentNeed.objects.all()
+
+ queryset = UrgentNeed.objects.filter(active=True)
serializer_class = UrgentNeedAPISerializer
permission_classes = [permissions.IsAuthenticated]
diff --git a/requirements.txt b/requirements.txt
index 5cf28784..4b7456e1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
asgiref==3.7.2
attrs==21.4.0
+Babel==2.15.0
beautifulsoup4==4.12.2
black==23.3.0
cachetools==4.2.4
@@ -18,6 +19,8 @@ django-parler-rest==2.2
django-phonenumber-field==6.3.0
django-sesame==3.1
djangorestframework==3.14.0
+django-import-export==3.3.8
+django-unfold==0.22.0
drf-yasg==1.21.7
fonttools==4.51.0
geographiclib==1.52
diff --git a/screener/admin.py b/screener/admin.py
index e159b13a..dd553bd2 100644
--- a/screener/admin.py
+++ b/screener/admin.py
@@ -1,4 +1,6 @@
from django.contrib import admin
+from unfold.admin import ModelAdmin
+
from .models import (
Message,
Screen,
@@ -6,11 +8,18 @@
)
-class screenAdmin(admin.ModelAdmin):
+class screenAdmin(ModelAdmin):
search_fields = ('id',)
-admin.site.register(Screen, screenAdmin)
-admin.site.register(Message)
-admin.site.register(IncomeStream)
+class CustomMessageAdmin(ModelAdmin):
+ pass
+
+class CustomIncomeStreamAdmin(ModelAdmin):
+ pass
+
+
+admin.site.register(Screen, screenAdmin)
+admin.site.register(Message, CustomMessageAdmin)
+admin.site.register(IncomeStream, CustomIncomeStreamAdmin)
diff --git a/screener/models.py b/screener/models.py
index 6ed440a5..1cab30f1 100644
--- a/screener/models.py
+++ b/screener/models.py
@@ -257,7 +257,8 @@ def has_benefit(self, name_abbreviated):
'rag': self.has_rag,
'cowap': self.has_cowap,
'ubp': self.has_ubp,
- 'medicaid': self.has_medicaid or self.has_medicaid_hi,
+ 'co_medicaid': self.has_medicaid or self.has_medicaid_hi,
+ 'nc_medicaid': self.has_medicaid or self.has_medicaid_hi,
'medicare': self.has_medicare_hi,
'chp': self.has_chp or self.has_chp_hi,
'va': self.has_va,
@@ -596,6 +597,8 @@ def insurance_map(self):
'private': self.private,
'chp': self.chp,
'medicaid': self.medicaid,
+ 'nc_medicaid': self.medicaid,
+ 'co_medicaid': self.medicaid,
'medicare': self.medicare,
'emergency_medicaid': self.emergency_medicaid,
'family_planning': self.family_planning,
diff --git a/screener/serializers.py b/screener/serializers.py
index f8208311..d8f4bc37 100644
--- a/screener/serializers.py
+++ b/screener/serializers.py
@@ -1,6 +1,7 @@
from screener.models import Screen, HouseholdMember, IncomeStream, Expense, Message, Insurance
from authentication.serializers import UserOffersSerializer
from rest_framework import serializers
+from translations.serializers import TranslationSerializer
class MessageSerializer(serializers.ModelSerializer):
@@ -195,11 +196,6 @@ def update(self, instance, validated_data):
return instance
-class TranslationSerializer(serializers.Serializer):
- default_message = serializers.CharField()
- label = serializers.CharField()
-
-
class NavigatorSerializer(serializers.Serializer):
name = TranslationSerializer()
phone_number = serializers.CharField()
diff --git a/screener/views.py b/screener/views.py
index 31221d68..894f73cf 100644
--- a/screener/views.py
+++ b/screener/views.py
@@ -386,6 +386,9 @@ def urgent_need_results(screen):
screen, missing_dependencies
),
'child_first': urgent_need_functions.ChildFirst.calc(screen, missing_dependencies),
+ 'ecmh': urgent_need_functions.EarlyChildhoodMentalHealthSupport.calc(screen, missing_dependencies),
+ 'hippy': urgent_need_functions.ParentsOfPreschoolYoungsters.calc(screen, missing_dependencies),
+ 'pat': urgent_need_functions.ParentsAsTeacher.calc(screen, missing_dependencies),
}
list_of_needs = []
diff --git a/templates/admin/programs/change_list.html b/templates/admin/programs/change_list.html
new file mode 100644
index 00000000..a869d9c7
--- /dev/null
+++ b/templates/admin/programs/change_list.html
@@ -0,0 +1,70 @@
+{% extends "admin/change_list.html" %} {% block extrahead %}
+
+
+
+{% endblock %}
diff --git a/translations/admin.py b/translations/admin.py
index e9bcd3a5..569aca08 100644
--- a/translations/admin.py
+++ b/translations/admin.py
@@ -1,10 +1,27 @@
from django.contrib import admin
+from django.urls import reverse_lazy
+from django.utils.html import format_html
from parler.admin import TranslatableAdmin
+from unfold.admin import ModelAdmin
from .models import Translation
-class TranslationAdmin(TranslatableAdmin):
+class TranslationAdmin(ModelAdmin, TranslatableAdmin):
search_fields = ('label',)
+ list_display = ['label', 'used_model', 'no_auto',
+ 'edited', 'active', 'go_to']
+ def used_model(self, obj):
+ model_name = obj.used_by['model_name']
+ return model_name.capitalize()
-admin.site.register(Translation, TranslatableAdmin)
+ used_model.short_description = 'Used by (Model)'
+
+ def go_to(self, obj):
+ url = reverse_lazy('translation_admin_url', args=[obj.pk])
+ return format_html('Label ', url)
+
+ go_to.short_description = 'Translate:'
+
+
+admin.site.register(Translation, TranslationAdmin)
diff --git a/translations/bulk_import_translations.py b/translations/bulk_import_translations.py
index 65b16bc1..0b6a6d78 100644
--- a/translations/bulk_import_translations.py
+++ b/translations/bulk_import_translations.py
@@ -43,10 +43,10 @@ def bulk_add(translations):
if ref[0] == 'programs_program':
try:
obj = Program.objects.get(external_name=ref[1])
- obj.active = True
except ObjectDoesNotExist:
obj = Program.objects.new_program(ref[1])
obj.external_name = ref[1]
+ obj.active = ref[3] if len(ref) == 4 else False
obj.save()
elif ref[0] == 'programs_navigator':
try:
@@ -58,10 +58,10 @@ def bulk_add(translations):
elif ref[0] == 'programs_urgentneed':
try:
obj = UrgentNeed.objects.get(external_name=ref[1])
- obj.active = True
except ObjectDoesNotExist:
obj = UrgentNeed.objects.new_urgent_need(ref[1], None)
obj.external_name = ref[1]
+ obj.active = ref[3] if len(ref) == 4 else False
obj.save()
elif ref[0] == 'programs_document':
try:
diff --git a/translations/models.py b/translations/models.py
index a94e3900..d3be47de 100644
--- a/translations/models.py
+++ b/translations/models.py
@@ -3,25 +3,30 @@
from django.conf import settings
+BLANK_TRANSLATION_PLACEHOLDER = '[PLACEHOLDER]'
+
+
class TranslationManager(TranslatableManager):
use_in_migrations = True
- def add_translation(self, label, default_message, active=True, no_auto=False):
+ def add_translation(self, label, default_message=BLANK_TRANSLATION_PLACEHOLDER, active=True, no_auto=False):
default_lang = settings.LANGUAGE_CODE
- parent = self.get_or_create(label=label, defaults={'active': active, 'no_auto': no_auto})[0]
+ parent = self.get_or_create(label=label, defaults={
+ "active": active, "no_auto": no_auto})[0]
if parent.active != active or parent.active != no_auto:
parent.active = active
parent.no_auto = no_auto
parent.save()
- parent.create_translation(default_lang, text=default_message, edited=True)
+ parent.create_translation(
+ default_lang, text=default_message, edited=True)
return parent
def edit_translation(self, label, lang, translation, manual=True):
parent = self.language(lang).get(label=label)
lang_trans = parent.get_lang(lang)
- is_edited = lang_trans is not None and lang_trans.edited is True and lang_trans.text != ''
+ is_edited = lang_trans is not None and lang_trans.edited is True and lang_trans.text != ""
if manual is False and (is_edited or parent.no_auto):
return parent
@@ -31,10 +36,11 @@ def edit_translation(self, label, lang, translation, manual=True):
return parent
def edit_translation_by_id(self, id, lang, translation, manual=True):
- parent = self.prefetch_related('translations').language(lang).get(pk=id)
+ parent = self.prefetch_related(
+ "translations").language(lang).get(pk=id)
lang_trans = parent.get_lang(lang)
- is_edited = lang_trans is not None and lang_trans.edited is True and lang_trans.text != ''
+ is_edited = lang_trans is not None and lang_trans.edited is True and lang_trans.text != ""
if manual is False and (is_edited or parent.no_auto):
return parent
@@ -43,8 +49,8 @@ def edit_translation_by_id(self, id, lang, translation, manual=True):
parent.save()
return parent
- def all_translations(self, langs=[lang['code'] for lang in settings.PARLER_LANGUAGES[None]]):
- translations = self.prefetch_related('translations')
+ def all_translations(self, langs=[lang["code"] for lang in settings.PARLER_LANGUAGES[None]]):
+ translations = self.prefetch_related("translations")
translations_dict = {}
for lang in langs:
lang_translations = {}
@@ -57,7 +63,7 @@ def all_translations(self, langs=[lang['code'] for lang in settings.PARLER_LANGU
def export_translations(self):
all_langs = settings.PARLER_LANGUAGES[None]
- translations = self.prefetch_related('translations')
+ translations = self.prefetch_related("translations")
translations_export = {}
for translation in translations:
@@ -67,59 +73,108 @@ def export_translations(self):
continue
translations_export[translation.label] = {
- 'active': translation.active,
- 'no_auto': translation.no_auto,
- 'langs': {},
- 'reference': reference,
+ "active": translation.active,
+ "no_auto": translation.no_auto,
+ "langs": {},
+ "reference": reference,
}
for lang in all_langs:
- translation.set_current_language(lang['code'])
- translations_export[translation.label]['langs'][lang['code']] = (translation.text, translation.edited)
+ translation.set_current_language(lang["code"])
+ translations_export[translation.label]["langs"][lang["code"]] = (
+ translation.text, translation.edited)
return translations_export
class Translation(TranslatableModel):
translations = TranslatedFields(
- text=models.TextField(null=True, blank=True),
- edited=models.BooleanField(default=False, null=False)
+ text=models.TextField(null=True, blank=True), edited=models.BooleanField(default=False, null=False)
)
active = models.BooleanField(default=True, null=False)
no_auto = models.BooleanField(default=False, null=False)
- label = models.CharField(max_length=128, null=False, blank=False, unique=True)
+ label = models.CharField(max_length=128, null=False,
+ blank=False, unique=True)
objects = TranslationManager()
- def get_lang(self, lang):
- return self.translations.filter(language_code=lang).first()
+ """
+ This method checks if the current Translation object is referenced by any related models
+ such as Program, Navigator, UrgentNeed, or Document. It iterates through all reverse
+ relationships of the Translation model and determines if any related object exists.
+ If a related object is found and `label_unpack` is True, it returns the reverse relationship.
+ Otherwise, it returns details of the related object including the table name, external name,
+ related name, and active status. If no related object is found, it returns False.
+ """
- def in_program(self):
+ def find_used_model(self, label_unpack=False):
+ has_relationship = False
# determine if a translation is refrenced by either a program, navigator, urgent_need, or document
# https://stackoverflow.com/questions/54711671/django-how-to-determine-if-an-object-is-referenced-by-any-other-object
- has_relationship = False
for reverse in (f for f in self._meta.get_fields() if f.auto_created and not f.concrete):
- if reverse.related_name == 'translations':
+ if reverse.related_name == "translations":
continue
+
name = reverse.get_accessor_name()
has_reverse_other = getattr(self, name).count()
if has_reverse_other:
+ if label_unpack:
+ return reverse
try:
active = getattr(self, reverse.related_name).first().active
except AttributeError:
active = True
- if not active:
- has_relationship = True
- continue
-
- external_name = getattr(self, reverse.related_name).first().external_name
- table = getattr(self, reverse.related_name).first()._meta.db_table
+ external_name = getattr(
+ self, reverse.related_name).first().external_name
+ table = getattr(
+ self, reverse.related_name).first()._meta.db_table
if external_name:
- return (table, external_name, reverse.related_name)
+ return (table, external_name, reverse.related_name, active)
has_relationship = True
return has_relationship
+ """
+ This property field stores information about the first model instance that uses
+ this Translation object. It checks for related models and, if a relationship
+ is found, retrieves the instance's ID, model name, field name, and display name (either
+ an external name or an abbreviated name). If no relationship is found, it returns
+ default values indicating the translation is unassigned.
+ """
+ @property
+ def used_by(self):
+ reverse = self.find_used_model(label_unpack=True)
+ if reverse:
+ instance = getattr(self, reverse.get_accessor_name()).first()
+ model_name = reverse.related_model._meta.model_name
+ field_name = reverse.field.name
+ external_name = getattr(instance, 'external_name', None)
+ abbreviated_name = getattr(instance, 'abbreviated_name', None)
+ display_name = external_name if external_name else abbreviated_name
+ return {
+ 'id': instance.id,
+ 'model_name': model_name,
+ 'field_name': field_name,
+ 'display_name': display_name
+ }
+ return {
+ 'id': None,
+ 'model_name': 'unassigned',
+ 'field_name': None,
+ 'display_name': None
+ }
+
+ def get_lang(self, lang):
+ return self.translations.filter(language_code=lang).first()
+
+ def in_program(self):
+ has_relationship = self.find_used_model()
+ return has_relationship
+
+ @property
+ def default_message(self):
+ return self.get_lang(settings.LANGUAGE_CODE).text
+
def __str__(self):
return self.label
diff --git a/translations/serializers.py b/translations/serializers.py
new file mode 100644
index 00000000..f2d98364
--- /dev/null
+++ b/translations/serializers.py
@@ -0,0 +1,17 @@
+from parler_rest.serializers import TranslatableModelSerializer
+from parler_rest.fields import TranslatedFieldsField
+from rest_framework import serializers
+from translations.models import Translation
+
+
+class ModelTranslationSerializer(TranslatableModelSerializer):
+ default_message = serializers.CharField(read_only=True)
+
+ class Meta:
+ model = Translation
+ fields = ("label", "default_message")
+
+
+class TranslationSerializer(serializers.Serializer):
+ default_message = serializers.CharField()
+ label = serializers.CharField()
diff --git a/translations/static/css/bulma_switch.css b/translations/static/css/bulma_switch.css
new file mode 100644
index 00000000..5ec9dcad
--- /dev/null
+++ b/translations/static/css/bulma_switch.css
@@ -0,0 +1 @@
+.switch{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.switch{cursor:pointer;display:inline-flex;align-items:center;position:relative;margin-right:0.5em}.switch+.switch:last-child{margin-right:0}.switch input[type=checkbox]{position:absolute;left:0;opacity:0;outline:none;z-index:-1}.switch input[type=checkbox]+.check{display:flex;align-items:center;flex-shrink:0;width:2.75em;height:1.575em;padding:.2em;background:#b5b5b5;border-radius:4px;transition:background 150ms ease-out,box-shadow 150ms ease-out}.switch input[type=checkbox]+.check.is-white-passive,.switch input[type=checkbox]+.check:hover{background:#fff}.switch input[type=checkbox]+.check.input[type=checkbox]+.switch input[type=checkbox]+.check.check{background:'pink'}.switch input[type=checkbox]+.check.is-black-passive,.switch input[type=checkbox]+.check:hover{background:#0a0a0a}.switch input[type=checkbox]+.check.input[type=checkbox]+.switch input[type=checkbox]+.check.check{background:'pink'}.switch input[type=checkbox]+.check.is-light-passive,.switch input[type=checkbox]+.check:hover{background:#f5f5f5}.switch input[type=checkbox]+.check.input[type=checkbox]+.switch input[type=checkbox]+.check.check{background:'pink'}.switch input[type=checkbox]+.check.is-dark-passive,.switch input[type=checkbox]+.check:hover{background:#363636}.switch input[type=checkbox]+.check.input[type=checkbox]+.switch input[type=checkbox]+.check.check{background:'pink'}.switch input[type=checkbox]+.check.is-primary-passive,.switch input[type=checkbox]+.check:hover{background:#00d1b2}.switch input[type=checkbox]+.check.input[type=checkbox]+.switch input[type=checkbox]+.check.check{background:'pink'}.switch input[type=checkbox]+.check.is-link-passive,.switch input[type=checkbox]+.check:hover{background:#485fc7}.switch input[type=checkbox]+.check.input[type=checkbox]+.switch input[type=checkbox]+.check.check{background:'pink'}.switch input[type=checkbox]+.check.is-info-passive,.switch input[type=checkbox]+.check:hover{background:#3e8ed0}.switch input[type=checkbox]+.check.input[type=checkbox]+.switch input[type=checkbox]+.check.check{background:'pink'}.switch input[type=checkbox]+.check.is-success-passive,.switch input[type=checkbox]+.check:hover{background:#48c78e}.switch input[type=checkbox]+.check.input[type=checkbox]+.switch input[type=checkbox]+.check.check{background:'pink'}.switch input[type=checkbox]+.check.is-warning-passive,.switch input[type=checkbox]+.check:hover{background:#ffe08a}.switch input[type=checkbox]+.check.input[type=checkbox]+.switch input[type=checkbox]+.check.check{background:'pink'}.switch input[type=checkbox]+.check.is-danger-passive,.switch input[type=checkbox]+.check:hover{background:#f14668}.switch input[type=checkbox]+.check.input[type=checkbox]+.switch input[type=checkbox]+.check.check{background:'pink'}.switch input[type=checkbox]+.check:before{content:"";display:block;border-radius:4px;width:1.175em;height:1.175em;background:#f5f5f5;transition:transform 150ms ease-out;will-change:transform;transform-origin:left}.switch input[type=checkbox]+.check.is-elastic:before{transform:scaleX(1.5);border-radius:4px}.switch input[type=checkbox]:checked+.check{background:#00d1b2}.switch input[type=checkbox]:checked+.check.is-white{background:#fff}.switch input[type=checkbox]:checked+.check.is-black{background:#0a0a0a}.switch input[type=checkbox]:checked+.check.is-light{background:#f5f5f5}.switch input[type=checkbox]:checked+.check.is-dark{background:#363636}.switch input[type=checkbox]:checked+.check.is-primary{background:#00d1b2}.switch input[type=checkbox]:checked+.check.is-link{background:#485fc7}.switch input[type=checkbox]:checked+.check.is-info{background:#3e8ed0}.switch input[type=checkbox]:checked+.check.is-success{background:#48c78e}.switch input[type=checkbox]:checked+.check.is-warning{background:#ffe08a}.switch input[type=checkbox]:checked+.check.is-danger{background:#f14668}.switch input[type=checkbox]:checked+.check:before{transform:translate3d(100%, 0, 0)}.switch input[type=checkbox]:checked+.check.is-elastic:before{transform:translate3d(50%, 0, 0) scaleX(1.5)}.switch input[type=checkbox]:focus,.switch input[type=checkbox]:active{outline:none}.switch .control-label{padding-left:0.5em}.switch:hover input[type=checkbox]+.check{background:rgba(181,181,181,0.9)}.switch:hover input[type=checkbox]+.check.is-white-passive{background:rgba(255,255,255,0.9)}.switch:hover input[type=checkbox]+.check.is-black-passive{background:rgba(10,10,10,0.9)}.switch:hover input[type=checkbox]+.check.is-light-passive{background:rgba(245,245,245,0.9)}.switch:hover input[type=checkbox]+.check.is-dark-passive{background:rgba(54,54,54,0.9)}.switch:hover input[type=checkbox]+.check.is-primary-passive{background:rgba(0,209,178,0.9)}.switch:hover input[type=checkbox]+.check.is-link-passive{background:rgba(72,95,199,0.9)}.switch:hover input[type=checkbox]+.check.is-info-passive{background:rgba(62,142,208,0.9)}.switch:hover input[type=checkbox]+.check.is-success-passive{background:rgba(72,199,142,0.9)}.switch:hover input[type=checkbox]+.check.is-warning-passive{background:rgba(255,224,138,0.9)}.switch:hover input[type=checkbox]+.check.is-danger-passive{background:rgba(241,70,104,0.9)}.switch:hover input[type=checkbox]:checked+.check{background:rgba(0,209,178,0.9)}.switch:hover input[type=checkbox]:checked+.check.is-white{background:rgba(255,255,255,0.9)}.switch:hover input[type=checkbox]:checked+.check.is-black{background:rgba(10,10,10,0.9)}.switch:hover input[type=checkbox]:checked+.check.is-light{background:rgba(245,245,245,0.9)}.switch:hover input[type=checkbox]:checked+.check.is-dark{background:rgba(54,54,54,0.9)}.switch:hover input[type=checkbox]:checked+.check.is-primary{background:rgba(0,209,178,0.9)}.switch:hover input[type=checkbox]:checked+.check.is-link{background:rgba(72,95,199,0.9)}.switch:hover input[type=checkbox]:checked+.check.is-info{background:rgba(62,142,208,0.9)}.switch:hover input[type=checkbox]:checked+.check.is-success{background:rgba(72,199,142,0.9)}.switch:hover input[type=checkbox]:checked+.check.is-warning{background:rgba(255,224,138,0.9)}.switch:hover input[type=checkbox]:checked+.check.is-danger{background:rgba(241,70,104,0.9)}.switch.is-rounded input[type=checkbox]+.check{border-radius:9999px}.switch.is-rounded input[type=checkbox]+.check:before{border-radius:9999px}.switch.is-rounded input[type=checkbox].is-elastic:before{transform:scaleX(1.5);border-radius:9999px}.switch.is-outlined input[type=checkbox]+.check{background:transparent;border:0.1rem solid #b5b5b5;padding:.1em}.switch.is-outlined input[type=checkbox]+.check.is-white-passive{border:0.1rem solid rgba(255,255,255,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-white-passive:before{background:#fff}.switch.is-outlined input[type=checkbox]+.check.is-white-passive:hover{border-color:rgba(255,255,255,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-black-passive{border:0.1rem solid rgba(10,10,10,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-black-passive:before{background:#0a0a0a}.switch.is-outlined input[type=checkbox]+.check.is-black-passive:hover{border-color:rgba(10,10,10,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-light-passive{border:0.1rem solid rgba(245,245,245,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-light-passive:before{background:#f5f5f5}.switch.is-outlined input[type=checkbox]+.check.is-light-passive:hover{border-color:rgba(245,245,245,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-dark-passive{border:0.1rem solid rgba(54,54,54,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-dark-passive:before{background:#363636}.switch.is-outlined input[type=checkbox]+.check.is-dark-passive:hover{border-color:rgba(54,54,54,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-primary-passive{border:0.1rem solid rgba(0,209,178,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-primary-passive:before{background:#00d1b2}.switch.is-outlined input[type=checkbox]+.check.is-primary-passive:hover{border-color:rgba(0,209,178,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-link-passive{border:0.1rem solid rgba(72,95,199,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-link-passive:before{background:#485fc7}.switch.is-outlined input[type=checkbox]+.check.is-link-passive:hover{border-color:rgba(72,95,199,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-info-passive{border:0.1rem solid rgba(62,142,208,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-info-passive:before{background:#3e8ed0}.switch.is-outlined input[type=checkbox]+.check.is-info-passive:hover{border-color:rgba(62,142,208,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-success-passive{border:0.1rem solid rgba(72,199,142,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-success-passive:before{background:#48c78e}.switch.is-outlined input[type=checkbox]+.check.is-success-passive:hover{border-color:rgba(72,199,142,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-warning-passive{border:0.1rem solid rgba(255,224,138,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-warning-passive:before{background:#ffe08a}.switch.is-outlined input[type=checkbox]+.check.is-warning-passive:hover{border-color:rgba(255,224,138,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-danger-passive{border:0.1rem solid rgba(241,70,104,0.9)}.switch.is-outlined input[type=checkbox]+.check.is-danger-passive:before{background:#f14668}.switch.is-outlined input[type=checkbox]+.check.is-danger-passive:hover{border-color:rgba(241,70,104,0.9)}.switch.is-outlined input[type=checkbox]+.check:before{background:#b5b5b5}.switch.is-outlined input[type=checkbox]:checked+.check{border-color:#00d1b2}.switch.is-outlined input[type=checkbox]:checked+.check.is-white{background:transparent;border-color:#fff}.switch.is-outlined input[type=checkbox]:checked+.check.is-white:before{background:#fff}.switch.is-outlined input[type=checkbox]:checked+.check.is-black{background:transparent;border-color:#0a0a0a}.switch.is-outlined input[type=checkbox]:checked+.check.is-black:before{background:#0a0a0a}.switch.is-outlined input[type=checkbox]:checked+.check.is-light{background:transparent;border-color:#f5f5f5}.switch.is-outlined input[type=checkbox]:checked+.check.is-light:before{background:#f5f5f5}.switch.is-outlined input[type=checkbox]:checked+.check.is-dark{background:transparent;border-color:#363636}.switch.is-outlined input[type=checkbox]:checked+.check.is-dark:before{background:#363636}.switch.is-outlined input[type=checkbox]:checked+.check.is-primary{background:transparent;border-color:#00d1b2}.switch.is-outlined input[type=checkbox]:checked+.check.is-primary:before{background:#00d1b2}.switch.is-outlined input[type=checkbox]:checked+.check.is-link{background:transparent;border-color:#485fc7}.switch.is-outlined input[type=checkbox]:checked+.check.is-link:before{background:#485fc7}.switch.is-outlined input[type=checkbox]:checked+.check.is-info{background:transparent;border-color:#3e8ed0}.switch.is-outlined input[type=checkbox]:checked+.check.is-info:before{background:#3e8ed0}.switch.is-outlined input[type=checkbox]:checked+.check.is-success{background:transparent;border-color:#48c78e}.switch.is-outlined input[type=checkbox]:checked+.check.is-success:before{background:#48c78e}.switch.is-outlined input[type=checkbox]:checked+.check.is-warning{background:transparent;border-color:#ffe08a}.switch.is-outlined input[type=checkbox]:checked+.check.is-warning:before{background:#ffe08a}.switch.is-outlined input[type=checkbox]:checked+.check.is-danger{background:transparent;border-color:#f14668}.switch.is-outlined input[type=checkbox]:checked+.check.is-danger:before{background:#f14668}.switch.is-outlined input[type=checkbox]:checked+.check:before{background:#00d1b2}.switch.is-outlined:hover input[type=checkbox]+.check{background:transparent;border-color:rgba(181,181,181,0.9)}.switch.is-outlined:hover input[type=checkbox]:checked+.check{background:transparent;border-color:rgba(0,209,178,0.9)}.switch.is-outlined:hover input[type=checkbox]:checked+.check.is-white{border-color:rgba(255,255,255,0.9)}.switch.is-outlined:hover input[type=checkbox]:checked+.check.is-black{border-color:rgba(10,10,10,0.9)}.switch.is-outlined:hover input[type=checkbox]:checked+.check.is-light{border-color:rgba(245,245,245,0.9)}.switch.is-outlined:hover input[type=checkbox]:checked+.check.is-dark{border-color:rgba(54,54,54,0.9)}.switch.is-outlined:hover input[type=checkbox]:checked+.check.is-primary{border-color:rgba(0,209,178,0.9)}.switch.is-outlined:hover input[type=checkbox]:checked+.check.is-link{border-color:rgba(72,95,199,0.9)}.switch.is-outlined:hover input[type=checkbox]:checked+.check.is-info{border-color:rgba(62,142,208,0.9)}.switch.is-outlined:hover input[type=checkbox]:checked+.check.is-success{border-color:rgba(72,199,142,0.9)}.switch.is-outlined:hover input[type=checkbox]:checked+.check.is-warning{border-color:rgba(255,224,138,0.9)}.switch.is-outlined:hover input[type=checkbox]:checked+.check.is-danger{border-color:rgba(241,70,104,0.9)}.switch.is-small{border-radius:2px;font-size:.75rem}.switch.is-medium{font-size:1.25rem}.switch.is-large{font-size:1.5rem}.switch[disabled]{opacity:0.5;cursor:not-allowed;color:#7a7a7a}
\ No newline at end of file
diff --git a/translations/static/css/styles.css b/translations/static/css/styles.css
new file mode 100644
index 00000000..abaaa0b4
--- /dev/null
+++ b/translations/static/css/styles.css
@@ -0,0 +1,144 @@
+@import url('https://fonts.google.com/materialsymbols?family=Material+Symbols:opsz,wght@48,400');
+
+/* Global styling */
+* {
+ font-family: Inter, sans-serif;
+}
+
+.box {
+ box-shadow: none;
+ border-radius: 0;
+ border: 1.5px solid rgba(230, 230, 230, 0.6);
+}
+
+html[data-theme="dark"] .box {
+ border-color: rgba(56, 56, 56, 0.5); /* Change this to the color you want */
+}
+
+.table tbody tr td:last-child {
+ text-align: center;
+}
+
+.columns:not(:last-child) {
+ margin-bottom: 0;
+}
+
+/* Table th tooltip styling */
+th {
+ position: relative;
+ cursor: pointer;
+ }
+
+ th[data-tooltip]::after {
+ content: attr(data-tooltip);
+ visibility: hidden;
+ width: 300px;
+ background-color: #555;
+ color: #fff;
+ text-align: center;
+ border-radius: 6px;
+ padding: 10px;
+ position: absolute;
+ z-index: 1;
+ bottom: 150%;
+ left: 50%;
+ transform: translateX(-50%);
+ opacity: 0;
+ transition: opacity 0.3s;
+ }
+
+ th[data-tooltip]:hover::after {
+ visibility: visible;
+ opacity: 1;
+ }
+
+ th[data-tooltip]:hover::after {
+ left: auto;
+ right: 0;
+ transform: translateX(10%);
+ }
+
+ th[data-tooltip]:first-child:hover::after {
+ left: 0;
+ right: auto;
+ transform: translateX(-10%);
+ }
+
+/* Sidebar styling */
+.sidebar-nav {
+ position: fixed;
+ top: 0;
+ padding: 1.5rem;
+ height: 100vh;
+ overflow-y: auto;
+}
+
+.sidebar-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 1.5rem;
+}
+
+.sidebar-item.is-active {
+ color: #fff;
+ background-color: rgba(88, 88, 141, 0.5);
+}
+
+/* Sidebar menu icon styling */
+.text-icon {
+ display: flex;
+ align-items: center;
+}
+
+.text-icon span:first-child {
+ margin-right: 5px;
+}
+
+.sidebar-item .text-icon span:first-child {
+ margin-right: 15px;
+}
+
+.theme-switch {
+ margin-top: auto;
+ margin-bottom: 2rem;
+}
+
+.theme-switch-icon {
+ height: 20;
+ width: 20px;
+ margin: 0 10px;
+}
+
+/* Edit Dropdown button styling */
+.dropdown-content {
+ text-align: left;
+ position: absolute;
+ right: 95px;
+ bottom: 100%;
+ transform: translateY(-40px); /* Adjust the vertical spacing */
+ display: none;
+}
+
+.dropdown.is-active .dropdown-content {
+ display: block;
+}
+
+.dropdown.is-top .dropdown-content {
+ bottom: auto;
+ top: 100%;
+ transform: translateY(0); /* Adjust the vertical spacing */
+}
+
+/* Pagination styling */
+.pagination-link.is-current {
+ background-color: rgba(88, 88, 141, 0.5);
+}
+
+.new-actions {
+ display: flex; flex-direction: column; align-items: flex-start;
+}
+
+/* Forms */
+.label-form-input {
+ width: 600px;
+}
\ No newline at end of file
diff --git a/translations/static/js/base.js b/translations/static/js/base.js
new file mode 100644
index 00000000..931fb805
--- /dev/null
+++ b/translations/static/js/base.js
@@ -0,0 +1,122 @@
+// For handing the sorting of tables
+function initializeTableSorting() {
+ let table = document.querySelector(".table");
+ let headers = table.querySelectorAll("th");
+ let currentSortColumn = null;
+ let sortDirection = "asc";
+
+ headers.forEach(function (header, index) {
+ header.addEventListener("click", function () {
+ sortTable(index);
+ });
+ });
+
+ function sortTable(columnIndex) {
+ let rows = Array.from(table.querySelectorAll("tbody tr"));
+ if (columnIndex === currentSortColumn) {
+ sortDirection = sortDirection === "asc" ? "desc" : "asc";
+ } else {
+ currentSortColumn = columnIndex;
+ sortDirection = "asc";
+ }
+
+ rows.sort(function (a, b) {
+ let cellA = a.querySelectorAll("td")[columnIndex].textContent.trim();
+ let cellB = b.querySelectorAll("td")[columnIndex].textContent.trim();
+
+ let valueA = isNaN(cellA) ? cellA.toLowerCase() : parseInt(cellA, 10);
+ let valueB = isNaN(cellB) ? cellB.toLowerCase() : parseInt(cellB, 10);
+
+ if (valueA < valueB) return sortDirection === "asc" ? -1 : 1;
+ if (valueA > valueB) return sortDirection === "asc" ? 1 : -1;
+ return 0;
+ });
+
+ let tbody = table.querySelector("tbody");
+ rows.forEach(function (row) {
+ tbody.appendChild(row);
+ });
+ }
+}
+
+// For handling the sidebar menu
+function initializeSidebarMenu() {
+ let menuItems = document.querySelectorAll(".menu-list .sidebar-item");
+ let currentUrl = window.location.href;
+
+ menuItems.forEach(function (menuItem) {
+ let linkUrl = menuItem.href;
+
+ if (linkUrl === currentUrl) {
+ menuItem.classList.add("is-active");
+ }
+
+ menuItem.addEventListener("click", function (event) {
+ menuItems.forEach(function (item) {
+ item.classList.remove("is-active");
+ });
+
+ event.currentTarget.classList.add("is-active");
+ });
+ });
+}
+
+// For handling the dropdown menu in the tables
+function initializeDropdowns() {
+ let dropdowns = document.querySelectorAll("#" + tableId + " .dropdown");
+ dropdowns.forEach(function (dropdown, index) {
+ let dropdownButton = dropdown.querySelector(".button");
+ let dropdownSpan = dropdown.querySelector(".material-symbols-outlined");
+ let dropdownContent = dropdown.querySelector(".dropdown-content");
+
+ dropdownButton.addEventListener("click", function (event) {
+ event.preventDefault();
+ dropdown.classList.toggle("is-active");
+ if (dropdown.classList.contains("is-active")) {
+ dropdownSpan.textContent = "expand_less";
+ checkDropdownPosition(dropdown, dropdownContent);
+ } else {
+ dropdownSpan.textContent = "expand_more";
+ dropdown.classList.remove("is-top");
+ }
+ });
+
+ // Close the dropdown if the user clicks outside of it
+ document.addEventListener("click", function (event) {
+ let isClickInside = dropdown.contains(event.target);
+ if (!isClickInside) {
+ dropdown.classList.remove("is-active");
+ dropdown.classList.remove("is-top");
+ dropdownSpan.textContent = "expand_more";
+ }
+ });
+
+ // Close dropdowns when the page is scrolled
+ window.addEventListener("scroll", function () {
+ dropdown.classList.remove("is-active");
+ dropdown.classList.remove("is-top");
+ dropdownSpan.textContent = "expand_more";
+ });
+ });
+}
+
+function checkDropdownPosition(dropdown, dropdownContent) {
+ let dropdownRect = dropdown.getBoundingClientRect();
+ let dropdownContentRect = dropdownContent.getBoundingClientRect();
+ let viewportHeight = window.innerHeight;
+
+ if (dropdownRect.bottom + dropdownContentRect.height > viewportHeight) {
+ dropdown.classList.remove("is-top");
+ } else {
+ dropdown.classList.add("is-top");
+ }
+}
+
+function initializeAll() {
+ initializeTableSorting();
+ initializeSidebarMenu();
+ initializeDropdowns();
+}
+
+document.addEventListener("DOMContentLoaded", initializeAll);
+document.body.addEventListener("htmx:afterSwap", initializeAll);
diff --git a/translations/static/js/theme.js b/translations/static/js/theme.js
new file mode 100644
index 00000000..692d7cbf
--- /dev/null
+++ b/translations/static/js/theme.js
@@ -0,0 +1,27 @@
+(() => {
+ "use strict";
+ const prefersDarkMode = window.matchMedia(
+ "(prefers-color-scheme: dark)"
+ ).matches;
+ const defaultTheme = prefersDarkMode ? "dark" : "light";
+ const preferredTheme = localStorage.getItem("theme");
+ const toggleDarkMode = document.querySelector("#bd-theme");
+
+ if (!preferredTheme) {
+ localStorage.setItem("theme", defaultTheme);
+ }
+
+ document.documentElement.setAttribute(
+ "data-theme",
+ preferredTheme || defaultTheme
+ );
+
+ toggleDarkMode.checked = preferredTheme === "dark" ? true : false;
+
+ toggleDarkMode.addEventListener("change", function () {
+ const isDarkTheme = localStorage.getItem("theme") === "dark";
+ const newTheme = isDarkTheme ? "light" : "dark";
+ localStorage.setItem("theme", newTheme);
+ document.documentElement.setAttribute("data-theme", newTheme);
+ });
+ })();
\ No newline at end of file
diff --git a/translations/templates/base.html b/translations/templates/base.html
index 8b44ec50..3db3ac25 100644
--- a/translations/templates/base.html
+++ b/translations/templates/base.html
@@ -1,21 +1,122 @@
+{% load static %}
-
-
-
-
- Translations Admin
-
+
+
+
+
+ Translations Admin
+
+
+
+
+
-
-
- Translations
- Programs
- Navigators
- Urgent Needs
- Documents
-
-
- {% block content %} {% endblock content %}
-
-
+
+
+
+
+
+
+
{% block content %} {% endblock content %}
+
+
+
+
+
+
+
diff --git a/translations/templates/documents/document.html b/translations/templates/documents/document.html
index 0760946c..dbedb26d 100644
--- a/translations/templates/documents/document.html
+++ b/translations/templates/documents/document.html
@@ -1,15 +1,33 @@
{% extends "base.html" %}{% block content %}
-{{ document.external_name }}
-
-
-
Text
+
+
+
+
+
+
+ External Name
+ Actions (add)
+
+
+
+
+ {{ document.external_name }}
+
+
+
+
+
+
{% endblock content %}
diff --git a/translations/templates/documents/filter.html b/translations/templates/documents/filter.html
index e0a171f8..f40a066b 100644
--- a/translations/templates/documents/filter.html
+++ b/translations/templates/documents/filter.html
@@ -1,9 +1,20 @@
+{% load static %}
diff --git a/translations/templates/documents/list.html b/translations/templates/documents/list.html
index f3d6ca6d..2f58bc49 100644
--- a/translations/templates/documents/list.html
+++ b/translations/templates/documents/list.html
@@ -1,8 +1,74 @@
-
- {% for document in documents %}
-
- {{ document.external_name }}
- Edit
-
- {% endfor %}
-
+{% load static %}
+
+
+
+
+ ID
+ External Name
+ External Name Label
+ Actions
+
+
+
+ {% for document in page_obj %}
+
+ {{ document.id }}
+ {{ document.external_name }}
+ {{ document.name }}
+
+
+
+
+ Go to
+ expand_more
+
+
+
+
+
+
+ {% empty %}
+
+ No results
+
+ {% endfor %}
+
+
+
+ Total: {{ page_obj.paginator.count }}
+
+
+
+
+ {% include "../pagination.html" %}
+
+
+
diff --git a/translations/templates/documents/main.html b/translations/templates/documents/main.html
index 6b8f1bb7..ef9b8860 100644
--- a/translations/templates/documents/main.html
+++ b/translations/templates/documents/main.html
@@ -1,6 +1,19 @@
{% extends "base.html" %} {% block content %}
-
Documents
-
New
-{% include "./filter.html" %}
+
+ Documents
+
+
+
{% include "./filter.html" %}
+
+
+
+
{% include "./list.html" %} {% endblock content %}
diff --git a/translations/templates/edit/form.html b/translations/templates/edit/form.html
index cef32da5..c7e7fb74 100644
--- a/translations/templates/edit/form.html
+++ b/translations/templates/edit/form.html
@@ -1,5 +1,5 @@
- {{ form }}
- Save
- {% if error_message %} {% include "error.html" %} {% endif %}
+ {{ form }}
+ Save
+ {% if error_message %} {% include "error.html" %} {% endif %}
diff --git a/translations/templates/edit/label_form.html b/translations/templates/edit/label_form.html
new file mode 100644
index 00000000..4ac5dfaa
--- /dev/null
+++ b/translations/templates/edit/label_form.html
@@ -0,0 +1,24 @@
+
+
+
+ {{ form.label.label }}
+
+
+
+ {{ form.active.label }}
+
+
+
+ {{ form.no_auto.label }}
+
+
+
+ Save
+ {% if error_message %} {% include "error.html" %} {% endif %}
+
diff --git a/translations/templates/edit/lang_form.html b/translations/templates/edit/lang_form.html
index 07e01e39..c887eb7f 100644
--- a/translations/templates/edit/lang_form.html
+++ b/translations/templates/edit/lang_form.html
@@ -1,10 +1,23 @@
-{% include "./form.html" with form=form %}
-
- Translate
-
+
+ {% for field in form %}
+
+ {{ field }} {% if field.help_text %}
+
{{ field.help_text }}
+ {% endif %} {% for error in field.errors %}
+
{{ error }}
+ {% endfor %}
+
+ {% endfor %}
+ Save
+
+ Translate
+
+ {% if error_message %} {% include "error.html" %} {% endif %}
+
diff --git a/translations/templates/edit/langs.html b/translations/templates/edit/langs.html
index 78f5a2ff..f32f5dc4 100644
--- a/translations/templates/edit/langs.html
+++ b/translations/templates/edit/langs.html
@@ -1,13 +1,15 @@
- {% for lang, form in langs.items %}
-
{{ lang }}
-
- {% endfor %}
+ {% for lang, form in langs.items %}
+
+ Language Code: {{ lang }}
+
+
+ {% endfor %}
diff --git a/translations/templates/edit/main.html b/translations/templates/edit/main.html
index d508abab..a71af25e 100644
--- a/translations/templates/edit/main.html
+++ b/translations/templates/edit/main.html
@@ -1,16 +1,51 @@
{% extends "base.html" %} {% block content %}
-
{{ translation.label }}
-
-{% include "./langs.html" %}
-
- Delete
-
-
-{% endblock content %}
+
+ {{ translation.label }}
+
+
+
+
+
+
+
+ {% for lang, form in langs.items %}
+
+ Language Code: {{ lang }}
+
+
+ {% endfor %}
+
+
+
+
+ {% endblock content %}
+
diff --git a/translations/templates/filter.html b/translations/templates/filter.html
index d121d0da..7285836b 100644
--- a/translations/templates/filter.html
+++ b/translations/templates/filter.html
@@ -1,7 +1,35 @@
-
diff --git a/translations/templates/main.html b/translations/templates/main.html
index 7f962439..9ad5a954 100644
--- a/translations/templates/main.html
+++ b/translations/templates/main.html
@@ -1,7 +1,19 @@
-{% extends "base.html" %} {% block content %}
-
Translations
-
New
-
-{% include 'filter.html' %}
+{% extends "base.html" %} {% load static %} {% block content %}
+
+ Translations
+
+
+
{% include "./filter.html" %}
+
+
+
+
{% include 'translations.html' %} {% endblock content %}
diff --git a/translations/templates/navigators/filter.html b/translations/templates/navigators/filter.html
index 509bb925..331c12f7 100644
--- a/translations/templates/navigators/filter.html
+++ b/translations/templates/navigators/filter.html
@@ -1,9 +1,20 @@
+{% load static %}
diff --git a/translations/templates/navigators/list.html b/translations/templates/navigators/list.html
index c5a3e310..6617b57e 100644
--- a/translations/templates/navigators/list.html
+++ b/translations/templates/navigators/list.html
@@ -1,8 +1,93 @@
-
- {% for navigator in navigators %}
-
- {{ navigator.name.text }}
- Edit
-
- {% endfor %}
-
+{% load static %}
+
+
+
+
+ ID
+ Name
+ External Name
+ Email
+ Name Label
+ Actions
+
+
+
+ {% for navigator in page_obj %}
+
+ {{ navigator.id }}
+ {{ navigator.name.text }}
+ {{ navigator.external_name }}
+ {{ navigator.email.text }}
+ {{ navigator.name }}
+
+
+
+
+ Go to
+ expand_more
+
+
+
+
+
+
+ {% empty %}
+
+ No results
+
+ {% endfor %}
+
+
+
+ Total: {{ page_obj.paginator.count }}
+
+
+
+
+ {% include "../pagination.html" %}
+
+
+
diff --git a/translations/templates/navigators/main.html b/translations/templates/navigators/main.html
index 9b1e51dd..ba9776df 100644
--- a/translations/templates/navigators/main.html
+++ b/translations/templates/navigators/main.html
@@ -1,6 +1,19 @@
{% extends "base.html" %} {% block content %}
-
Navigator
-
New
-{% include "./filter.html" %}
+
+ Navigators
+
+
+
{% include "./filter.html" %}
+
+
+
+
{% include "./list.html" %} {% endblock content %}
diff --git a/translations/templates/navigators/navigator.html b/translations/templates/navigators/navigator.html
index 9e5b04a0..ef31e30f 100644
--- a/translations/templates/navigators/navigator.html
+++ b/translations/templates/navigators/navigator.html
@@ -1,28 +1,40 @@
{% extends "base.html" %}{% block content %}
-
{{ navigator.name.text }}
-
-
-
-
-
-
- Description
-
+
+
+
+
+
+
+ Label
+ Actions (add)
+
+
+
+
+ {{ navigator.name }}
+
+
+
+
+
+
{% endblock content %}
diff --git a/translations/templates/pagination.html b/translations/templates/pagination.html
new file mode 100644
index 00000000..99253887
--- /dev/null
+++ b/translations/templates/pagination.html
@@ -0,0 +1,63 @@
+
diff --git a/translations/templates/programs/filter.html b/translations/templates/programs/filter.html
index 7238f167..314f7b8f 100644
--- a/translations/templates/programs/filter.html
+++ b/translations/templates/programs/filter.html
@@ -1,5 +1,20 @@
-
diff --git a/translations/templates/programs/list.html b/translations/templates/programs/list.html
index 16091dcc..f3d972c8 100644
--- a/translations/templates/programs/list.html
+++ b/translations/templates/programs/list.html
@@ -1,8 +1,128 @@
-
- {% for program in programs %}
-
- {{ program.name.text }}
- Edit
-
- {% endfor %}
-
+{% load static %}
+
+
+
+
+ ID
+ Name
+ Abbreviated Name
+ Active
+ Name Label
+ Actions
+
+
+
+ {% for program in page_obj %}
+
+ {{ program.id }}
+ {{ program.name.text }}
+ {{ program.name_abbreviated }}
+ {{ program.active }}
+ {{ program.name }}
+
+
+
+
+ Go to
+ expand_more
+
+
+
+
+
+
+ {% empty %}
+
+ No results
+
+ {% endfor %}
+
+
+
+ Total: {{ page_obj.paginator.count }}
+
+
+
+
+ {% include "../pagination.html" %}
+
+
+
diff --git a/translations/templates/programs/main.html b/translations/templates/programs/main.html
index facda867..bbf5665d 100644
--- a/translations/templates/programs/main.html
+++ b/translations/templates/programs/main.html
@@ -1,6 +1,19 @@
{% extends "base.html" %} {% block content %}
-
Programs
-
New
-{% include "./filter.html" %}
+
+ Programs
+
+
+
{% include "./filter.html" %}
+
+
+
+
{% include "./list.html" %} {% endblock content %}
diff --git a/translations/templates/programs/program.html b/translations/templates/programs/program.html
index fdb57bbb..66ea6efd 100644
--- a/translations/templates/programs/program.html
+++ b/translations/templates/programs/program.html
@@ -1,55 +1,72 @@
{% extends "base.html" %}{% block content %}
-
{{ program.name.text }}
-
-
-
-
-
-
-
-
-
-
-
-
-
Estimated value
+
+
+
+
+
+
+ Abbreviated Name
+ Actions (add)
+
+
+
+
+ {{ program.name_abbreviated }}
+
+
+
+
+
+
{% endblock content %}
diff --git a/translations/templates/translations.html b/translations/templates/translations.html
index f85a2fdb..ce5a6993 100644
--- a/translations/templates/translations.html
+++ b/translations/templates/translations.html
@@ -1,8 +1,90 @@
-
- {% for translation in translations %}
-
- {{translation.label}}
- Edit
-
- {% endfor %}
-
+{% load static %}
+
+
+
+
+
+ ID
+
+ Label
+
+ Model
+
+ Abbreviated/External Name
+
+ Field
+
+ Edited
+ Active
+ Actions
+
+
+
+ {% for translation in page_obj %}
+
+ {{ translation.entry_id }}
+ {{ translation.label }}
+ {{ translation.model_name|capfirst }}
+ {{ translation.display_name }}
+ {{ translation.field_name }}
+ {{ translation.edited }}
+ {{ translation.active }}
+
+
+
+
+ Go to
+ expand_more
+
+
+
+
+
+
+ {% empty %}
+
+ No results
+
+ {% endfor %}
+
+
+
+ Total: {{ page_obj.paginator.count }}
+
+
+
+
+ {% include "pagination.html" %}
+
diff --git a/translations/templates/urgent_needs/filter.html b/translations/templates/urgent_needs/filter.html
index 963bdd00..9f5c902c 100644
--- a/translations/templates/urgent_needs/filter.html
+++ b/translations/templates/urgent_needs/filter.html
@@ -1,9 +1,20 @@
+{% load static %}
diff --git a/translations/templates/urgent_needs/list.html b/translations/templates/urgent_needs/list.html
index 6677f55a..e4658c72 100644
--- a/translations/templates/urgent_needs/list.html
+++ b/translations/templates/urgent_needs/list.html
@@ -1,8 +1,101 @@
-
- {% for urgent_need in urgent_needs %}
-
- {{ urgent_need.name.text }}
- Edit
-
- {% endfor %}
-
+{% load static %}
+
+
+
+
+ ID
+ Name
+ External Name
+ Name Label
+ Actions
+
+
+
+ {% for urgent_need in page_obj %}
+
+ {{ urgent_need.id }}
+ {{ urgent_need.name.text }}
+ {{ urgent_need.external_name }}
+ {{ urgent_need.name }}
+
+
+
+
+ Go to
+ expand_more
+
+
+
+
+
+
+ {% empty %}
+
+ No results
+
+ {% endfor %}
+
+
+
+ Total: {{ page_obj.paginator.count }}
+
+
+
+
+ {% include "../pagination.html" %}
+
+
+
diff --git a/translations/templates/urgent_needs/main.html b/translations/templates/urgent_needs/main.html
index a979ed8c..9d8d1356 100644
--- a/translations/templates/urgent_needs/main.html
+++ b/translations/templates/urgent_needs/main.html
@@ -1,6 +1,19 @@
{% extends "base.html" %} {% block content %}
-
Urgent Needs
-
New
-{% include "./filter.html" %}
+
+ Urgent Needs
+
+
+
{% include "./filter.html" %}
+
+
+
+
{% include "./list.html" %} {% endblock content %}
diff --git a/translations/templates/urgent_needs/urgent_need.html b/translations/templates/urgent_needs/urgent_need.html
index d70b15ee..17285066 100644
--- a/translations/templates/urgent_needs/urgent_need.html
+++ b/translations/templates/urgent_needs/urgent_need.html
@@ -1,29 +1,44 @@
{% extends "base.html" %}{% block content %}
-
{{ urgent_need.name.text }}
-
-
-
-
-
-
-
Warning
+
+
+
+
+
+
+ Label
+ Actions (add)
+
+
+
+
+ {{ urgent_need.name }}
+
+
+
+
+
+
{% endblock content %}
diff --git a/translations/templates/util/create_form.html b/translations/templates/util/create_form.html
index 587bc041..04172e39 100644
--- a/translations/templates/util/create_form.html
+++ b/translations/templates/util/create_form.html
@@ -1,6 +1,24 @@
{% extends "base.html" %} {% block content %}
-
Create
-
+
+
+
+
{% endblock content %}
diff --git a/translations/urls.py b/translations/urls.py
index 62e42591..aa5c9cc4 100644
--- a/translations/urls.py
+++ b/translations/urls.py
@@ -4,7 +4,7 @@
urlpatterns = [
path('', views.TranslationView.as_view()),
- path('admin', views.admin_view),
+ path('admin', views.admin_view, name='translations_api_url'),
path('admin/filter', views.filter_view),
path('admin/create', views.create_translation_view),
path('admin/programs', views.programs_view),
@@ -23,7 +23,7 @@
path('admin/urgent_needs/filter', views.urgent_need_filter_view),
path('admin/urgent_needs/create', views.create_urgent_need_view),
path('admin/urgent_needs/
', views.urgent_need_view),
- path('admin/', views.translation_view),
+ path('admin/', views.translation_view, name='translation_admin_url'),
path('admin//', views.edit_translation),
path('admin///auto', views.auto_translate),
]
diff --git a/translations/views.py b/translations/views.py
index 049ba724..aa014dc5 100644
--- a/translations/views.py
+++ b/translations/views.py
@@ -1,3 +1,5 @@
+import re
+from django.core.paginator import Paginator
from django.shortcuts import render
from django.conf import settings
from .models import Translation
@@ -8,6 +10,7 @@
from django.db.models import ProtectedError
from programs.models import Program, Navigator, UrgentNeed, Document
from phonenumber_field.formfields import PhoneNumberField
+from phonenumber_field.widgets import PhoneNumberPrefixWidget
from django.contrib.auth.decorators import login_required
from django.contrib.admin.views.decorators import staff_member_required
from integrations.services.google_translate.integration import Translate
@@ -28,18 +31,32 @@ def get(self, request):
class NewTranslationForm(forms.Form):
- label = forms.CharField(max_length=128)
- default_message = forms.CharField(widget=forms.Textarea(attrs={'name': 'text', 'rows': 3, 'cols': 50}))
+ label = forms.CharField(max_length=128, widget=forms.TextInput(
+ attrs={'class': 'input'}))
+ default_message = forms.CharField(widget=forms.Textarea(
+ attrs={'name': 'text', 'rows': 3, 'cols': 50, 'class': 'textarea'}))
@login_required(login_url='/admin/login')
@staff_member_required
def admin_view(request):
if request.method == 'GET':
- translations = Translation.objects.all()
+ translations = Translation.objects.all().order_by('id')
+ # Display 50 translations per page
+ paginator = Paginator(translations, 50)
+ page_number = request.GET.get('page')
+ page_obj = paginator.get_page(page_number)
+
+ for translation in page_obj:
+ used_by_info = translation.used_by
+
+ translation.entry_id = used_by_info['id']
+ translation.model_name = used_by_info['model_name']
+ translation.field_name = used_by_info['field_name']
+ translation.display_name = used_by_info['display_name']
context = {
- 'translations': translations,
+ 'page_obj': page_obj
}
return render(request, "main.html", context)
@@ -47,12 +64,15 @@ def admin_view(request):
form = NewTranslationForm(request.POST)
if form.is_valid():
text = form['default_message'].value()
- translation = Translation.objects.add_translation(form['label'].value(), text)
+ translation = Translation.objects.add_translation(
+ form['label'].value(), text)
- auto_translations = Translate().bulk_translate(['__all__'], [text])[text]
+ auto_translations = Translate().bulk_translate(
+ ['__all__'], [text])[text]
for [language, auto_text] in auto_translations.items():
- Translation.objects.edit_translation_by_id(translation.id, language, auto_text, False)
+ Translation.objects.edit_translation_by_id(
+ translation.id, language, auto_text, False)
response = HttpResponse()
response.headers["HX-Redirect"] = f"/api/translations/admin/{translation.id}"
@@ -74,22 +94,35 @@ def create_translation_view(request):
@staff_member_required
def filter_view(request):
translations = Translation.objects \
- .filter(label__contains=request.GET.get('label', '')) \
- .translated(text__contains=request.GET.get('text', ''))
+ .filter(label__icontains=request.GET.get('label', '')) \
+ .translated(text__icontains=request.GET.get('text', ''))
+ paginator = Paginator(translations, 50)
+ page_number = request.GET.get('page')
+ page_obj = paginator.get_page(page_number)
+
+ for translation in page_obj:
+ used_by_info = translation.used_by
+
+ translation.entry_id = used_by_info['id']
+ translation.model_name = used_by_info['model_name']
+ translation.field_name = used_by_info['field_name']
+ translation.display_name = used_by_info['display_name']
context = {
- 'translations': translations
+ 'page_obj': page_obj
}
return render(request, "translations.html", context)
class TranslationForm(forms.Form):
- text = forms.CharField(widget=forms.Textarea(attrs={'name': 'text', 'rows': 3, 'cols': 50}), required=False)
+ text = forms.CharField(widget=forms.Textarea(
+ attrs={'name': 'text', 'rows': 3, 'cols': 50, 'class': 'textarea'}), required=False)
class LabelForm(forms.Form):
- label = forms.CharField(max_length=128)
+ label = forms.CharField(max_length=128, widget=forms.TextInput(
+ attrs={'class': 'input'}))
active = forms.BooleanField(required=False)
no_auto = forms.BooleanField(required=False)
@@ -98,10 +131,12 @@ class LabelForm(forms.Form):
@staff_member_required
def translation_view(request, id=0):
if request.method == 'GET':
- translation = Translation.objects.prefetch_related('translations').get(pk=id)
+ translation = Translation.objects.prefetch_related(
+ 'translations').get(pk=id)
langs = [lang['code'] for lang in settings.PARLER_LANGUAGES[None]]
- translations = {t.language_code: TranslationForm({'text': t.text}) for t in translation.translations.all()}
+ translations = {t.language_code: TranslationForm(
+ {'text': t.text}) for t in translation.translations.all()}
for lang in langs:
if lang not in translations:
@@ -132,9 +167,9 @@ def translation_view(request, id=0):
'label': translation.label,
'active': translation.active,
'no_auto': translation.no_auto
- })
+ }),
}
- return render(request, "edit/form.html", context)
+ return render(request, "edit/label_form.html", context)
elif request.method == 'DELETE':
try:
Translation.objects.get(pk=id).delete()
@@ -156,16 +191,20 @@ def edit_translation(request, id=0, lang='en-us'):
form = TranslationForm(request.POST)
if form.is_valid():
text = form['text'].value()
- translation = Translation.objects.edit_translation_by_id(id, lang, text)
+ translation = Translation.objects.edit_translation_by_id(
+ id, lang, text)
if lang == settings.LANGUAGE_CODE:
- translations = Translate().bulk_translate(['__all__'], [text])[text]
+ translations = Translate().bulk_translate(
+ ['__all__'], [text])[text]
for [language, translation] in translations.items():
- Translation.objects.edit_translation_by_id(id, language, translation, False)
+ Translation.objects.edit_translation_by_id(
+ id, language, translation, False)
parent = Translation.objects.get(pk=id)
- forms = {t.language_code: TranslationForm({'text': t.text}) for t in parent.translations.all()}
+ forms = {t.language_code: TranslationForm(
+ {'text': t.text}) for t in parent.translations.all()}
context = {
'translation': parent,
'langs': forms,
@@ -177,12 +216,14 @@ def edit_translation(request, id=0, lang='en-us'):
@staff_member_required
def auto_translate(request, id=0, lang='en-us'):
if request.method == 'POST':
- translation = Translation.objects.language(settings.LANGUAGE_CODE).get(pk=id)
+ translation = Translation.objects.language(
+ settings.LANGUAGE_CODE).get(pk=id)
auto = Translate().translate(lang, translation.text)
# Set text to manualy edited initially in order to update, and then set it to not edited
- new_translation = Translation.objects.edit_translation_by_id(translation.id, lang, auto)
+ new_translation = Translation.objects.edit_translation_by_id(
+ translation.id, lang, auto)
new_translation.edited = False
new_translation.save()
@@ -195,16 +236,22 @@ def auto_translate(request, id=0, lang='en-us'):
class NewProgramForm(forms.Form):
- name_abbreviated = forms.CharField(max_length=120)
+ name_abbreviated = forms.CharField(max_length=120, widget=forms.TextInput(
+ attrs={'class': 'input'}))
@login_required(login_url='/admin/login')
@staff_member_required
def programs_view(request):
if request.method == 'GET':
- programs = Program.objects.all()
+ programs = Program.objects.all().order_by('external_name')
+
+ paginator = Paginator(programs, 50)
+ page_number = request.GET.get('page')
+ page_obj = paginator.get_page(page_number)
+
context = {
- 'programs': programs
+ 'page_obj': page_obj
}
return render(request, 'programs/main.html', context)
@@ -247,30 +294,41 @@ def program_view(request, id=0):
@staff_member_required
def programs_filter_view(request):
if request.method == 'GET':
- programs = Program.objects.all()
- query = request.GET.get('name', '')
- programs = filter(lambda p: query in p.name.text, programs)
+ programs = Program.objects.filter(
+ name__translations__text__icontains=request.GET.get('name', '')).distinct().order_by('external_name')
+
+ paginator = Paginator(programs, 50)
+ page_number = request.GET.get('page')
+ page_obj = paginator.get_page(page_number)
context = {
- 'programs': programs
+ 'page_obj': page_obj
}
return render(request, 'programs/list.html', context)
class NewNavigatorForm(forms.Form):
- label = forms.CharField(max_length=50)
- phone_number = PhoneNumberField(required=False)
+ label = forms.CharField(max_length=50, widget=forms.TextInput(
+ attrs={'class': 'input'}))
+ phone_number = PhoneNumberField(required=False,
+ widget=forms.TextInput(
+ attrs={'class': 'input'})
+ )
@login_required(login_url='/admin/login')
@staff_member_required
def navigators_view(request):
if request.method == 'GET':
- navigators = Navigator.objects.all()
+ navigators = Navigator.objects.all().order_by('external_name')
+
+ paginator = Paginator(navigators, 50)
+ page_number = request.GET.get('page')
+ page_obj = paginator.get_page(page_number)
context = {
- 'navigators': navigators
+ 'page_obj': page_obj
}
return render(request, 'navigators/main.html', context)
@@ -282,7 +340,8 @@ def navigators_view(request):
form['phone_number'].value(),
)
response = HttpResponse()
- response.headers["HX-Redirect"] = f"/api/translations/admin/navigators/{navigator.id}"
+ response.headers[
+ "HX-Redirect"] = f"/api/translations/admin/navigators/{navigator.id}"
return response
@@ -314,32 +373,42 @@ def navigator_view(request, id=0):
@staff_member_required
def navigator_filter_view(request):
if request.method == 'GET':
- navigators = Navigator.objects.all()
- query = request.GET.get('name', '')
- navigators = filter(lambda p: query in p.name.text, navigators)
+ navigators = Navigator.objects.filter(
+ name__translations__text__icontains=request.GET.get('name', '')).distinct().order_by('external_name')
+
+ paginator = Paginator(navigators, 50)
+ page_number = request.GET.get('page')
+ page_obj = paginator.get_page(page_number)
context = {
- 'navigators': navigators
+ 'page_obj': page_obj
}
return render(request, 'navigators/list.html', context)
class NewUrgentNeedForm(forms.Form):
- label = forms.CharField(max_length=50)
- phone_number = PhoneNumberField(required=False)
+ label = forms.CharField(max_length=50, widget=forms.TextInput(
+ attrs={'class': 'input'}))
+ phone_number = PhoneNumberField(required=False,
+ widget=forms.TextInput(
+ attrs={'class': 'input'})
+ )
@login_required(login_url='/admin/login')
@staff_member_required
def urgent_needs_view(request):
if request.method == 'GET':
- urgent_needs = UrgentNeed.objects.all()
+ urgent_needs = UrgentNeed.objects.all().order_by('external_name')
+
+ paginator = Paginator(urgent_needs, 50)
+ page_number = request.GET.get('page')
+ page_obj = paginator.get_page(page_number)
context = {
- 'urgent_needs': urgent_needs
+ 'page_obj': page_obj
}
-
return render(request, 'urgent_needs/main.html', context)
if request.method == 'POST':
form = NewUrgentNeedForm(request.POST)
@@ -349,7 +418,8 @@ def urgent_needs_view(request):
form['phone_number'].value(),
)
response = HttpResponse()
- response.headers["HX-Redirect"] = f"/api/translations/admin/urgent_needs/{urgent_need.id}"
+ response.headers[
+ "HX-Redirect"] = f"/api/translations/admin/urgent_needs/{urgent_need.id}"
return response
@@ -381,36 +451,44 @@ def urgent_need_view(request, id=0):
@staff_member_required
def urgent_need_filter_view(request):
if request.method == 'GET':
- urgent_needs = UrgentNeed.objects.all()
- query = request.GET.get('name', '')
- urgent_needs = filter(lambda p: query in p.name.text, urgent_needs)
+ urgent_needs = UrgentNeed.objects.filter(
+ name__translations__text__icontains=request.GET.get('name', '')).distinct().order_by('external_name')
+
+ paginator = Paginator(urgent_needs, 50)
+ page_number = request.GET.get('page')
+ page_obj = paginator.get_page(page_number)
context = {
- 'urgent_needs': urgent_needs
+ 'page_obj': page_obj
}
return render(request, 'urgent_needs/list.html', context)
class NewDocumentForm(forms.Form):
- external_name = forms.CharField(max_length=120)
+ external_name = forms.CharField(max_length=120, widget=forms.TextInput(
+ attrs={'class': 'input'}))
@login_required(login_url='/admin/login')
@staff_member_required
def documents_view(request):
if request.method == 'GET':
- documents = Document.objects.all()
+ documents = Document.objects.all().order_by('external_name')
+
+ paginator = Paginator(documents, 50)
+ page_number = request.GET.get('page')
+ page_obj = paginator.get_page(page_number)
context = {
- 'documents': documents
+ 'page_obj': page_obj
}
-
return render(request, 'documents/main.html', context)
if request.method == 'POST':
form = NewDocumentForm(request.POST)
if form.is_valid():
- document = Document.objects.new_document(form['external_name'].value())
+ document = Document.objects.new_document(
+ form['external_name'].value())
response = HttpResponse()
response.headers["HX-Redirect"] = f"/api/translations/admin/documents/{document.id}"
return response
@@ -445,10 +523,15 @@ def document_view(request, id=0):
def document_filter_view(request):
if request.method == 'GET':
query = request.GET.get('name', '')
- documents = Document.objects.filter(external_name__contains=query)
+ documents = Document.objects.filter(
+ external_name__contains=query).order_by('external_name')
+
+ paginator = Paginator(documents, 50)
+ page_number = request.GET.get('page')
+ page_obj = paginator.get_page(page_number)
context = {
- 'documents': documents
+ 'page_obj': page_obj
}
return render(request, 'documents/list.html', context)