Skip to content

Commit aa06d83

Browse files
authored
Merge pull request #112 from openimis/feature/project
Activity & Project backend to facilitate conditional cash transfer
2 parents 7384779 + 2448b61 commit aa06d83

17 files changed

+880
-30
lines changed

locale/en/LC_MESSAGES/django.po

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ msgstr "BenefitPlan name %(name) already exists"
3737
msgid "social_protection.validation.field_empty"
3838
msgstr "%(field) can't be empty"
3939

40+
msgid "social_protection.validation.project.name_exists"
41+
msgstr "Project name %(name) already exists"
42+
4043
msgid "Invalid file type. Allowed: .csv, .xls, .xlsx"
4144
msgstr "Invalid file type. Allowed: .csv, .xls, .xlsx"
4245

social_protection/admin.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
11
from django.contrib import admin
2+
from .models import Activity
23

3-
# Register your models here.
4+
@admin.register(Activity)
5+
class ActivityAdmin(admin.ModelAdmin):
6+
list_display = [
7+
'id',
8+
'name',
9+
'version',
10+
'is_deleted',
11+
'user_updated',
12+
'date_updated',
13+
]
14+
readonly_fields = [
15+
'user_created', 'date_created', 'user_updated', 'date_updated', 'version'
16+
]
17+
exclude = ['date_valid_from', 'date_valid_to', 'json_ext', 'replacement_uuid']
18+
search_fields = ['name']
19+
20+
def save_model(self, request, obj, form, change):
21+
obj.save(user=request.user)

social_protection/apps.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
"gql_schema_create_perms": ["171002"],
1919
"gql_schema_update_perms": ["171003"],
2020
"gql_schema_delete_perms": ["171004"],
21+
"gql_activity_search_perms": ["208001"],
22+
"gql_project_search_perms": ["209001"],
23+
"gql_project_create_perms": ["209002"],
2124

2225

2326
# Create task for model instead of performing crud action
@@ -62,6 +65,7 @@
6265
class SocialProtectionConfig(AppConfig):
6366
default_auto_field = 'django.db.models.BigAutoField'
6467
name = 'social_protection'
68+
verbose_name = 'Social Protection'
6569

6670
gql_benefit_plan_search_perms = None
6771
gql_benefit_plan_create_perms = None
@@ -76,6 +80,9 @@ class SocialProtectionConfig(AppConfig):
7680
gql_schema_create_perms = None
7781
gql_schema_update_perms = None
7882
gql_schema_delete_perms = None
83+
gql_activity_search_perms = None
84+
gql_project_search_perms = None
85+
gql_project_create_perms = None
7986

8087
gql_check_benefit_plan_update = None
8188
gql_check_beneficiary_crud = None

social_protection/gql_mutations.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
from core.schema import OpenIMISMutation
99
from social_protection.apps import SocialProtectionConfig
1010
from social_protection.models import (
11-
BenefitPlan,
11+
BenefitPlan, Project, Activity,
1212
Beneficiary, GroupBeneficiary, BeneficiaryStatus, BenefitPlanMutation
1313
)
1414
from social_protection.services import (
15-
BenefitPlanService,
15+
BenefitPlanService, ProjectService,
1616
BeneficiaryService, GroupBeneficiaryService
1717
)
18+
from location.models import Location
1819

1920

2021
def check_perms_for_field(user, permission, data, field_string):
@@ -441,3 +442,46 @@ def _mutate(cls, user, **data):
441442

442443
class Input(OpenIMISMutation.Input):
443444
ids = graphene.List(graphene.UUID)
445+
446+
class CreateProjectInputType(OpenIMISMutation.Input):
447+
benefit_plan_id = graphene.ID(required=True)
448+
name = graphene.String(required=True)
449+
status = graphene.String(required=False)
450+
activity_id = graphene.ID(required=True)
451+
location_id = graphene.ID(required=True)
452+
target_beneficiaries = graphene.Int(required=True)
453+
working_days = graphene.Int(required=True)
454+
455+
456+
class CreateProjectMutation(BaseHistoryModelCreateMutationMixin, BaseMutation):
457+
_mutation_class = "CreateProjectMutation"
458+
_mutation_module = "social_protection"
459+
_model = Project
460+
461+
@classmethod
462+
def _validate_mutation(cls, user, **data):
463+
if isinstance(user, AnonymousUser) or not user.has_perms(
464+
SocialProtectionConfig.gql_project_create_perms
465+
):
466+
raise ValidationError("mutation.authentication_required")
467+
468+
@classmethod
469+
def _mutate(cls, user, **data):
470+
if "client_mutation_id" in data:
471+
data.pop("client_mutation_id")
472+
if "client_mutation_label" in data:
473+
data.pop("client_mutation_label")
474+
475+
data["benefit_plan"] = BenefitPlan.objects.get(id=data.pop("benefit_plan_id"))
476+
data["activity"] = Activity.objects.get(id=data.pop("activity_id"))
477+
data["location"] = Location.objects.get(uuid=data.pop("location_id"))
478+
data.setdefault("status", Project._meta.get_field("status").get_default())
479+
480+
service = ProjectService(user)
481+
res = service.create(data)
482+
483+
return res if not res['success'] else None
484+
485+
class Input(CreateProjectInputType):
486+
pass
487+

social_protection/gql_queries.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
from individual.gql_queries import IndividualGQLType, GroupGQLType, \
1111
IndividualDataSourceUploadGQLType
1212
from social_protection.apps import SocialProtectionConfig
13-
from social_protection.models import Beneficiary, BenefitPlan, GroupBeneficiary, BenefitPlanDataUploadRecords
13+
from social_protection.models import (
14+
Beneficiary, BenefitPlan, GroupBeneficiary, BenefitPlanDataUploadRecords,
15+
Activity, Project,
16+
)
1417

1518

1619
def _have_permissions(user, permission):
@@ -200,3 +203,54 @@ def resolve_beneficiary_data_schema(self, info):
200203

201204
def resolve_has_payment_plans(self, info):
202205
return PaymentPlan.objects.filter(benefit_plan_id=self.id).exists()
206+
207+
208+
class ActivityFilter(django_filters.FilterSet):
209+
class Meta:
210+
model = Activity
211+
fields = {
212+
"id": ["exact"],
213+
"name": ["exact", "iexact", "startswith", "istartswith", "contains", "icontains"],
214+
"date_created": ["exact", "lt", "lte", "gt", "gte"],
215+
"date_updated": ["exact", "lt", "lte", "gt", "gte"],
216+
"is_deleted": ["exact"],
217+
"version": ["exact"],
218+
}
219+
220+
221+
class ActivityGQLType(DjangoObjectType, JsonExtMixin):
222+
uuid = graphene.String(source='uuid')
223+
224+
class Meta:
225+
model = Activity
226+
interfaces = (graphene.relay.Node,)
227+
filterset_class = ActivityFilter
228+
connection_class = ExtendedConnection
229+
230+
231+
class ProjectFilter(django_filters.FilterSet):
232+
class Meta:
233+
model = Project
234+
fields = {
235+
"id": ["exact"],
236+
"name": ["exact", "iexact", "startswith", "istartswith", "contains", "icontains"],
237+
'status': ['exact', 'icontains'],
238+
'benefit_plan__id': ['exact'],
239+
'activity__id': ['exact'],
240+
'location__id': ['exact'],
241+
'target_beneficiaries': ['exact', 'gte', 'lte'],
242+
'working_days': ['exact', 'gte', 'lte'],
243+
"date_created": ["exact", "lt", "lte", "gt", "gte"],
244+
"date_updated": ["exact", "lt", "lte", "gt", "gte"],
245+
"is_deleted": ["exact"],
246+
"version": ["exact"],
247+
}
248+
249+
class ProjectGQLType(DjangoObjectType, JsonExtMixin):
250+
uuid = graphene.String(source='uuid')
251+
252+
class Meta:
253+
model = Project
254+
interfaces = (graphene.relay.Node,)
255+
filterset_class = ProjectFilter
256+
connection_class = ExtendedConnection
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Generated by Django 4.2.20 on 2025-04-04 10:09
2+
3+
import core.fields
4+
import datetime
5+
import dirtyfields.dirtyfields
6+
from django.conf import settings
7+
from django.db import migrations, models
8+
import django.db.models.deletion
9+
import simple_history.models
10+
11+
12+
class Migration(migrations.Migration):
13+
14+
dependencies = [
15+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16+
('social_protection', '0014_replace_benefitplan_max_beneficiaries_values'),
17+
]
18+
19+
operations = [
20+
migrations.CreateModel(
21+
name='HistoricalActivity',
22+
fields=[
23+
('id', models.UUIDField(db_column='UUID', db_index=True, default=None, editable=False)),
24+
('is_deleted', models.BooleanField(db_column='isDeleted', default=False)),
25+
('json_ext', models.JSONField(blank=True, db_column='Json_ext', null=True)),
26+
('date_created', core.fields.DateTimeField(db_column='DateCreated', default=datetime.datetime.now, null=True)),
27+
('date_updated', core.fields.DateTimeField(db_column='DateUpdated', default=datetime.datetime.now, null=True)),
28+
('version', models.IntegerField(default=1)),
29+
('date_valid_from', core.fields.DateTimeField(db_column='DateValidFrom', default=datetime.datetime.now)),
30+
('date_valid_to', core.fields.DateTimeField(blank=True, db_column='DateValidTo', null=True)),
31+
('replacement_uuid', models.UUIDField(blank=True, db_column='ReplacementUUID', null=True)),
32+
('name', models.CharField(db_index=True, max_length=255)),
33+
('history_id', models.AutoField(primary_key=True, serialize=False)),
34+
('history_date', models.DateTimeField(db_index=True)),
35+
('history_change_reason', models.CharField(max_length=100, null=True)),
36+
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
37+
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
38+
('user_created', models.ForeignKey(blank=True, db_column='UserCreatedUUID', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
39+
('user_updated', models.ForeignKey(blank=True, db_column='UserUpdatedUUID', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
40+
],
41+
options={
42+
'verbose_name': 'historical Activity',
43+
'verbose_name_plural': 'historical Activities',
44+
'ordering': ('-history_date', '-history_id'),
45+
'get_latest_by': ('history_date', 'history_id'),
46+
},
47+
bases=(simple_history.models.HistoricalChanges, models.Model),
48+
),
49+
migrations.CreateModel(
50+
name='Activity',
51+
fields=[
52+
('id', models.UUIDField(db_column='UUID', default=None, editable=False, primary_key=True, serialize=False)),
53+
('is_deleted', models.BooleanField(db_column='isDeleted', default=False)),
54+
('json_ext', models.JSONField(blank=True, db_column='Json_ext', null=True)),
55+
('date_created', core.fields.DateTimeField(db_column='DateCreated', default=datetime.datetime.now, null=True)),
56+
('date_updated', core.fields.DateTimeField(db_column='DateUpdated', default=datetime.datetime.now, null=True)),
57+
('version', models.IntegerField(default=1)),
58+
('date_valid_from', core.fields.DateTimeField(db_column='DateValidFrom', default=datetime.datetime.now)),
59+
('date_valid_to', core.fields.DateTimeField(blank=True, db_column='DateValidTo', null=True)),
60+
('replacement_uuid', models.UUIDField(blank=True, db_column='ReplacementUUID', null=True)),
61+
('name', models.CharField(max_length=255, unique=True)),
62+
('user_created', models.ForeignKey(db_column='UserCreatedUUID', on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(class)s_user_created', to=settings.AUTH_USER_MODEL)),
63+
('user_updated', models.ForeignKey(db_column='UserUpdatedUUID', on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(class)s_user_updated', to=settings.AUTH_USER_MODEL)),
64+
],
65+
options={
66+
'verbose_name': 'Activity',
67+
'verbose_name_plural': 'Activities',
68+
},
69+
bases=(dirtyfields.dirtyfields.DirtyFieldsMixin, models.Model),
70+
),
71+
]
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Generated by Django 4.2.20 on 2025-04-22 13:14
2+
3+
import core.fields
4+
import core.utils
5+
import datetime
6+
import dirtyfields.dirtyfields
7+
from django.conf import settings
8+
from django.db import migrations, models
9+
import django.db.models.deletion
10+
import simple_history.models
11+
12+
13+
class Migration(migrations.Migration):
14+
15+
dependencies = [
16+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
17+
('location', '0018_auto_20230925_2243'),
18+
('social_protection', '0015_historicalactivity_activity'),
19+
]
20+
21+
operations = [
22+
migrations.CreateModel(
23+
name='Project',
24+
fields=[
25+
('id', models.UUIDField(db_column='UUID', default=None, editable=False, primary_key=True, serialize=False)),
26+
('is_deleted', models.BooleanField(db_column='isDeleted', default=False)),
27+
('json_ext', models.JSONField(blank=True, db_column='Json_ext', null=True)),
28+
('date_created', core.fields.DateTimeField(db_column='DateCreated', default=datetime.datetime.now, null=True)),
29+
('date_updated', core.fields.DateTimeField(db_column='DateUpdated', default=datetime.datetime.now, null=True)),
30+
('version', models.IntegerField(default=1)),
31+
('date_valid_from', core.fields.DateTimeField(db_column='DateValidFrom', default=datetime.datetime.now)),
32+
('date_valid_to', core.fields.DateTimeField(blank=True, db_column='DateValidTo', null=True)),
33+
('replacement_uuid', models.UUIDField(blank=True, db_column='ReplacementUUID', null=True)),
34+
('name', models.CharField(max_length=255)),
35+
('status', models.CharField(choices=[('PREPARATION', 'PREPARATION'), ('IN PROGRESS', 'IN PROGRESS'), ('COMPLETED', 'COMPLETED')], default='PREPARATION', max_length=100)),
36+
('target_beneficiaries', models.SmallIntegerField()),
37+
('working_days', models.SmallIntegerField()),
38+
('activity', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='social_protection.activity')),
39+
('benefit_plan', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='social_protection.benefitplan')),
40+
('location', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='location.location')),
41+
('user_created', models.ForeignKey(db_column='UserCreatedUUID', on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(class)s_user_created', to=settings.AUTH_USER_MODEL)),
42+
('user_updated', models.ForeignKey(db_column='UserUpdatedUUID', on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(class)s_user_updated', to=settings.AUTH_USER_MODEL)),
43+
],
44+
options={
45+
'abstract': False,
46+
},
47+
bases=(dirtyfields.dirtyfields.DirtyFieldsMixin, core.utils.CachedModelMixin, models.Model),
48+
),
49+
migrations.CreateModel(
50+
name='HistoricalProject',
51+
fields=[
52+
('id', models.UUIDField(db_column='UUID', db_index=True, default=None, editable=False)),
53+
('is_deleted', models.BooleanField(db_column='isDeleted', default=False)),
54+
('json_ext', models.JSONField(blank=True, db_column='Json_ext', null=True)),
55+
('date_created', core.fields.DateTimeField(db_column='DateCreated', default=datetime.datetime.now, null=True)),
56+
('date_updated', core.fields.DateTimeField(db_column='DateUpdated', default=datetime.datetime.now, null=True)),
57+
('version', models.IntegerField(default=1)),
58+
('date_valid_from', core.fields.DateTimeField(db_column='DateValidFrom', default=datetime.datetime.now)),
59+
('date_valid_to', core.fields.DateTimeField(blank=True, db_column='DateValidTo', null=True)),
60+
('replacement_uuid', models.UUIDField(blank=True, db_column='ReplacementUUID', null=True)),
61+
('name', models.CharField(max_length=255)),
62+
('status', models.CharField(choices=[('PREPARATION', 'PREPARATION'), ('IN PROGRESS', 'IN PROGRESS'), ('COMPLETED', 'COMPLETED')], default='PREPARATION', max_length=100)),
63+
('target_beneficiaries', models.SmallIntegerField()),
64+
('working_days', models.SmallIntegerField()),
65+
('history_id', models.AutoField(primary_key=True, serialize=False)),
66+
('history_date', models.DateTimeField(db_index=True)),
67+
('history_change_reason', models.CharField(max_length=100, null=True)),
68+
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
69+
('activity', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='social_protection.activity')),
70+
('benefit_plan', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='social_protection.benefitplan')),
71+
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
72+
('location', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='location.location')),
73+
('user_created', models.ForeignKey(blank=True, db_column='UserCreatedUUID', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
74+
('user_updated', models.ForeignKey(blank=True, db_column='UserUpdatedUUID', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
75+
],
76+
options={
77+
'verbose_name': 'historical project',
78+
'verbose_name_plural': 'historical projects',
79+
'ordering': ('-history_date', '-history_id'),
80+
'get_latest_by': ('history_date', 'history_id'),
81+
},
82+
bases=(simple_history.models.HistoricalChanges, models.Model),
83+
),
84+
]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django.db import migrations
2+
from core.utils import insert_role_right_for_system, remove_role_right_for_system
3+
4+
activity_rights = [208001, 208002, 208003, 208004]
5+
project_rights = [209001, 209002, 209003, 209004]
6+
imis_administrator_system = 64
7+
8+
9+
def add_rights(apps, schema_editor):
10+
for right_id in activity_rights + project_rights:
11+
insert_role_right_for_system(imis_administrator_system, right_id, apps)
12+
13+
14+
def remove_rights(apps, schema_editor):
15+
for right_id in activity_rights + project_rights:
16+
remove_role_right_for_system(imis_administrator_system, right_id, apps)
17+
18+
19+
class Migration(migrations.Migration):
20+
21+
dependencies = [
22+
('social_protection', '0016_project_historicalproject'),
23+
]
24+
25+
operations = [
26+
migrations.RunPython(add_rights, remove_rights),
27+
]

0 commit comments

Comments
 (0)