Ejemplo n.º 1
0
class Scenario(models.Model):
    plan = models.ForeignKey(Plan,
                             on_delete=models.CASCADE,
                             related_name='scenarios',
                             verbose_name=_('plan'))
    name = models.CharField(max_length=100, verbose_name=_('name'))
    identifier = IdentifierField()
    description = models.TextField(null=True,
                                   blank=True,
                                   verbose_name=_('description'))

    public_fields = [
        'id',
        'plan',
        'name',
        'identifier',
        'description',
    ]

    class Meta:
        unique_together = (('plan', 'identifier'), )
        verbose_name = _('scenario')
        verbose_name_plural = _('scenarios')

    def __str__(self):
        return self.name
Ejemplo n.º 2
0
class MonitoringQualityPoint(OrderedModel, TranslatableModel):
    plan = models.ForeignKey(Plan,
                             on_delete=models.CASCADE,
                             related_name='monitoring_quality_points',
                             verbose_name=_('plan'))
    identifier = IdentifierField()
    translations = TranslatedFields(
        name=models.CharField(
            max_length=100,
            verbose_name=_('name'),
        ),
        description_yes=models.CharField(
            max_length=200,
            verbose_name=_("description when action fulfills criteria")),
        description_no=models.CharField(
            max_length=200,
            verbose_name=_(
                "description when action doesn\'t fulfill criteria")),
    )

    class Meta:
        verbose_name = _('monitoring quality point')
        verbose_name_plural = _('monitoring quality points')
        unique_together = (('plan', 'order'), )
        ordering = ('plan', 'order')

    def __str__(self):
        return self.name
Ejemplo n.º 3
0
class Plan(ModelWithImage, models.Model):
    name = models.CharField(max_length=100, verbose_name=_('name'))
    identifier = IdentifierField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True, editable=False)
    site_url = models.URLField(blank=True,
                               null=True,
                               verbose_name=_('site URL'))
    actions_locked = models.BooleanField(
        default=False,
        verbose_name=_('actions locked'),
        help_text=_('Can actions be added and the official metadata edited?'),
    )
    allow_images_for_actions = models.BooleanField(
        default=True,
        verbose_name=_('allow images for actions'),
        help_text=_('Should custom images for individual actions be allowed'))

    general_admins = models.ManyToManyField(
        User,
        blank=True,
        related_name='general_admin_plans',
        verbose_name=_('general administrators'),
        help_text=_(
            'Users that can modify everything related to the action plan'))

    public_fields = [
        'id',
        'name',
        'identifier',
        'image_url',
        'action_schedules',
        'actions',
        'category_types',
        'action_statuses',
        'indicator_levels',
        'action_impacts',
        'blog_posts',
        'static_pages',
        'general_content',
        'impact_groups',
        'monitoring_quality_points',
    ]

    class Meta:
        verbose_name = _('plan')
        verbose_name_plural = _('plans')
        get_latest_by = 'created_at'
        ordering = ('created_at', )

    def __str__(self):
        return self.name

    def get_last_action_identifier(self):
        return self.actions.order_by('order').values_list('identifier',
                                                          flat=True).last()
Ejemplo n.º 4
0
class ActionDecisionLevel(models.Model):
    plan = models.ForeignKey(Plan,
                             on_delete=models.CASCADE,
                             related_name='action_decision_levels',
                             verbose_name=_('plan'))
    name = models.CharField(max_length=200, verbose_name=_('name'))
    identifier = IdentifierField()

    class Meta:
        unique_together = (('plan', 'identifier'), )

    def __str__(self):
        return self.name
Ejemplo n.º 5
0
class ActionImpact(OrderedModel):
    plan = models.ForeignKey(Plan,
                             on_delete=models.CASCADE,
                             related_name='action_impacts',
                             verbose_name=_('plan'))
    name = models.CharField(max_length=200, verbose_name=_('name'))
    identifier = IdentifierField()

    class Meta:
        unique_together = (('plan', 'identifier'), )
        ordering = ('plan', 'order')
        verbose_name = _('action impact class')
        verbose_name_plural = _('action impact classes')

    def __str__(self):
        return '%s (%s)' % (self.name, self.identifier)
Ejemplo n.º 6
0
class CategoryType(models.Model):
    plan = models.ForeignKey(Plan,
                             on_delete=models.CASCADE,
                             related_name='category_types')
    name = models.CharField(max_length=50)
    identifier = IdentifierField()

    class Meta:
        unique_together = (('plan', 'identifier'), )
        ordering = ('plan', 'name')
        verbose_name = _('category type')
        verbose_name_plural = _('category types')

    def __str__(self):
        return "%s (%s:%s)" % (self.name, self.plan.identifier,
                               self.identifier)
Ejemplo n.º 7
0
class ActionStatus(models.Model):
    plan = models.ForeignKey(Plan,
                             on_delete=models.CASCADE,
                             related_name='action_statuses',
                             verbose_name=_('plan'))
    name = models.CharField(max_length=50, verbose_name=_('name'))
    identifier = IdentifierField(max_length=20)
    is_completed = models.BooleanField(default=False,
                                       verbose_name=_('is completed'))

    class Meta:
        unique_together = (('plan', 'identifier'), )
        verbose_name = _('action status')
        verbose_name_plural = _('action statuses')

    def __str__(self):
        return self.name
Ejemplo n.º 8
0
class Category(OrderedModel, ModelWithImage):
    type = models.ForeignKey(CategoryType,
                             on_delete=models.PROTECT,
                             related_name='categories',
                             verbose_name=_('type'))
    identifier = IdentifierField()
    name = models.CharField(max_length=100, verbose_name=_('name'))
    parent = models.ForeignKey('self',
                               null=True,
                               blank=True,
                               on_delete=models.SET_NULL,
                               related_name='children',
                               verbose_name=_('parent category'))
    short_description = models.CharField(max_length=200,
                                         blank=True,
                                         verbose_name=_('short description'))

    class Meta:
        unique_together = (('type', 'identifier'), )
        verbose_name = _('category')
        verbose_name_plural = _('categories')
        ordering = ('type', 'identifier')

    def clean(self):
        if self.parent_id is not None:
            seen_categories = {self.id}
            obj = self.parent
            while obj is not None:
                if obj.id in seen_categories:
                    raise ValidationError({
                        'parent':
                        _('Parent forms a loop. Leave empty if top-level category.'
                          )
                    })
                seen_categories.add(obj.id)
                obj = obj.parent

    def __str__(self):
        if self.identifier and self.identifier[0].isnumeric():
            return "%s %s" % (self.identifier, self.name)
        else:
            return self.name
Ejemplo n.º 9
0
class ImpactGroup(TranslatableModel):
    plan = models.ForeignKey(Plan,
                             on_delete=models.CASCADE,
                             related_name='impact_groups',
                             verbose_name=_('plan'))
    identifier = IdentifierField()
    parent = models.ForeignKey('self',
                               on_delete=models.SET_NULL,
                               related_name='children',
                               null=True,
                               blank=True,
                               verbose_name=_('parent'))
    weight = models.FloatField(verbose_name=_('weight'), null=True, blank=True)
    color = models.CharField(max_length=16,
                             verbose_name=_('color'),
                             null=True,
                             blank=True,
                             validators=[validate_hex_color])

    translations = TranslatedFields(name=models.CharField(
        verbose_name=_('name'), max_length=200), )

    public_fields = [
        'id',
        'plan',
        'identifier',
        'parent',
        'weight',
        'name',
        'color',
        'actions',
    ]

    class Meta:
        unique_together = (('plan', 'identifier'), )
        verbose_name = _('impact group')
        verbose_name_plural = _('impact groups')
        ordering = ('plan', '-weight')

    def __str__(self):
        return self.name
Ejemplo n.º 10
0
class Category(OrderedModel, ModelWithImage):
    type = models.ForeignKey(CategoryType,
                             on_delete=models.CASCADE,
                             related_name='categories',
                             verbose_name=_('type'))
    identifier = IdentifierField()
    name = models.CharField(max_length=100, verbose_name=_('name'))
    parent = models.ForeignKey('self',
                               null=True,
                               blank=True,
                               on_delete=models.SET_NULL,
                               related_name='children',
                               verbose_name=_('parent category'))

    class Meta:
        unique_together = (('type', 'identifier'), )
        verbose_name = _('category')
        verbose_name_plural = _('categories')
        ordering = ('type', 'identifier')

    def __str__(self):
        return "%s %s" % (self.identifier, self.name)
Ejemplo n.º 11
0
class CategoryType(models.Model):
    plan = models.ForeignKey(Plan,
                             on_delete=models.CASCADE,
                             related_name='category_types')
    name = models.CharField(max_length=50, verbose_name=_('name'))
    identifier = IdentifierField()
    editable_for_actions = models.BooleanField(
        default=False,
        verbose_name=_('editable for actions'),
    )
    editable_for_indicators = models.BooleanField(
        default=False,
        verbose_name=_('editable for indicators'),
    )

    class Meta:
        unique_together = (('plan', 'identifier'), )
        ordering = ('plan', 'name')
        verbose_name = _('category type')
        verbose_name_plural = _('category types')

    def __str__(self):
        return "%s (%s:%s)" % (self.name, self.plan.identifier,
                               self.identifier)
Ejemplo n.º 12
0
class Indicator(models.Model):
    TIME_RESOLUTIONS = (('year', _('year')), ('month', _('month')),
                        ('week', _('week')), ('day', _('day')))
    LEVELS = (
        ('strategic', _('strategic')),
        ('tactical', _('tactical')),
        ('operational', _('operational')),
    )

    plans = models.ManyToManyField('actions.Plan',
                                   through='indicators.IndicatorLevel',
                                   blank=True,
                                   verbose_name=_('plans'))
    identifier = IdentifierField(null=True, blank=True)
    name = models.CharField(max_length=100, verbose_name=_('name'))
    quantity = models.ForeignKey(Quantity,
                                 related_name='indicators',
                                 on_delete=models.PROTECT,
                                 verbose_name=pgettext_lazy(
                                     'physical', 'quantity'),
                                 null=True,
                                 blank=True)
    unit = models.ForeignKey(Unit,
                             related_name='indicators',
                             on_delete=models.PROTECT,
                             verbose_name=_('unit'))
    description = models.TextField(null=True,
                                   blank=True,
                                   verbose_name=_('description'))
    categories = models.ManyToManyField('actions.Category', blank=True)
    time_resolution = models.CharField(max_length=50,
                                       choices=TIME_RESOLUTIONS,
                                       default=TIME_RESOLUTIONS[0][0],
                                       verbose_name=_('time resolution'))
    latest_graph = models.ForeignKey('IndicatorGraph',
                                     null=True,
                                     blank=True,
                                     related_name='+',
                                     on_delete=models.SET_NULL,
                                     editable=False)
    latest_value = models.ForeignKey('IndicatorValue',
                                     null=True,
                                     blank=True,
                                     related_name='+',
                                     on_delete=models.SET_NULL,
                                     editable=False)
    datasets = models.ManyToManyField(Dataset,
                                      blank=True,
                                      verbose_name=_('datasets'))

    # summaries = models.JSONField(null=True)
    # E.g.:
    # {
    #    "day_when_target_reached": "2079-01-22",
    #    "yearly_ghg_emission_reductions_left": "1963000",
    # }

    updated_at = models.DateTimeField(auto_now=True,
                                      editable=False,
                                      verbose_name=_('updated at'))
    created_at = models.DateTimeField(auto_now_add=True,
                                      editable=False,
                                      verbose_name=_('created at'))

    class Meta:
        verbose_name = _('indicator')
        verbose_name_plural = _('indicators')
        ordering = ('-updated_at', )

    def get_latest_graph(self):
        return self.graphs.latest()

    def set_latest_value(self):
        try:
            latest_value = self.values.latest()
        except IndicatorValue.DoesNotExist:
            latest_value = None
        if self.latest_value == latest_value:
            return
        self.latest_value = latest_value
        self.save(update_fields=['latest_value'])

    def has_current_data(self):
        return self.latest_value_id is not None

    def has_current_goals(self):
        now = timezone.now()
        return self.goals.filter(date__gte=now).exists()

    def has_datasets(self):
        return self.datasets.exists()

    has_datasets.short_description = _('Has datasets')
    has_datasets.boolean = True

    def has_data(self):
        return self.latest_value_id is not None

    has_data.short_description = _('Has data')
    has_data.boolean = True

    def has_graph(self):
        return self.latest_graph_id is not None

    has_graph.short_description = _('Has a graph')
    has_graph.boolean = True

    def __str__(self):
        return self.name
Ejemplo n.º 13
0
class Plan(ModelWithImage, models.Model):
    name = models.CharField(max_length=100, verbose_name=_('name'))
    identifier = IdentifierField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True, editable=False)
    site_url = models.URLField(blank=True,
                               null=True,
                               verbose_name=_('site URL'))
    actions_locked = models.BooleanField(
        default=False,
        verbose_name=_('actions locked'),
        help_text=_('Can actions be added and the official metadata edited?'),
    )
    allow_images_for_actions = models.BooleanField(
        default=True,
        verbose_name=_('allow images for actions'),
        help_text=_('Should custom images for individual actions be allowed'))

    general_admins = models.ManyToManyField(
        User,
        blank=True,
        related_name='general_admin_plans',
        verbose_name=_('general administrators'),
        help_text=_(
            'Users that can modify everything related to the action plan'))

    root_collection = models.OneToOneField(
        Collection,
        null=True,
        on_delete=models.PROTECT,
        editable=False,
        related_name='plan',
    )
    admin_group = models.OneToOneField(
        Group,
        null=True,
        on_delete=models.PROTECT,
        editable=False,
        related_name='admin_for_plan',
    )
    contact_person_group = models.OneToOneField(
        Group,
        null=True,
        on_delete=models.PROTECT,
        editable=False,
        related_name='contact_person_for_plan',
    )

    i18n = TranslationField(fields=['name'])

    public_fields = [
        'id',
        'name',
        'identifier',
        'image_url',
        'action_schedules',
        'actions',
        'category_types',
        'action_statuses',
        'indicator_levels',
        'action_impacts',
        'blog_posts',
        'static_pages',
        'general_content',
        'impact_groups',
        'monitoring_quality_points',
        'scenarios',
    ]

    class Meta:
        verbose_name = _('plan')
        verbose_name_plural = _('plans')
        get_latest_by = 'created_at'
        ordering = ('created_at', )

    def __str__(self):
        return self.name

    def get_last_action_identifier(self):
        return self.actions.order_by('order').values_list('identifier',
                                                          flat=True).last()

    def save(self, *args, **kwargs):
        ret = super().save(*args, **kwargs)

        update_fields = []
        if self.root_collection is None:
            obj = Collection(name=self.name)
            Collection.add_root(instance=obj)
            self.root_collection = obj
            update_fields.append('root_collection')

        if self.admin_group is None:
            obj = Group.objects.create(name='%s admins' % self.name)
            self.admin_group = obj
            update_fields.append('admin_group')

        if self.contact_person_group is None:
            obj = Group.objects.create(name='%s contact persons' % self.name)
            self.contact_person_group = obj
            update_fields.append('contact_person_group')

        if update_fields:
            super().save(update_fields=update_fields)
        return ret
Ejemplo n.º 14
0
class Action(ModelWithImage, OrderedModel, ClusterableModel):
    plan = models.ForeignKey(Plan,
                             on_delete=models.CASCADE,
                             default=latest_plan,
                             related_name='actions',
                             verbose_name=_('plan'))
    name = models.CharField(max_length=1000, verbose_name=_('name'))
    official_name = models.CharField(
        null=True,
        blank=True,
        max_length=1000,
        verbose_name=_('official name'),
        help_text=_('The name as approved by an official party'))
    identifier = IdentifierField(
        help_text=_('The identifier for this action (e.g. number)'))
    description = RichTextField(
        null=True,
        blank=True,
        verbose_name=_('description'),
        help_text=_('What does this action involve in more detail?'))
    impact = models.ForeignKey(
        'ActionImpact',
        blank=True,
        null=True,
        related_name='actions',
        on_delete=models.SET_NULL,
        verbose_name=_('impact'),
        help_text=_('The impact of this action'),
    )
    internal_priority = models.PositiveIntegerField(
        blank=True, null=True, verbose_name=_('internal priority'))
    internal_priority_comment = models.TextField(
        blank=True, null=True, verbose_name=_('internal priority comment'))
    status = models.ForeignKey(
        'ActionStatus',
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        verbose_name=_('status'),
    )
    merged_with = models.ForeignKey(
        'Action',
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        verbose_name=_('merged with action'),
        help_text=_('Set if this action is merged with another action'),
        related_name='merged_actions')
    completion = models.PositiveIntegerField(
        null=True,
        blank=True,
        verbose_name=_('completion'),
        editable=False,
        help_text=_('The completion percentage for this action'))
    schedule = models.ManyToManyField('ActionSchedule',
                                      blank=True,
                                      verbose_name=_('schedule'))
    decision_level = models.ForeignKey('ActionDecisionLevel',
                                       blank=True,
                                       null=True,
                                       related_name='actions',
                                       on_delete=models.SET_NULL,
                                       verbose_name=_('decision-making level'))
    categories = models.ManyToManyField('Category',
                                        blank=True,
                                        verbose_name=_('categories'))
    indicators = models.ManyToManyField('indicators.Indicator',
                                        blank=True,
                                        verbose_name=_('indicators'),
                                        through='indicators.ActionIndicator',
                                        related_name='actions')

    contact_persons_unordered = models.ManyToManyField(
        'people.Person',
        through='ActionContactPerson',
        blank=True,
        related_name='contact_for_actions',
        verbose_name=_('contact persons'))

    monitoring_quality_points = models.ManyToManyField(
        'MonitoringQualityPoint',
        blank=True,
        related_name='actions',
        editable=False,
    )

    updated_at = models.DateTimeField(editable=False,
                                      verbose_name=_('updated at'),
                                      default=timezone.now)

    sent_notifications = GenericRelation('notifications.SentNotification',
                                         related_query_name='action')

    objects = ActionQuerySet.as_manager()

    # Used by GraphQL + REST API code
    public_fields = [
        'id',
        'plan',
        'name',
        'official_name',
        'identifier',
        'description',
        'status',
        'completion',
        'schedule',
        'decision_level',
        'responsible_parties',
        'categories',
        'indicators',
        'contact_persons',
        'updated_at',
        'tasks',
        'related_indicators',
        'impact',
        'status_updates',
        'merged_with',
        'merged_actions',
        'impact_groups',
        'monitoring_quality_points',
    ]

    class Meta:
        verbose_name = _('action')
        verbose_name_plural = _('actions')
        ordering = ('plan', 'order')
        index_together = (('plan', 'order'), )
        permissions = (('admin_action', _("Can administrate all actions")), )

    def __str__(self):
        return "%s. %s" % (self.identifier, self.name)

    def clean(self):
        if self.merged_with is not None:
            other = self.merged_with
            if other.merged_with == self:
                raise ValidationError(
                    {'merged_with': _('Other action is merged with this one')})
        # FIXME: Make sure FKs and M2Ms point to objects that are within the
        # same action plan.

    def is_merged(self):
        return self.merged_with_id is not None

    def is_active(self):
        return not self.is_merged() and (self.status is None
                                         or not self.status.is_completed)

    def get_next_action(self):
        return Action.objects.filter(plan=self.plan_id,
                                     order__gt=self.order).unmerged().first()

    def get_previous_action(self):
        return Action.objects.filter(
            plan=self.plan_id,
            order__lt=self.order).unmerged().order_by('-order').first()

    def _calculate_status_from_indicators(self):
        progress_indicators = self.related_indicators.filter(
            indicates_action_progress=True)
        total_completion = 0
        total_indicators = 0
        is_late = False

        for action_ind in progress_indicators:
            ind = action_ind.indicator
            try:
                latest_value = ind.values.latest()
            except ind.values.model.DoesNotExist:
                continue

            start_value = ind.values.first()

            try:
                last_goal = ind.goals.filter(plan=self.plan).latest()
            except ind.goals.model.DoesNotExist:
                continue

            diff = last_goal.value - start_value.value

            if not diff:
                # Avoid divide by zero
                continue

            completion = (latest_value.value - start_value.value) / diff
            total_completion += completion
            total_indicators += 1

            # Figure out if the action is late or not by comparing
            # the latest measured value to the closest goal
            closest_goal = ind.goals.filter(
                plan=self.plan, date__lte=latest_value.date).last()
            if closest_goal is None:
                continue

            # Are we supposed to up or down?
            if diff > 0:
                # Up!
                if closest_goal.value - latest_value.value > 0:
                    is_late = True
            else:
                # Down
                if closest_goal.value - latest_value.value < 0:
                    is_late = True

        if not total_indicators:
            return None

        # Return average completion
        completion = int((total_completion / total_indicators) * 100)
        return dict(completion=completion, is_late=is_late)

    def _calculate_completion_from_tasks(self, tasks):
        if not tasks:
            return None
        n_completed = len(
            list(filter(lambda x: x.completed_at is not None, tasks)))
        return dict(completion=int(n_completed * 100 / len(tasks)))

    def _determine_status(self, tasks, indicator_status):
        statuses = self.plan.action_statuses.all()
        if not statuses:
            return None

        by_id = {x.identifier: x for x in statuses}
        KNOWN_IDS = {'not_started', 'on_time', 'late'}
        # If the status set is not something we can handle, bail out.
        if not KNOWN_IDS.issubset(set(by_id.keys())):
            logger.error('Unknown action status IDs: %s' % set(by_id.keys()))
            return None

        if indicator_status is not None and indicator_status.get('is_late'):
            return by_id['late']

        today = date.today()

        def is_late(task):
            if task.due_at is None or task.completed_at is not None:
                return False
            return today > task.due_at

        late_tasks = list(filter(is_late, tasks))
        if not late_tasks:
            completed_tasks = list(
                filter(lambda x: x.completed_at is not None, tasks))
            if not completed_tasks:
                return by_id['not_started']
            else:
                return by_id['on_time']

        return by_id['late']

    def recalculate_status(self):
        if self.merged_with is not None:
            return

        if self.status is not None and self.status.is_completed:
            if self.completion != 100:
                self.completion = 100
                self.save(update_fields=['completion'])
            return

        determine_monitoring_quality(self,
                                     self.plan.monitoring_quality_points.all())

        tasks = self.tasks.exclude(state=ActionTask.CANCELLED).only(
            'due_at', 'completed_at')
        update_fields = []

        indicator_status = self._calculate_status_from_indicators()
        if indicator_status:
            new_completion = indicator_status['completion']
        else:
            new_completion = None

        if self.completion != new_completion:
            update_fields.append('completion')
            self.completion = new_completion
            self.updated_at = timezone.now()
            update_fields.append('updated_at')

        status = self._determine_status(tasks, indicator_status)
        if status is not None and status.id != self.status_id:
            self.status = status
            update_fields.append('status')

        if not update_fields:
            return
        self.save(update_fields=update_fields)

    def set_categories(self, type, categories):
        if isinstance(type, str):
            type = self.plan.category_types.get(identifier=type)
        all_cats = {x.identifier: x for x in type.categories.all()}

        existing_cats = set(self.categories.filter(type=type))
        new_cats = set()
        for cat in categories:
            if isinstance(cat, str):
                cat = all_cats[cat]
            new_cats.add(cat)

        for cat in existing_cats - new_cats:
            self.categories.remove(cat)
        for cat in new_cats - existing_cats:
            self.categories.add(cat)

    def get_notification_context(self):
        change_url = reverse('admin:actions_action_change', args=(self.id, ))
        return {
            'id': self.id,
            'identifier': self.identifier,
            'name': self.name,
            'change_url': change_url,
            'updated_at': self.updated_at
        }

    def has_contact_persons(self):
        return self.contact_persons.exists()

    has_contact_persons.short_description = _('Has contact persons')
    has_contact_persons.boolean = True

    def active_task_count(self):
        def task_active(task):
            return task.state != ActionTask.CANCELLED and not task.completed_at

        active_tasks = [task for task in self.tasks.all() if task_active(task)]
        return len(active_tasks)

    active_task_count.short_description = _('Active tasks')