goals/models.py
class Goal(models.Model):
    plan = models.ForeignKey('goals.Plan', verbose_name='plan',
                             related_name='goals')
    code = models.CharField(_('Goal number'), max_length=10)
    name = models.CharField(_('Goal name'), max_length=255)
    caption = models.CharField(_('Caption'), max_length=255, blank=True)
    description = models.TextField(_('Goal description'), blank=True)
    image = models.ImageField(_('Image'),
                              upload_to='goals/goals/images',
                              blank=True, null=True)
    image_small = ImageSpecField(source='image',
                                 processors=[ResizeToFit(100, 100)],
                                 format='PNG',
                                 options={'quality': 90})
    image_medium = ImageSpecField(source='image',
                                  processors=[ResizeToFit(250, 250)],
                                  format='PNG',
                                  options={'quality': 90})
    image_large = ImageSpecField(source='image',
                                 processors=[ResizeToFit(700)],
                                 options={'quality': 80})
    slug = models.SlugField(_('Slug'), blank=True)
    created = models.DateTimeField(_('Created'), auto_now_add=True)
    last_modified = models.DateTimeField(_('Last modified'),
                                         auto_now=True)
    extras = HStoreField(_('Extras'), blank=True, null=True, default={})

    class Meta:
        verbose_name = _('Goal')
        verbose_name_plural = _('Goals')
        unique_together = ['code', 'plan']

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = self.get_slug()
        self.extras['plan_name'] = self.plan.name
        self.extras['plan_code'] = self.plan.code
        super(Goal, self).save(*args, **kwargs)

    def get_slug(self):
        if not self.slug:
            slug = slugify(self.name[:50])
            return slug
        return self.slug

    @cached_property
    def api_url(self):
        try:
            return reverse('goal-detail', args=[self.pk])
        except:
            # API isn't installed
            # FIXME: Catch a specific exception
            return ''

    @cached_property
    def image_url(self):
        if self.image:
            return self.image.url

    @cached_property
    def image_small_url(self):
        if self.image_small:
            return self.image_small.url

    @cached_property
    def image_medium_url(self):
        if self.image_medium:
            return self.image_medium.url

    @cached_property
    def image_large_url(self):
        if self.image_large:
            return self.image_large.url

    @cached_property
    def plan_name(self):
        return self.extras.get('plan_name', '') or self.plan.name

    @cached_property
    def plan_code(self):
        return self.extras.get('plan_code', '') or self.plan.code
goals/models.py
class Theme(models.Model):
    plan = models.ForeignKey('goals.Plan', verbose_name='Plans',
                             related_name='themes')
    name = models.CharField(_('Theme name'), max_length=255)
    code = models.CharField(_('Theme number'), max_length=10)
    caption = models.CharField(_('Caption'), max_length=255, blank=True)
    description = models.TextField(_('Theme description'), blank=True)
    image = models.ImageField(_('Image'),
                              upload_to='goals/themes/images',
                              blank=True, null=True)
    image_small = ImageSpecField(source='image',
                                 processors=[ResizeToFit(100, 100)],
                                 format='PNG',
                                 options={'quality': 90})
    image_medium = ImageSpecField(source='image',
                                  processors=[ResizeToFit(250, 250)],
                                  format='PNG',
                                  options={'quality': 90})
    image_large = ImageSpecField(source='image',
                                 processors=[ResizeToFit(700)],
                                 options={'quality': 80})
    slug = models.SlugField(_('Slug'), blank=True)
    created = models.DateTimeField(_('Created'), auto_now_add=True)
    last_modified = models.DateTimeField(_('Last modified'),
                                         auto_now=True)
    extras = HStoreField(_('Extras'), blank=True, null=True, default={})

    class Meta:
        verbose_name = _('Theme')
        verbose_name_plural = _('Themes')

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        self.extras['plan_name'] = self.plan.name
        self.extras['plan_code'] = self.plan.code
        if not self.slug:
            self.slug = self.get_slug()
        super(Theme, self).save(*args, **kwargs)

    @cached_property
    def image_url(self):
        if self.image:
            return self.image.url

    @cached_property
    def image_small_url(self):
        if self.image_small:
            return self.image_small.url

    @cached_property
    def image_medium_url(self):
        if self.image_medium:
            return self.image_medium.url

    @cached_property
    def image_large_url(self):
        if self.image_large:
            return self.image_large.url

    @cached_property
    def plan_name(self):
        return self.extras.get('plan_name', '') or self.plan.name

    @cached_property
    def plan_code(self):
        return self.extras.get('plan_code', '') or self.plan.code

    @cached_property
    def api_url(self):
        try:
            return reverse('theme-detail', args=[self.pk])
        except:
            # API isn't installed
            # FIXME: Catch a specific exception
            return ''
goals/models.py
class Area(MPTTModel):
    parent = TreeForeignKey('self', null=True, blank=True,
                            related_name='children', db_index=True)
    code = models.CharField(_('Area code'), max_length=20, unique=True)
    name = models.CharField(_('Area name'), max_length=255)
    type = models.ForeignKey('goals.AreaType',
                             verbose_name=_('Area type'),
                             related_name='areas')
    description = models.TextField(_('Area description'), blank=True)
    image = models.ImageField(_('Image'),
                              upload_to='goals/areas/images',
                              blank=True, null=True)
    image_small = ImageSpecField(source='image',
                                 processors=[ResizeToFit(100, 100)],
                                 format='PNG',
                                 options={'quality': 90})
    image_medium = ImageSpecField(source='image',
                                  processors=[ResizeToFit(250, 250)],
                                  format='PNG',
                                  options={'quality': 90})
    image_large = ImageSpecField(source='image',
                                 processors=[ResizeToFit(700)],
                                 options={'quality': 80})
    slug = models.SlugField(_('Slug'), blank=True)
    created = models.DateTimeField(_('Created'), auto_now_add=True)
    last_modified = models.DateTimeField(_('Last modified'),
                                         auto_now=True)
    extras = HStoreField(_('Extras'), blank=True, null=True, default={})

    class Meta:
        verbose_name = _('Area')
        verbose_name_plural = _('Areas')

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = self.get_slug()
        if self.type:
            self.extras['type_code'] = self.type.code
            self.extras['type_name'] = self.type.name
        super(Area, self).save(*args, **kwargs)

    def get_slug(self):
        if not self.slug:
            slug = slugify(self.name[:50])
            return slug
        return self.slug

    @cached_property
    def api_url(self):
        try:
            return reverse('area-detail', args=[self.pk])
        except:
            # API isn't installed
            # FIXME: Catch a specific exception
            return ''

    @cached_property
    def type_code(self):
        if self.type:
            return self.extras.get('type_code', '') or self.type.code
        return ""

    @cached_property
    def type_name(self):
        if self.type:
            return self.extras.get('type_name', '') or self.type.name
        return ""
goals/models.py
class Target(models.Model):
    goal = models.ForeignKey(Goal, verbose_name=_('Goal'),
                             related_name='targets')
    code = models.CharField(_('Target number'), max_length=10)
    name = models.CharField(_('Target'), max_length=255)
    description = models.TextField(_('Target description'),
                                   blank=True)
    image = models.ImageField(_('Image'),
                              upload_to='goals/targets/images',
                              blank=True, null=True)
    image_small = ImageSpecField(source='image',
                                 processors=[ResizeToFit(100, 100)],
                                 format='PNG',
                                 options={'quality': 90})
    image_medium = ImageSpecField(source='image',
                                  processors=[ResizeToFit(250, 250)],
                                  format='PNG',
                                  options={'quality': 90})
    image_large = ImageSpecField(source='image',
                                 processors=[ResizeToFit(700)],
                                 options={'quality': 80})
    slug = models.SlugField(_('Slug'), blank=True)
    created = models.DateTimeField(_('Created'), auto_now_add=True)
    last_modified = models.DateTimeField(_('Last modified'),
                                         auto_now=True)
    extras = HStoreField(_('Extras'), blank=True, null=True, default={})

    class Meta:
        verbose_name = _('Target')
        verbose_name_plural = _('Targets')
        unique_together = ['code', 'goal']

    def __str__(self):
        return '%s %s : %s' % (self.plan_code, self.code, truncatechars(self.description, 50))

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = self.get_slug()
        self.extras['goal_code'] = self.goal.code
        self.extras['goal_name'] = self.goal.name
        self.extras['plan_id'] = self.plan.id
        self.extras['plan_code'] = self.plan.code
        self.extras['plan_name'] = self.plan.name
        super(Target, self).save(*args, **kwargs)

    def get_slug(self):
        if not self.slug:
            slug = slugify(self.name[:50])
            return slug
        return self.slug

    @cached_property
    def api_url(self):
        try:
            return reverse('target-detail', args=[self.pk])
        except:
            # API isn't installed
            # FIXME: Catch a specific exception
            return ''

    @cached_property
    def image_url(self):
        if self.image:
            return self.image.url

    @cached_property
    def image_small_url(self):
        if self.image_small:
            return self.image_small.url

    @cached_property
    def image_medium_url(self):
        if self.image_medium:
            return self.image_medium.url

    @cached_property
    def image_large_url(self):
        if self.image_large:
            return self.image_large.url

    @cached_property
    def goal_code(self):
        return self.extras.get('goal_code', '') or self.goal.code

    @cached_property
    def goal_name(self):
        return self.extras.get('goal_name', '') or self.goal.name

    @cached_property
    def plan(self):
        return self.goal.plan

    @cached_property
    def plan_id(self):
        return int(self.extras.get('plan_id', '0')) or self.goal.plan_id

    @cached_property
    def plan_code(self):
        return self.extras.get('plan_code', '') or self.goal.plan_code

    @cached_property
    def plan_name(self):
        return self.extras.get('plan_name', '') or self.goal.plan_name
goals/models.py
class Progress(models.Model):
    component = models.ForeignKey(Component,
                                  verbose_name=_('Component'),
                                  related_name='progress')
    area = models.ForeignKey(Area, verbose_name=_('Area'),
                             related_name='progress')
    groups = ArrayField(
        models.CharField(max_length=50, blank=True), null=True,
        blank=True, verbose_name=_('Groups'), default=[])
    year = models.PositiveIntegerField(_('Year'))
    fiscal_year = models.CharField(_('Fiscal year'), max_length=9,
                                   blank=True)
    value = models.FloatField(_('Value'))
    remarks = models.TextField(_('Remarks'), blank=True)
    created = models.DateTimeField(_('Created'), auto_now_add=True)
    last_modified = models.DateTimeField(_('Last modified'),
                                         auto_now=True)
    extras = HStoreField(_('Extras'), blank=True, null=True, default={})

    class Meta:
        verbose_name = _('Progress')
        verbose_name_plural = _('Progress')

    def __str__(self):
        return '%d:%d' % (self.year, self.value)

    def save(self, *args, **kwargs):
        self.extras['area_code'] = self.area.code
        self.extras['area_name'] = self.area.name
        self.extras['area_type_id'] = self.area.type_id
        self.extras['area_type_code'] = self.area.type_code
        self.extras['area_type_name'] = self.area.type_name
        self.extras['component_code'] = self.component.code
        self.extras['component_name'] = self.component.name
        self.extras['value_unit'] = self.component.value_unit
        super(Progress, self).save(*args, **kwargs)

    @cached_property
    def api_url(self):
        try:
            return reverse('progress-detail', args=[self.pk])
        except:
            # API isn't installed
            # FIXME: Catch a specific exception
            return ''

    @cached_property
    def component_code(self):
        return self.extras.get('component_code', '')\
            or self.component.code

    @cached_property
    def component_name(self):
        return self.extras.get('component_name', '')\
            or self.component.name

    @cached_property
    def area_code(self):
        return self.extras.get('area_code', '') or self.area.code

    @cached_property
    def area_name(self):
        return self.extras.get('area_name', '') or self.area.name

    @cached_property
    def area_type_id(self):
        return int(self.extras.get('area_type_id', 0))\
            or self.area.type_id

    @cached_property
    def area_type_code(self):
        return self.extras.get('area_type_code', '')\
            or self.area.type_code

    @cached_property
    def area_type_name(self):
        return self.extras.get('area_type_name', '')\
            or self.area.type_name

    @cached_property
    def value_unit(self):
        return self.extras.get('value_unit', '')
goals/models.py
class AreaType(models.Model):
    code = models.CharField(_('Code'), max_length=20, unique=True)
    name = models.CharField(_('Name'), max_length=255)
    description = models.TextField(_('Description'), blank=True)
    topojson = models.FileField(_('TopoJSON'), blank=True, null=True,
                                upload_to=area_type_topo_path)
    created = models.DateTimeField(_('Created'), auto_now_add=True)
    last_modified = models.DateTimeField(_('Last modified'),
                                         auto_now=True)
    extras = HStoreField(_('Extras'), blank=True, null=True, default={})

    class Meta:
        verbose_name = _('Area Type')
        verbose_name_plural = _('Area Types')

    def __str__(self):
        return self.name
goals/models.py
class Indicator(models.Model):
    theme = models.ForeignKey('goals.Theme', verbose_name=_('Theme'),
                              related_name='indicators', null=True, blank=True)
    sector = models.ForeignKey('goals.Sector', verbose_name=_('Sector'),
                               related_name='indicators', null=True, blank=True)
    target = models.ForeignKey(Target, verbose_name=_('Target'),
                               related_name='indicators', null=True, blank=True)
    name = models.CharField(_('Indicator'), max_length=255)
    code = models.CharField(_('Indicator number'), max_length=10)
    description = models.TextField(_('Indicator description'),
                                   blank=True)
    image = models.ImageField(_('Image'),
                              upload_to='goals/indicators/images',
                              blank=True, null=True)
    image_small = ImageSpecField(source='image',
                                 processors=[ResizeToFit(100, 100)],
                                 format='PNG',
                                 options={'quality': 90})
    image_medium = ImageSpecField(source='image',
                                  processors=[ResizeToFit(250, 250)],
                                  format='PNG',
                                  options={'quality': 90})
    image_large = ImageSpecField(source='image',
                                 processors=[ResizeToFit(700)],
                                 options={'quality': 80})
    slug = models.SlugField(_('Slug'), blank=True)
    created = models.DateTimeField(_('Created'), auto_now_add=True)
    last_modified = models.DateTimeField(_('Last modified'),
                                         auto_now=True)
    extras = HStoreField(_('Extras'), blank=True, null=True, default={})
    sectors_ids = ArrayField(
        models.IntegerField(), null=True, blank=True, editable=False,
        verbose_name=_('Sectors ids'), default=[])
    plan_id = models.IntegerField(_('Plan ID'), null=True, blank=True,
                                  editable=False)

    class Meta:
        verbose_name = _('Indicator')
        verbose_name_plural = _('Indicators')
        unique_together = ['code', 'target', 'sector', 'theme']

    def __str__(self):
        return '%s %s : %s' \
            % (self.plan_code, self.code, self.name)

    def save(self, *args, **kwargs):
        self.clean()
        if not self.slug:
            self.slug = self.get_slug()
        if self.theme:
            self.extras['theme_code'] = self.theme.code
            self.extras['theme_name'] = self.theme.name
        if self.sector:
            self.sectors_ids = self.sector.ancestors_ids + [self.sector_id]
            self.extras['sector_code'] = self.sector.code
            self.extras['sector_name'] = self.sector.name
            self.extras['sectors_codes'] = json.dumps(self.sector.ancestors_codes + [self.sector.code])
            self.extras['sectors_names'] = json.dumps(self.sector.ancestors_names + [self.sector.name])
            self.extras['sector_type_code'] = self.sector.type.code
            self.extras['sector_type_name'] = self.sector.type.name
            self.extras['root_sector_id'] = self.sector.get_root().id
            self.extras['root_sector_code'] = self.sector.get_root().code
            self.extras['root_sector_name'] = self.sector.get_root().name
        if self.target:
            self.extras['target_code'] = self.target.code
            self.extras['target_name'] = self.target.name
        if self.goal:
            self.extras['goal_id'] = self.goal.id
            self.extras['goal_code'] = self.goal.code
            self.extras['goal_name'] = self.goal.name
        if self.plan:
            self.plan_id = self.plan.id
            self.extras['plan_code'] = self.plan.code
            self.extras['plan_name'] = self.plan.name
        super(Indicator, self).save(*args, **kwargs)

    def clean(self):
        if self.theme and self.target:
            if self.theme.plan_id != self.target.goal.plan_id:
                raise ValidationError(
                    _('Theme and Target must belong to the same plan'))

    def get_slug(self):
        if not self.slug:
            slug = slugify(self.name[:50])
            return slug
        return self.slug

    @cached_property
    def api_url(self):
        try:
            return reverse('indicator-detail', args=[self.pk])
        except:
            # API isn't installed
            # FIXME: Catch a specific exception
            return ''

    @cached_property
    def image_url(self):
        if self.image:
            return self.image.url

    @cached_property
    def image_small_url(self):
        if self.image_small:
            return self.image_small.url

    @cached_property
    def image_medium_url(self):
        if self.image_medium:
            return self.image_medium.url

    @cached_property
    def image_large_url(self):
        if self.image_large:
            return self.image_large.url

    @cached_property
    def theme_code(self):
        return self.extras.get('theme_code', '')

    @cached_property
    def theme_name(self):
        return self.extras.get('theme_name', '')

    @cached_property
    def sectors_names(self):
        if self.sector:
            return json.loads(self.extras.get('sectors_names', '[]'))
        return []

    @cached_property
    def sectors_codes(self):
        if self.sector:
            return json.loads(self.extras.get('sectors_codes', '[]'))
        return []

    @cached_property
    def sector_type_code(self):
        return self.extras.get('sector_type_code', '')

    @cached_property
    def sector_type_name(self):
        return self.extras.get('sector_type_name', '')

    @cached_property
    def sector_code(self):
        return self.extras.get('sector_code', '')

    @cached_property
    def sector_name(self):
        return self.extras.get('sector_name', '')

    @cached_property
    def root_sector_id(self):
        return int(self.extras.get('root_sector_id', '0')) or None

    @cached_property
    def root_sector_code(self):
        return self.extras.get('root_sector_code', '')

    @cached_property
    def root_sector_name(self):
        return self.extras.get('root_sector_name', '')

    @cached_property
    def target_code(self):
        return self.extras.get('target_code', '')

    @cached_property
    def target_name(self):
        return self.extras.get('target_name', '')

    @cached_property
    def goal(self):
        if self.target:
            return self.target.goal
        return None

    @cached_property
    def goal_id(self):
        return int(self.extras.get('goal_id', '0')) or None

    @cached_property
    def goal_code(self):
        return self.extras.get('goal_code', '')

    @cached_property
    def goal_name(self):
        return self.extras.get('goal_name', '')

    @cached_property
    def plan(self):
        if self.target:
            return self.target.goal.plan
        elif self.theme:
            return self.theme.plan
        return None

    @cached_property
    def plan_code(self):
        return self.extras.get('plan_code', '')

    @cached_property
    def plan_name(self):
        return self.extras.get('plan_name', '')

    def get_progress_count(self):
        return Progress.objects.filter(component__indicators=self.id).count()

    def get_progress_preview(self):
        return Progress.objects.filter(component__indicators=self.id)\
            .order_by('component__indicators', '-year')\
            .distinct('component__indicators')
goals/models.py
class Plan(models.Model):
    code = models.CharField(_('code'), max_length=10,
                            unique=True)
    name = models.CharField(_('Name'), max_length=255)
    caption = models.CharField(_('Caption'), max_length=255, blank=True)
    description = models.TextField(_('Description'), blank=True)
    image = models.ImageField(_('Image'),
                              upload_to='goals/goals/images',
                              blank=True, null=True)
    image_small = ImageSpecField(source='image',
                                 processors=[ResizeToFit(100, 100)],
                                 format='PNG',
                                 options={'quality': 90})
    image_medium = ImageSpecField(source='image',
                                  processors=[ResizeToFit(250, 250)],
                                  format='PNG',
                                  options={'quality': 90})
    image_large = ImageSpecField(source='image',
                                 processors=[ResizeToFit(700)],
                                 options={'quality': 80})
    slug = models.SlugField(_('Slug'), blank=True)
    created = models.DateTimeField(_('Created'), auto_now_add=True)
    last_modified = models.DateTimeField(_('Last modified'),
                                         auto_now=True)
    extras = HStoreField(_('Extras'), blank=True, null=True, default={})

    class Meta:
        verbose_name = _('Plan')
        verbose_name_plural = _('Plans')

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = self.get_slug()
        super(Plan, self).save(*args, **kwargs)

    def get_slug(self):
        if not self.slug:
            slug = slugify(self.name[:50])
            return slug
        return self.slug

    @cached_property
    def api_url(self):
        try:
            return reverse('plan-detail', args=[self.pk])
        except:
            # API isn't installed
            # FIXME: Catch a specific exception
            return ''

    @cached_property
    def image_url(self):
        if self.image:
            return self.image.url

    @cached_property
    def image_small_url(self):
        if self.image_small:
            return self.image_small.url

    @cached_property
    def image_medium_url(self):
        if self.image_medium:
            return self.image_medium.url

    @cached_property
    def image_large_url(self):
        if self.image_large:
            return self.image_large.url
goals/models.py
class Sector(MPTTModel):
    themes = models.ManyToManyField('goals.Theme', verbose_name='Themes',
                                    related_name='sectors')
    parent = TreeForeignKey('self', null=True, blank=True,
                            related_name='children', db_index=True)
    name = models.CharField(_('Sector name'), max_length=255)
    code = models.CharField(_('Sector code'), max_length=20)
    type = models.ForeignKey('goals.SectorType',
                             verbose_name=_('Sector type'),
                             related_name='sextors')
    description = models.TextField(_('Sector description'), blank=True)
    image = models.ImageField(_('Image'),
                              upload_to='goals/sectors/images',
                              blank=True, null=True)
    image_small = ImageSpecField(source='image',
                                 processors=[ResizeToFit(100, 100)],
                                 format='PNG',
                                 options={'quality': 90})
    image_medium = ImageSpecField(source='image',
                                  processors=[ResizeToFit(250, 250)],
                                  format='PNG',
                                  options={'quality': 90})
    image_large = ImageSpecField(source='image',
                                 processors=[ResizeToFit(700)],
                                 options={'quality': 80})
    slug = models.SlugField(_('Slug'), blank=True)
    created = models.DateTimeField(_('Created'), auto_now_add=True)
    last_modified = models.DateTimeField(_('Last modified'),
                                         auto_now=True)
    extras = HStoreField(_('Extras'), blank=True, null=True, default={})

    class Meta:
        verbose_name = _('Sector')
        verbose_name_plural = _('Sectors')

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = self.get_slug()
        if self.type:
            self.extras['type_code'] = self.type.code
            self.extras['type_name'] = self.type.name
        if self.parent:
            self.extras['parent_name'] = self.parent.name
        super(Sector, self).save(*args, **kwargs)

    def get_slug(self):
        if not self.slug:
            slug = slugify(self.name[:50])
            return slug
        return self.slug

    @cached_property
    def image_url(self):
        if self.image:
            return self.image.url

    @cached_property
    def image_small_url(self):
        if self.image_small:
            return self.image_small.url

    @cached_property
    def image_medium_url(self):
        if self.image_medium:
            return self.image_medium.url

    @cached_property
    def image_large_url(self):
        if self.image_large:
            return self.image_large.url

    @cached_property
    def type_code(self):
        return self.extras.get('type_code', '') or self.type.code

    @cached_property
    def type_name(self):
        return self.extras.get('type_name', '') or self.type.name

    @cached_property
    def parent_name(self):
        if self.parent:
            return self.extras.get('parent_name', '') or self.parent.name
        return ''

    @cached_property
    def ancestors_ids(self):
        return json.loads(self.extras.get('ancestors_ids', '[]'))\
            or [ancestor.id for ancestor in self.get_ancestors()]

    @cached_property
    def ancestors_codes(self):
        return json.loads(self.extras.get('ancestors_codes', '[]'))\
            or [ancestor.code for ancestor in self.get_ancestors()]

    @cached_property
    def ancestors_names(self):
        return json.loads(self.extras.get('ancestors_names', '[]'))\
            or [ancestor.name for ancestor in self.get_ancestors()]

    @cached_property
    def themes_codes(self):
        return json.loads(self.extras.get('themes_codes', '[]'))

    @cached_property
    def themes_names(self):
        return json.loads(self.extras.get('themes_names', '[]'))

    @cached_property
    def plans_ids(self):
        return json.loads(self.extras.get('plans_ids', '[]'))

    @cached_property
    def plans_codes(self):
        return json.loads(self.extras.get('plans_codes', '[]'))

    @cached_property
    def plans_names(self):
        return json.loads(self.extras.get('plans_names', '[]'))

    @cached_property
    def api_url(self):
        try:
            return reverse('sector-detail', args=[self.pk])
        except:
            # API isn't installed
            # FIXME: Catch a specific exception
            return ''
goals/models.py
class Component(models.Model):
    YES = 'YES'
    NO = 'NO'
    PARTIALLY = 'PARTIALLY'
    UNKNOWN = 'UNKNOWN'
    STATS_AVAILABLE_CHOICES = (
        (YES, _('Yes')),
        (NO, _('No')),
        (PARTIALLY, _('Partially')),
        (UNKNOWN, _('Unknown')),
    )
    indicators = models.ManyToManyField('goals.Indicator',
                                        verbose_name=_('Indicators'),
                                        related_name='components')
    code = models.CharField(_('Component number'), max_length=10,
                            unique=True)
    name = models.CharField(_('Component name'), max_length=255)
    description = models.TextField(_('Component description'),
                                   blank=True)
    image = models.ImageField(_('Image'),
                              upload_to='goals/components/images',
                              blank=True, null=True)
    image_small = ImageSpecField(source='image',
                                 processors=[ResizeToFit(100, 100)],
                                 format='PNG',
                                 options={'quality': 90})
    image_medium = ImageSpecField(source='image',
                                  processors=[ResizeToFit(250, 250)],
                                  format='PNG',
                                  options={'quality': 90})
    image_large = ImageSpecField(source='image',
                                 processors=[ResizeToFit(700)],
                                 options={'quality': 80})
    target_value = models.FloatField(_('Target value'), blank=True,
                                     null=True)
    value_unit = models.CharField(_('Value unit'), blank=True,
                                  max_length=50)
    stats_available = models.CharField(
        _('Statistics availble'), max_length=50, blank=True,
        choices=STATS_AVAILABLE_CHOICES, default=UNKNOWN)
    data_source = models.CharField(_('Data source'), max_length=255,
                                   blank=True)
    agency = models.CharField(_('Agency'), max_length=255, blank=True)
    slug = models.SlugField(_('Slug'), blank=True)
    created = models.DateTimeField(_('Created'), auto_now_add=True)
    last_modified = models.DateTimeField(_('Last modified'),
                                         auto_now=True)
    extras = HStoreField(_('Extras'), blank=True, null=True, default={})

    class Meta:
        verbose_name = _('Component')
        verbose_name_plural = _('Components')

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = self.get_slug()
        super(Component, self).save(*args, **kwargs)

    def get_slug(self):
        if not self.slug:
            slug = slugify(self.name[:50])
            return slug
        return self.slug

    @cached_property
    def api_url(self):
        try:
            return reverse('component-detail', args=[self.pk])
        except:
            # API isn't installed
            # FIXME: Catch a specific exception
            return ''

    @cached_property
    def image_url(self):
        if self.image:
            return self.image.url

    @cached_property
    def image_small_url(self):
        if self.image_small:
            return self.image_small.url

    @cached_property
    def image_medium_url(self):
        if self.image_medium:
            return self.image_medium.url

    @cached_property
    def image_large_url(self):
        if self.image_large:
            return self.image_large.url

    @cached_property
    def indicators_codes(self):
        return json.loads(self.extras.get('indicators_codes', '[]')) \
            or list(self.indicators.values_list('code', flat=True))

    @cached_property
    def indicators_names(self):
        return json.loads(self.extras.get('indicators_names', '[]')) \
            or list(self.indicators.values_list('name', flat=True))

    @cached_property
    def targets_ids(self):
        return json.loads(self.extras.get('targets_ids', '[]'))

    @cached_property
    def targets_codes(self):
        return json.loads(self.extras.get('targets_codes', '[]'))

    @cached_property
    def targets_names(self):
        return json.loads(self.extras.get('targets_names', '[]'))

    @cached_property
    def goals_ids(self):
        return json.loads(self.extras.get('goals_ids', '[]'))

    @cached_property
    def goals_codes(self):
        return json.loads(self.extras.get('goals_codes', '[]'))

    @cached_property
    def goals_names(self):
        return json.loads(self.extras.get('goals_names', '[]'))

    @cached_property
    def plans_ids(self):
        return json.loads(self.extras.get('plans_ids', '[]'))

    @cached_property
    def plans_codes(self):
        return json.loads(self.extras.get('plans_codes', '[]'))

    @cached_property
    def plans_names(self):
        return json.loads(self.extras.get('plans_names', '[]'))

    def get_progress_count(self):
        return Progress.objects.filter(component=self.id).count()


Question: from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from .models import (Plan, Goal, Theme, Sector, Target, Indicator, Component,
                     Progress, Area, AreaType)
import django_filters


class SimpleIntegerArrayField(SimpleArrayField):

    def __init__(self, base_field=forms.IntegerField(), delimiter=',',
                 max_length=None, min_length=None, *args, **kwargs):
        super(SimpleIntegerArrayField, self).__init__(
            base_field=base_field, delimiter=delimiter,
            max_length=max_length, min_length=min_length, *args, **kwargs)


class IntegerArrayFilter(django_filters.Filter):
    field_class = SimpleIntegerArrayField


class AreaFilter(django_filters.FilterSet):
    name = django_filters.CharFilter(lookup_expr='iexact')
    type = django_filters.CharFilter(lookup_expr='iexact')

    class Meta:
        model = Area
        fields = ['code', 'level']


class SectorFilter(django_filters.FilterSet):
    name = django_filters.CharFilter(lookup_expr='iexact')
    description = django_filters.CharFilter(lookup_expr='icontains')

    class Meta:
        model = Sector
        fields = ['parent', 'themes']


class PlanFilter(django_filters.FilterSet):
    name = django_filters.CharFilter(lookup_expr='iexact')

    class Meta:
        model = Plan
        fields = ['code']


class ThemeFilter(django_filters.FilterSet):
    name = django_filters.CharFilter(lookup_expr='iexact')
    description = django_filters.CharFilter(lookup_expr='icontains')
    plan_code = django_filters.CharFilter(name='plan__code')

    class Meta:
        model = Theme
        fields = ['plan', 'code']


class GoalFilter(ThemeFilter):

    class Meta:
        model = Goal
        fields = ['plan', 'code']


class TargetFilter(django_filters.FilterSet):
    description = django_filters.CharFilter(lookup_expr='icontains')

    class Meta:
        model = Target
        fields = ['goal', 'code']


class IndicatorFilter(django_filters.FilterSet):
    goal = django_filters.ModelChoiceFilter(name='target__goal',
                                            queryset=Goal.objects.all())
    description = django_filters.CharFilter(lookup_expr='icontains')
    data_source = django_filters.CharFilter(lookup_expr='icontains')
    agency = django_filters.CharFilter(lookup_expr='iexact')
    progress_count = django_filters.NumberFilter(lookup_expr='gte')
    sectors_ids = IntegerArrayFilter(lookup_expr='contains')

    class Meta:
        model = Indicator
        fields = ['plan_id', 'theme', 'target', 'sector', 'code']


class ComponentFilter(django_filters.FilterSet):
    name = django_filters.CharFilter(lookup_expr='icontains')
    description = django_filters.CharFilter(lookup_expr='icontains')
    goal = django_filters.ModelChoiceFilter(
        name='indicators__target__goal', queryset=Goal.objects.all())
    progress_count = django_filters.NumberFilter(lookup_expr='gte')

    class Meta:
        model = Component
        fields = ['indicators', 'code', 'stats_available']


class ProgressFilter(django_filters.FilterSet):
    indicator = django_filters.ModelChoiceFilter(
        name='component__indicators', queryset=Indicator.objects.all())
    target = django_filters.ModelChoiceFilter(
        name='component__indicators__target',
        queryset=Target.objects.all())
    area_code = django_filters.CharFilter(name='area__code')
    area_name = django_filters.CharFilter(name='area__name')
    area_type = django_filters.ModelChoiceFilter(
        name='area__type', queryset=AreaType.objects.all())
    area_type_code = django_filters.CharFilter(name='area__type__code')
    area_type_name = django_filters.CharFilter(name='area__type__name')

    class Meta:

Answer:         model = Progress