Skip to content

Commit

Permalink
Merge pull request #3636 from unicef/staging
Browse files Browse the repository at this point in the history
Staging
  • Loading branch information
robertavram authored Mar 12, 2024
2 parents 5770049 + d95e281 commit f32f3b3
Show file tree
Hide file tree
Showing 60 changed files with 844 additions and 82 deletions.
62 changes: 62 additions & 0 deletions src/etools/applications/action_points/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ def is_satisfied(self):
return self.user == self.action_point.author


class ActionPointNotAuthorCondition(SimpleCondition):
predicate = 'user!=action_points_actionpoint.author'

def __init__(self, action_point, user):
self.action_point = action_point
self.user = user

def is_satisfied(self):
return self.user != self.action_point.author


class ActionPointAssigneeCondition(SimpleCondition):
predicate = 'user=action_points_actionpoint.assigned_to'

Expand All @@ -38,6 +49,17 @@ def is_satisfied(self):
return self.user == self.action_point.assigned_by


class ActionPointPotentialVerifierCondition(SimpleCondition):
predicate = 'user=action_points_actionpoint.potential_verifier'

def __init__(self, action_point, user):
self.action_point = action_point
self.user = user

def is_satisfied(self):
return self.user == self.action_point.potential_verifier


class RelatedActionPointCondition(SimpleCondition):
predicate = 'action_points_actionpoint.related_object'

Expand All @@ -56,3 +78,43 @@ def __init__(self, action_point):

def is_satisfied(self):
return self.action_point.related_object is None


class NotVerifiedActionPointCondition(SimpleCondition):
predicate = '!action_points_actionpoint.verified'

def __init__(self, action_point):
self.action_point = action_point

def is_satisfied(self):
return self.action_point.verified_by is None


class PotentialVerifierProvidedCondition(SimpleCondition):
predicate = 'action_points_actionpoint.potential_verifier_provided'

def __init__(self, action_point):
self.action_point = action_point

def is_satisfied(self):
return self.action_point.potential_verifier is not None


class HighPriorityActionPointCondition(SimpleCondition):
predicate = 'action_points_actionpoint.high_priority'

def __init__(self, action_point):
self.action_point = action_point

def is_satisfied(self):
return self.action_point.high_priority


class LowPriorityActionPointCondition(SimpleCondition):
predicate = '!action_points_actionpoint.high_priority'

def __init__(self, action_point):
self.action_point = action_point

def is_satisfied(self):
return not self.action_point.high_priority
8 changes: 6 additions & 2 deletions src/etools/applications/action_points/export/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@


class ActionPointCSVRenderer(ListSeperatorCSVRenderMixin, FriendlyCSVRenderer):

header = [
'ref', 'ref_link', 'cp_output', 'partner', 'office', 'section', 'category', 'assigned_to', 'due_date',
'status', 'high_priority', 'description', 'intervention', 'pd_ssfa', 'location', 'related_module',
'assigned_by', 'date_of_completion', 'related_ref', 'related_object_str', 'related_object_url', 'action_taken',
'date_of_verification', 'verified_by', 'is_adequate', 'potential_verifier',
]
labels = {
'ref': _('Ref. #'),
Expand All @@ -32,5 +32,9 @@ class ActionPointCSVRenderer(ListSeperatorCSVRenderMixin, FriendlyCSVRenderer):
'related_ref': _('Related Document No.'),
'related_object_str': _('Task/Trip Activity Reference No.'),
'related_object_url': _('Related Document URL'),
'action_taken': _('Actions Taken')
'action_taken': _('Actions Taken'),
'date_of_verification': _('Date of Verification'),
'verified_by': _('Verified By'),
'is_adequate': _('Verification: Is Adequate'),
'potential_verifier': _('Potential Verifier'),
}
4 changes: 4 additions & 0 deletions src/etools/applications/action_points/export/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class ActionPointExportSerializer(serializers.Serializer):
related_object_url = serializers.SerializerMethodField()
related_object_str = serializers.SerializerMethodField()
action_taken = serializers.SerializerMethodField()
date_of_verification = serializers.DateTimeField(format='%d/%m/%Y')
verified_by = serializers.CharField(source='verified_by.get_full_name', allow_null=True)
is_adequate = serializers.BooleanField()
potential_verifier = serializers.CharField(source='potential_verifier.get_full_name', allow_null=True)

def get_action_taken(self, obj):
return ";\n\n".join(["{} ({}): {}".format(c.user if c.user else '-', c.submit_date.strftime(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@
ActionPointAssigneeCondition,
ActionPointAuthorCondition,
ActionPointModuleCondition,
ActionPointNotAuthorCondition,
ActionPointPotentialVerifierCondition,
HighPriorityActionPointCondition,
LowPriorityActionPointCondition,
NotVerifiedActionPointCondition,
PotentialVerifierProvidedCondition,
RelatedActionPointCondition,
UnRelatedActionPointCondition,
)
from etools.applications.action_points.models import ActionPoint, PME, UNICEFUser
from etools.applications.action_points.models import ActionPoint, OperationsGroup, PME, UNICEFUser
from etools.applications.permissions2.conditions import GroupCondition, NewObjectCondition, ObjectStatusCondition
from etools.applications.permissions2.models import Permission
from etools.applications.permissions2.utils import get_model_target
Expand Down Expand Up @@ -55,6 +61,11 @@ class Command(BaseCommand):
related_action_point_edit = action_point_base_edit
not_related_action_point_edit = action_point_base_edit + action_point_pmp_relations

# verification
action_point_verification_process_edit = [
'action_points.actionpoint.is_adequate',
]

# common fields, should be visible always
action_point_list = [
'action_points.actionpoint.reference_number',
Expand All @@ -80,19 +91,27 @@ class Command(BaseCommand):

'action_points.actionpoint.created',
'action_points.actionpoint.date_of_completion',
'action_points.actionpoint.date_of_verification',
'action_points.actionpoint.verified_by',
'action_points.actionpoint.is_adequate',
'action_points.actionpoint.potential_verifier',

'action_points.actionpoint.comments',
'action_points.actionpoint.history',
]

unicef_user = 'unicef_user'
pme = 'pme'
operations = 'operations'
author = 'author'
assigned_by = 'assigned_by'
assignee = 'assignee'
potential_verifier = 'potential_verifier'

user_roles = {
pme: [GroupCondition.predicate_template.format(group=PME.name)],
operations: [GroupCondition.predicate_template.format(group=OperationsGroup.name)],
potential_verifier: [ActionPointPotentialVerifierCondition.predicate],
unicef_user: [GroupCondition.predicate_template.format(group=UNICEFUser.name)],
author: [ActionPointAuthorCondition.predicate],
assigned_by: [ActionPointAssignedByCondition.predicate],
Expand Down Expand Up @@ -148,6 +167,21 @@ def related_action_point(self):
def not_related_action_point(self):
return [UnRelatedActionPointCondition.predicate]

def not_verified_action_point(self):
return [NotVerifiedActionPointCondition.predicate]

def potential_verifier_provided(self):
return [PotentialVerifierProvidedCondition.predicate]

def high_priority_action_point(self):
return [HighPriorityActionPointCondition.predicate]

def low_priority_action_point(self):
return [LowPriorityActionPointCondition.predicate]

def not_author(self):
return [ActionPointNotAuthorCondition.predicate]

def handle(self, *args, **options):
self.verbosity = options.get('verbosity', 1)

Expand Down Expand Up @@ -202,9 +236,25 @@ def assign_permissions(self):
condition=self.action_point_status(ActionPoint.STATUSES.open) + self.not_related_action_point()
)

# completion is different for high & low priority action points
self.add_permission(
[self.pme, self.author, self.assigned_by, self.assignee],
[self.pme, self.assigned_by, self.assignee, self.author],
'action',
'action_points.actionpoint.complete',
condition=self.action_point_status(ActionPoint.STATUSES.open) + self.low_priority_action_point()
)

# if high priority, unlock verification process
self.add_permission(
[self.pme, self.operations, self.potential_verifier],
'edit',
self.action_point_verification_process_edit,
condition=self.action_point_status(ActionPoint.STATUSES.completed) + self.high_priority_action_point() + self.not_author() + self.not_verified_action_point()
)
# require potential verifier to be provided if transition performs by author even if it has PME group
self.add_permission(
[self.pme, self.assigned_by, self.assignee, self.author],
'action',
'action_points.actionpoint.complete',
condition=self.action_point_status(ActionPoint.STATUSES.open)
condition=self.action_point_status(ActionPoint.STATUSES.open) + self.high_priority_action_point()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 3.2.19 on 2024-02-12 07:17

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


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('action_points', '0014_auto_20210108_1634'),
]

operations = [
migrations.AddField(
model_name='actionpoint',
name='date_of_verification',
field=model_utils.fields.MonitorField(blank=True, default=None, monitor='verified_by', null=True, verbose_name='Date Action Point Verified'),
),
migrations.AddField(
model_name='actionpoint',
name='is_adequate',
field=models.BooleanField(default=False, verbose_name='Is Adequate'),
),
migrations.AddField(
model_name='actionpoint',
name='potential_verifier',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='action_points_to_verify', to=settings.AUTH_USER_MODEL, verbose_name='Potential Verifier'),
),
migrations.AddField(
model_name='actionpoint',
name='verified_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='verified_action_points', to=settings.AUTH_USER_MODEL, verbose_name='Verified By'),
),
]
23 changes: 22 additions & 1 deletion src/etools/applications/action_points/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ class ActionPoint(TimeStampedModel):
on_delete=models.CASCADE)
date_of_completion = MonitorField(verbose_name=_('Date Action Point Completed'), null=True, blank=True,
default=None, monitor='status', when=[STATUSES.completed])
date_of_verification = MonitorField(verbose_name=_('Date Action Point Verified'), null=True, blank=True,
default=None, monitor='verified_by')
comments = GenericRelation('django_comments.Comment', object_id_field='object_pk')
history = GenericRelation('unicef_snapshot.Activity', object_id_field='target_object_id',
content_type_field='target_content_type')
Expand All @@ -93,6 +95,15 @@ class ActionPoint(TimeStampedModel):
max_length=100,
null=True,
)

# verification
verified_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='verified_action_points',
verbose_name=_('Verified By'), on_delete=models.CASCADE, blank=True, null=True)
is_adequate = models.BooleanField(default=False, verbose_name=_('Is Adequate'))
potential_verifier = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='action_points_to_verify',
verbose_name=_('Potential Verifier'), on_delete=models.CASCADE,
blank=True, null=True)

tracker = FieldTracker(fields=['assigned_to', 'reference_number'])

objects = ActionPointManager()
Expand Down Expand Up @@ -232,6 +243,11 @@ def send_email(self, recipient, template_name, additional_context=None, cc=None)
)

def _do_complete(self, completed_by=None):
if self.potential_verifier:
self.send_email(
self.potential_verifier,
'action_points/action_point/action_point-available-for-verification',
)
self.send_email(self.assigned_by, 'action_points/action_point/completed', cc=[self.assigned_to.email],
additional_context={'completed_by': (completed_by or self.assigned_to).get_full_name()})

Expand All @@ -241,12 +257,17 @@ def _do_complete(self, completed_by=None):
ActionPointCompleteActionsTakenCheck.as_condition()
],
custom={'serializer': ActionPointCompleteSerializer})
def complete(self, completed_by=None):
def complete(self, completed_by=None, potential_verifier=None):
if potential_verifier:
self.potential_verifier = potential_verifier
self._do_complete(completed_by=completed_by)


PME = GroupWrapper(code='pme',
name='PME')

OperationsGroup = GroupWrapper(code='operations',
name='Operations')

UNICEFUser = GroupWrapper(code='unicef_user',
name='UNICEF User')
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from unicef_notification.utils import strip_text

name = 'action_points/action_point/action_point-available-for-verification'
defaults = {
'description': 'Action point available for verification',
'subject': '[eTools] ACTION POINT available for verification',

'content': strip_text("""
Dear {{ recipient }},
You were assigned as a verifier to an action point {% if action_point.partner %}related to:
Implementing Partner: {{ action_point.partner }}{% endif %}
Description: {{ action_point.description }}
Link: {{ action_point.object_url }}
"""),

'html_content': """
{% extends "email-templates/base" %}
{% block content %}
Dear {{ recipient }},<br/><br/>
You were assigned as a verifier to an action point {% if action_point.partner %}related to:
<br/>
Implementing Partner: {{ action_point.partner }}{% endif %}<br/>
Description: {{ action_point.description }}<br/>
Link: <a href="{{ action_point.object_url }}">{{ action_point.reference_number }}</a>
{% endblock %}
"""
}
17 changes: 17 additions & 0 deletions src/etools/applications/action_points/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class Meta(ActionPointBaseSerializer.Meta):
fields = ActionPointBaseSerializer.Meta.fields + [
'related_module', 'cp_output', 'partner', 'intervention', 'location',
'engagement', 'psea_assessment', 'tpm_activity', 'travel_activity',
'date_of_verification',
]


Expand Down Expand Up @@ -171,16 +172,32 @@ def get_action(self, obj):
class ActionPointSerializer(WritableNestedSerializerMixin, ActionPointListSerializer):
comments = CommentSerializer(many=True, label=_('Actions Taken'), required=False)
history = HistorySerializer(many=True, label=_('History'), read_only=True, source='get_meaningful_history')
potential_verifier = MinimalUserSerializer(read_only=True, label=_('Potential Verifier'))
verified_by = MinimalUserSerializer(read_only=True, label=_('Verified By'))

related_object_str = serializers.ReadOnlyField(label=_('Related Document'))
related_object_url = serializers.ReadOnlyField()

class Meta(WritableNestedSerializerMixin.Meta, ActionPointListSerializer.Meta):
fields = ActionPointListSerializer.Meta.fields + [
'comments', 'history', 'related_object_str', 'related_object_url',
'potential_verifier', 'verified_by', 'is_adequate',
]

def validate_category(self, value):
if value and value.module != self.instance.related_module:
raise serializers.ValidationError(_('Category doesn\'t belong to selected module.'))
return value

def validate(self, attrs):
validated_data = super().validate(attrs)
if 'potential_verifier' in validated_data:
if self.instance.author == validated_data['potential_verifier']:
raise serializers.ValidationError(_("Author cannot verify own action point."))

return validated_data

def update(self, instance, validated_data):
if 'is_adequate' in validated_data:
validated_data['verified_by'] = self.get_user()
return super().update(instance, validated_data)
Loading

0 comments on commit f32f3b3

Please sign in to comment.