From 4f97fd6f361c9b0e9a0bd779016d82c82f92d58e Mon Sep 17 00:00:00 2001 From: Giovani Date: Sat, 27 Oct 2018 00:31:16 -0300 Subject: [PATCH 1/5] Start Federal Senate app --- jarbas/federal_senate/__init__.py | 0 jarbas/federal_senate/app.py | 6 ++++++ jarbas/federal_senate/migrations/__init__.py | 0 jarbas/federal_senate/models.py | 3 +++ jarbas/federal_senate/views.py | 3 +++ jarbas/settings.py | 1 + 6 files changed, 13 insertions(+) create mode 100644 jarbas/federal_senate/__init__.py create mode 100644 jarbas/federal_senate/app.py create mode 100644 jarbas/federal_senate/migrations/__init__.py create mode 100644 jarbas/federal_senate/models.py create mode 100644 jarbas/federal_senate/views.py diff --git a/jarbas/federal_senate/__init__.py b/jarbas/federal_senate/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/jarbas/federal_senate/app.py b/jarbas/federal_senate/app.py new file mode 100644 index 000000000..7a1d40476 --- /dev/null +++ b/jarbas/federal_senate/app.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FederalSenateConfig(AppConfig): + name = 'jarbas.federal_senate' + verbose_name = "Senado Federal - Cota para Exercício da Atividade Parlamentar" diff --git a/jarbas/federal_senate/migrations/__init__.py b/jarbas/federal_senate/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/jarbas/federal_senate/models.py b/jarbas/federal_senate/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/jarbas/federal_senate/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/jarbas/federal_senate/views.py b/jarbas/federal_senate/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/jarbas/federal_senate/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/jarbas/settings.py b/jarbas/settings.py index 29dc29166..f59d1f0b9 100644 --- a/jarbas/settings.py +++ b/jarbas/settings.py @@ -48,6 +48,7 @@ 'rest_framework', 'jarbas.core.app.CoreConfig', 'jarbas.chamber_of_deputies.app.ChamberOfDeputiesConfig', + 'jarbas.federal_senate.app.FederalSenateConfig', 'jarbas.layers', 'jarbas.dashboard', 'django.contrib.admin', From e664a76c62106a68ab4458e22901b280701c2c05 Mon Sep 17 00:00:00 2001 From: Giovani Date: Mon, 29 Oct 2018 01:15:10 -0300 Subject: [PATCH 2/5] Add Reimbursement model of federal_senate --- .../federal_senate/migrations/0001_initial.py | 43 +++++++++++++++ jarbas/federal_senate/models.py | 29 ++++++++++- jarbas/federal_senate/tests/__init__.py | 0 .../tests/test_reimbursement_model.py | 52 +++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 jarbas/federal_senate/migrations/0001_initial.py create mode 100644 jarbas/federal_senate/tests/__init__.py create mode 100644 jarbas/federal_senate/tests/test_reimbursement_model.py diff --git a/jarbas/federal_senate/migrations/0001_initial.py b/jarbas/federal_senate/migrations/0001_initial.py new file mode 100644 index 000000000..9f666d495 --- /dev/null +++ b/jarbas/federal_senate/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# Generated by Django 2.0.8 on 2018-10-27 03:46 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Reimbursement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('document_id', models.CharField(blank=True, max_length=140, null=True, verbose_name='Número do Reembolso')), + ('last_update', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Atualizado no Jarbas em')), + ('year', models.IntegerField(db_index=True, verbose_name='Ano')), + ('month', models.IntegerField(db_index=True, verbose_name='Mês')), + ('date', models.DateField(db_index=True, verbose_name='Data')), + ('congressperson_name', models.CharField(blank=True, db_index=True, max_length=140, null=True, verbose_name='Nome do Parlamentar')), + ('expense_type', models.CharField(blank=True, max_length=140, null=True, verbose_name='Tipo da Despesa')), + ('expense_details', models.CharField(blank=True, max_length=140, null=True, verbose_name='Descrição da Despesa')), + ('supplier', models.CharField(max_length=256, verbose_name='Fornecedor')), + ('cnpj_cpf', models.CharField(blank=True, db_index=True, max_length=14, null=True, verbose_name='CNPJ ou CPF')), + ('reimbursement_value', models.DecimalField(blank=True, decimal_places=3, max_digits=10, null=True, verbose_name='Valor do Reembolso')), + ('probability', models.DecimalField(blank=True, decimal_places=5, max_digits=6, null=True, verbose_name='Probabilidade')), + ('suspicions', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='Suspeitas')), + ], + options={ + 'verbose_name': 'reembolso', + 'verbose_name_plural': 'reembolsos', + 'ordering': ('-year', '-date'), + }, + ), + migrations.AlterIndexTogether( + name='reimbursement', + index_together={('year', 'date', 'id')}, + ), + ] diff --git a/jarbas/federal_senate/models.py b/jarbas/federal_senate/models.py index 71a836239..d5e996a7d 100644 --- a/jarbas/federal_senate/models.py +++ b/jarbas/federal_senate/models.py @@ -1,3 +1,30 @@ from django.db import models +from django.contrib.postgres.fields import JSONField -# Create your models here. + +class Reimbursement(models.Model): + document_id = models.CharField('Número do Reembolso', max_length=140, blank=True, null=True) + last_update = models.DateTimeField('Atualizado no Jarbas em', db_index=True, auto_now=True) + + year = models.IntegerField('Ano', db_index=True) + month = models.IntegerField('Mês', db_index=True) + date = models.DateField('Data', db_index=True) + + congressperson_name = models.CharField('Nome do Parlamentar', max_length=140, db_index=True, blank=True, null=True) + + expense_type = models.CharField('Tipo da Despesa', max_length=140, blank=True, null=True) + expense_details = models.CharField('Descrição da Despesa', max_length=140, blank=True, null=True) + + supplier = models.CharField('Fornecedor', max_length=256) + cnpj_cpf = models.CharField('CNPJ ou CPF', max_length=14, db_index=True, blank=True, null=True) + + reimbursement_value = models.DecimalField('Valor do Reembolso', max_digits=10, decimal_places=3, blank=True, null=True) + + probability = models.DecimalField('Probabilidade', max_digits=6, decimal_places=5, blank=True, null=True) + suspicions = JSONField('Suspeitas', blank=True, null=True) + + class Meta: + ordering = ('-year', '-date') + verbose_name = 'reembolso' + verbose_name_plural = 'reembolsos' + index_together = [['year', 'date', 'id']] diff --git a/jarbas/federal_senate/tests/__init__.py b/jarbas/federal_senate/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/jarbas/federal_senate/tests/test_reimbursement_model.py b/jarbas/federal_senate/tests/test_reimbursement_model.py new file mode 100644 index 000000000..fa55d30c8 --- /dev/null +++ b/jarbas/federal_senate/tests/test_reimbursement_model.py @@ -0,0 +1,52 @@ +from django.test import TestCase + +from jarbas.federal_senate.models import Reimbursement + + +class TestReimbursement(TestCase): + + def setUp(self): + self.data = dict( + document_id='214', + year=2013, + month=5, + date='2013-10-05', + congressperson_name='MOZARILDO CAVALCANTI', + expense_type='Publicity of parliamentary activity', + expense_details='Despesas com divulgação da atividade parlamentar do Senador Mozarildo Cavalcanti', + supplier='Editora Zenite Ltda.', + cnpj_cpf='08509060000146', + reimbursement_value=300, + probability=0.5, + suspicions={'invalid_cnpj_cpf': {'is_suspect': True, 'probability': 1.0}}, + ) + + +class TestCreate(TestReimbursement): + + def test_create(self): + self.assertEqual(0, Reimbursement.objects.count()) + Reimbursement.objects.create(**self.data) + self.assertEqual(1, Reimbursement.objects.count()) + + def test_last_update(self): + reimbursement = Reimbursement.objects.create(**self.data) + created_at = reimbursement.last_update + reimbursement.year = 1971 + reimbursement.save() + self.assertGreater(reimbursement.last_update, created_at) + + def test_optional_fields(self): + optional = ( + 'document_id', + 'congressperson_name', + 'expense_type', + 'expense_details', + 'cnpj_cpf', + 'reimbursement_value', + 'probability', + 'suspicions' + ) + new_data = {k: v for k, v in self.data.items() if k not in optional} + Reimbursement.objects.create(**new_data) + self.assertEqual(1, Reimbursement.objects.count()) From 2c2619b222d45b68a1ee5c5882d54fe85e04c2c5 Mon Sep 17 00:00:00 2001 From: Giovani Date: Mon, 29 Oct 2018 01:46:01 -0300 Subject: [PATCH 3/5] Remove codeclimate issue --- jarbas/federal_senate/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jarbas/federal_senate/models.py b/jarbas/federal_senate/models.py index d5e996a7d..98efad31a 100644 --- a/jarbas/federal_senate/models.py +++ b/jarbas/federal_senate/models.py @@ -18,7 +18,9 @@ class Reimbursement(models.Model): supplier = models.CharField('Fornecedor', max_length=256) cnpj_cpf = models.CharField('CNPJ ou CPF', max_length=14, db_index=True, blank=True, null=True) - reimbursement_value = models.DecimalField('Valor do Reembolso', max_digits=10, decimal_places=3, blank=True, null=True) + reimbursement_value = models.DecimalField( + 'Valor do Reembolso', max_digits=10, decimal_places=3, blank=True, null=True + ) probability = models.DecimalField('Probabilidade', max_digits=6, decimal_places=5, blank=True, null=True) suspicions = JSONField('Suspeitas', blank=True, null=True) From 10e2d0f138bce51aaf35e0909b4ca8a896952db5 Mon Sep 17 00:00:00 2001 From: Giovani Date: Sun, 6 Oct 2019 22:35:59 -0300 Subject: [PATCH 4/5] Register FederalSenate Reimbursement on dashboard --- jarbas/dashboard/admin/__init__.py | 94 +++++++++++++------ .../dashboard/tests/test_dashboard_admin.py | 4 +- jarbas/public_admin/sites.py | 7 +- 3 files changed, 71 insertions(+), 34 deletions(-) diff --git a/jarbas/dashboard/admin/__init__.py b/jarbas/dashboard/admin/__init__.py index c4cbd4f07..39a7abd30 100644 --- a/jarbas/dashboard/admin/__init__.py +++ b/jarbas/dashboard/admin/__init__.py @@ -14,6 +14,10 @@ ReimbursementSummary, SocialMedia, ) + +from jarbas.federal_senate.models import ( + Reimbursement as FederalSenateReimbursement, +) from jarbas.chamber_of_deputies.serializers import clean_cnpj_cpf from jarbas.dashboard.admin import list_filters, widgets from jarbas.dashboard.admin.paginators import CachedCountPaginator @@ -27,7 +31,56 @@ READONLY_FIELDS = (f.name for f in ALL_FIELDS if f.name not in CUSTOM_WIDGETS) -class ReimbursementModelAdmin(PublicAdminModelAdmin): +class ReimbursementAdmin(PublicAdminModelAdmin): + + def short_document_id(self, obj): + return obj.document_id + + short_document_id.short_description = 'Reembolso' + + def suspicious(self, obj): + return obj.suspicions is not None + + suspicious.short_description = 'suspeito' + suspicious.boolean = True + + def supplier_info(self, obj): + return mark_safe(f'{obj.supplier}
{self._format_document(obj)}') + + supplier_info.short_description = 'Fornecedor' + + def _format_document(self, obj): + if obj.cnpj_cpf: + if len(obj.cnpj_cpf) == 14: + return format_cnpj(obj.cnpj_cpf) + + if len(obj.cnpj_cpf) == 11: + return format_cpf(obj.cnpj_cpf) + + return obj.cnpj_cpf + + def _format_currency(self, value): + return 'R$ {:.2f}'.format(value).replace('.', ',') + + +class FederalSenateReimbursementAdmin(ReimbursementAdmin): + list_display = ( + 'short_document_id', + 'congressperson_name', + 'year', + 'expense_type', + 'supplier_info', + 'value', + 'suspicious', + ) + + def value(self, obj): + return self._format_currency(obj.reimbursement_value) + + value.short_description = 'valor' + + +class ChamberOfDeputiesReimbursementModelAdmin(ReimbursementAdmin): list_display = ( 'short_document_id', @@ -61,21 +114,6 @@ class ReimbursementModelAdmin(PublicAdminModelAdmin): list_select_related = ('tweet',) paginator = CachedCountPaginator - def _format_document(self, obj): - if obj.cnpj_cpf: - if len(obj.cnpj_cpf) == 14: - return format_cnpj(obj.cnpj_cpf) - - if len(obj.cnpj_cpf) == 11: - return format_cpf(obj.cnpj_cpf) - - return obj.cnpj_cpf - - def supplier_info(self, obj): - return mark_safe(f'{obj.supplier}
{self._format_document(obj)}') - - supplier_info.short_description = 'Fornecedor' - def jarbas(self, obj): base_url = '/layers/#/documentId/{}/' url = base_url.format(obj.document_id) @@ -124,12 +162,6 @@ def receipt_link(self, obj): receipt_link.short_description = '' - def suspicious(self, obj): - return obj.suspicions is not None - - suspicious.short_description = 'suspeito' - suspicious.boolean = True - def has_receipt_url(self, obj): return obj.receipt_url is not None @@ -137,16 +169,11 @@ def has_receipt_url(self, obj): has_receipt_url.boolean = True def value(self, obj): - return 'R$ {:.2f}'.format(obj.total_net_value).replace('.', ',') + return self._format_currency(obj.total_net_value) value.short_description = 'valor' value.admin_order_field = 'total_net_value' - def short_document_id(self, obj): - return obj.document_id - - short_document_id.short_description = 'Reembolso' - def subquota_translated(self, obj): return Subquotas.pt_br(obj.subquota_description) @@ -167,7 +194,7 @@ def formfield_for_dbfield(self, db_field, **kwargs): return super().formfield_for_dbfield(db_field, **kwargs) def get_search_results(self, request, queryset, search_term): - queryset, distinct = super(ReimbursementModelAdmin, self) \ + queryset, distinct = super(ChamberOfDeputiesReimbursementModelAdmin, self) \ .get_search_results(request, queryset, None) if search_term: @@ -307,5 +334,12 @@ def changelist_view(self, request, extra=None): return response -public_admin.register(Reimbursement, ReimbursementModelAdmin) +public_admin.register( + Reimbursement, + ChamberOfDeputiesReimbursementModelAdmin +) +public_admin.register( + FederalSenateReimbursement, + FederalSenateReimbursementAdmin +) public_admin.register(ReimbursementSummary, ReimbursementSummaryModelAdmin) diff --git a/jarbas/dashboard/tests/test_dashboard_admin.py b/jarbas/dashboard/tests/test_dashboard_admin.py index f6d944265..ec417a7d6 100644 --- a/jarbas/dashboard/tests/test_dashboard_admin.py +++ b/jarbas/dashboard/tests/test_dashboard_admin.py @@ -4,7 +4,7 @@ from django.test import TestCase from jarbas.chamber_of_deputies.models import Reimbursement -from jarbas.dashboard.admin import ReimbursementModelAdmin +from jarbas.dashboard.admin import ChamberOfDeputiesReimbursementModelAdmin from jarbas.dashboard.admin.list_filters import SubquotaListFilter from jarbas.dashboard.admin.widgets import ReceiptUrlWidget, SubquotaWidget, SuspiciousWidget @@ -17,7 +17,7 @@ class TestDashboardSite(TestCase): def setUp(self): self.requests = map(Request, ('GET', 'POST', 'PUT', 'PATCH', 'DELETE')) - self.ma = ReimbursementModelAdmin(Reimbursement, 'dashboard') + self.ma = ChamberOfDeputiesReimbursementModelAdmin(Reimbursement, 'dashboard') def test_has_add_permission(self): permissions = map(self.ma.has_add_permission, self.requests) diff --git a/jarbas/public_admin/sites.py b/jarbas/public_admin/sites.py index c4d5b7da2..e71fe40bb 100644 --- a/jarbas/public_admin/sites.py +++ b/jarbas/public_admin/sites.py @@ -8,10 +8,13 @@ class DummyUser(AnonymousUser): def has_module_perms(self, app_label): - return app_label == 'chamber_of_deputies' + return app_label in ('chamber_of_deputies', 'federal_senate') def has_perm(self, permission, obj=None): - return permission == 'chamber_of_deputies.change_reimbursement' + return permission in ( + 'chamber_of_deputies.change_reimbursement', + 'federal_senate.change_reimbursement' + ) class PublicAdminSite(AdminSite): From a04727f13b9e5fcfb2776083b0932f463995a3b0 Mon Sep 17 00:00:00 2001 From: Giovani Date: Sun, 6 Oct 2019 22:57:11 -0300 Subject: [PATCH 5/5] Split admin models into different files --- jarbas/dashboard/admin/__init__.py | 339 +----------------- jarbas/dashboard/admin/chamber_of_deputies.py | 279 ++++++++++++++ jarbas/dashboard/admin/federal_senate.py | 18 + .../admin/reimbursement_admin_model.py | 38 ++ .../dashboard/tests/test_dashboard_admin.py | 8 +- 5 files changed, 351 insertions(+), 331 deletions(-) create mode 100644 jarbas/dashboard/admin/chamber_of_deputies.py create mode 100644 jarbas/dashboard/admin/federal_senate.py create mode 100644 jarbas/dashboard/admin/reimbursement_admin_model.py diff --git a/jarbas/dashboard/admin/__init__.py b/jarbas/dashboard/admin/__init__.py index 39a7abd30..317c4d021 100644 --- a/jarbas/dashboard/admin/__init__.py +++ b/jarbas/dashboard/admin/__init__.py @@ -1,345 +1,26 @@ -from decimal import Decimal, InvalidOperation -from hashlib import md5 - -from brazilnum.cnpj import format_cnpj -from brazilnum.cpf import format_cpf -from django.contrib.postgres.search import SearchQuery, SearchRank -from django.core.cache import cache -from django.db.models import Count, F, Sum -from django.db.models.functions import Concat -from django.utils.safestring import mark_safe - from jarbas.chamber_of_deputies.models import ( - Reimbursement, + Reimbursement as ChamberOfDeputiesReimbursement, ReimbursementSummary, - SocialMedia, ) from jarbas.federal_senate.models import ( Reimbursement as FederalSenateReimbursement, ) -from jarbas.chamber_of_deputies.serializers import clean_cnpj_cpf -from jarbas.dashboard.admin import list_filters, widgets -from jarbas.dashboard.admin.paginators import CachedCountPaginator -from jarbas.dashboard.admin.subquotas import Subquotas -from jarbas.public_admin.admin import PublicAdminModelAdmin +from jarbas.dashboard.admin.chamber_of_deputies import ( + ChamberOfDeputiesReimbursementModelAdmin, + ReimbursementSummaryModelAdmin +) +from jarbas.dashboard.admin.federal_senate import ( + FederalSenateReimbursementModelAdmin, +) from jarbas.public_admin.sites import public_admin - -ALL_FIELDS = sorted(Reimbursement._meta.fields, key=lambda f: f.verbose_name) -CUSTOM_WIDGETS = ('receipt_url', 'subquota_description', 'suspicions') -READONLY_FIELDS = (f.name for f in ALL_FIELDS if f.name not in CUSTOM_WIDGETS) - - -class ReimbursementAdmin(PublicAdminModelAdmin): - - def short_document_id(self, obj): - return obj.document_id - - short_document_id.short_description = 'Reembolso' - - def suspicious(self, obj): - return obj.suspicions is not None - - suspicious.short_description = 'suspeito' - suspicious.boolean = True - - def supplier_info(self, obj): - return mark_safe(f'{obj.supplier}
{self._format_document(obj)}') - - supplier_info.short_description = 'Fornecedor' - - def _format_document(self, obj): - if obj.cnpj_cpf: - if len(obj.cnpj_cpf) == 14: - return format_cnpj(obj.cnpj_cpf) - - if len(obj.cnpj_cpf) == 11: - return format_cpf(obj.cnpj_cpf) - - return obj.cnpj_cpf - - def _format_currency(self, value): - return 'R$ {:.2f}'.format(value).replace('.', ',') - - -class FederalSenateReimbursementAdmin(ReimbursementAdmin): - list_display = ( - 'short_document_id', - 'congressperson_name', - 'year', - 'expense_type', - 'supplier_info', - 'value', - 'suspicious', - ) - - def value(self, obj): - return self._format_currency(obj.reimbursement_value) - - value.short_description = 'valor' - - -class ChamberOfDeputiesReimbursementModelAdmin(ReimbursementAdmin): - - list_display = ( - 'short_document_id', - 'jarbas', - 'social_profile', - 'rosies_tweet', - 'receipt_link', - 'congressperson_name', - 'year', - 'subquota_translated', - 'supplier_info', - 'value', - 'suspicious' - ) - - search_fields = ('search_vector',) - - list_filter = ( - list_filters.SuspiciousListFilter, - list_filters.HasReceiptFilter, - list_filters.HasReimbursementNumberFilter, - list_filters.StateListFilter, - list_filters.YearListFilter, - list_filters.MonthListFilter, - list_filters.DocumentTypeListFilter, - list_filters.SubquotaListFilter, - ) - - fields = tuple(f.name for f in ALL_FIELDS) - readonly_fields = tuple(READONLY_FIELDS) - list_select_related = ('tweet',) - paginator = CachedCountPaginator - - def jarbas(self, obj): - base_url = '/layers/#/documentId/{}/' - url = base_url.format(obj.document_id) - image = 'Ver no Jarbas' - return mark_safe('{}'.format(url, image)) - - jarbas.short_description = '' - - def social_profile(self, obj): - social_media = SocialMedia.objects.filter(congressperson_id=obj.congressperson_id).first() - if not social_media: - return '' - - tw_link = '' - tw_img = '/static/image/twitter-icon.png' - tw_profile = social_media.twitter_profile or social_media.secondary_twitter_profile - if tw_profile: - tw_link = ''.format( - tw_profile, tw_img - ) - - fb_link = '' - fb_img = '/static/image/facebook-icon.png' - if social_media.facebook_page: - fb_link = ''.format( - social_media.facebook_page, fb_img - ) - - return mark_safe(f'{tw_link} {fb_link}') - - social_profile.short_description = 'Social' - - def rosies_tweet(self, obj): - try: - return mark_safe('🤖'.format(obj.tweet.get_url())) - except Reimbursement.tweet.RelatedObjectDoesNotExist: - return '' - - rosies_tweet.short_description = '' - - def receipt_link(self, obj): - if not obj.receipt_url: - return '' - image = '' - return mark_safe('{}'.format(obj.receipt_url, image)) - - receipt_link.short_description = '' - - def has_receipt_url(self, obj): - return obj.receipt_url is not None - - has_receipt_url.short_description = 'recibo' - has_receipt_url.boolean = True - - def value(self, obj): - return self._format_currency(obj.total_net_value) - - value.short_description = 'valor' - value.admin_order_field = 'total_net_value' - - def subquota_translated(self, obj): - return Subquotas.pt_br(obj.subquota_description) - - def get_object(self, request, object_id, from_field=None): - obj = super().get_object(request, object_id, from_field) - if obj and not obj.receipt_fetched: - obj.get_receipt_url() - return obj - - def formfield_for_dbfield(self, db_field, **kwargs): - if db_field.name in CUSTOM_WIDGETS: - custom_widgets = dict( - subquota_description=widgets.SubquotaWidget, - receipt_url=widgets.ReceiptUrlWidget, - suspicions=widgets.SuspiciousWidget - ) - kwargs['widget'] = custom_widgets.get(db_field.name) - return super().formfield_for_dbfield(db_field, **kwargs) - - def get_search_results(self, request, queryset, search_term): - queryset, distinct = super(ChamberOfDeputiesReimbursementModelAdmin, self) \ - .get_search_results(request, queryset, None) - - if search_term: - # if a cnpj/cpf strip characters other than digits - search_term = clean_cnpj_cpf(search_term) - query = SearchQuery(search_term, config='portuguese') - rank = SearchRank(F('search_vector'), query) - queryset = queryset.annotate(rank=rank).filter(search_vector=query) - - if not queryset.was_ordered(): - queryset.order_by('-rank') - - return queryset, distinct - - -class ReimbursementSummaryModelAdmin(PublicAdminModelAdmin): - change_list_template = 'dashboard/reimbursement_summary_change_list.html' - list_filter = ( - list_filters.SuspiciousListFilter, - list_filters.HasReimbursementNumberFilter, - list_filters.StateListFilter, - list_filters.YearListFilter, - list_filters.MonthListFilter, - list_filters.DocumentTypeListFilter, - ) - - def get_chart_grouping(self, request): - """Depending on the year selected on the sidebar filters, returns the - grouping criteria for the bottom bar chart: - * if user is seeing a page with no year filter, the chart shows - reimbursements grouped by year - * if the user is seeing a page filtered by a specific year, the chart - shows reimbursements grouped by month - """ - if 'year' in request.GET: - return 'month' - return 'year' - - @staticmethod - def serialize_summary_over_time(row, minimum_percentage='0.05', **kwargs): - low = kwargs.get('low') or Decimal('0') - high = kwargs.get('high') or Decimal('0') - chart_grouping = kwargs.get('chart_grouping') - chart_grouping_key = kwargs.get('chart_grouping_key') - minimum_percentage = Decimal(minimum_percentage) - total = row['total'] - - try: - percentage = (total - low) / (high - low) - except InvalidOperation: - percentage = Decimal('0') - - ratio = Decimal('1') - minimum_percentage - corrected_percentage = minimum_percentage + (ratio * percentage) - bar_height = Decimal('100') * corrected_percentage - - return { - 'label': chart_grouping, - 'chart_grouping': row[chart_grouping_key], - 'total': row['total'] or 0, - 'percent': bar_height - } - - def get_cached_context(self, request, queryset): - url = request.build_absolute_uri() - hashed = md5(url.encode('utf-8')).hexdigest() - key = f'cached_reimbursement_summary_context_{hashed}' - context = cache.get(key) - - if context is not None: - return context - - metrics = { - 'total_reimbursements': Count('id'), - 'total_value': Sum('total_net_value'), - } - queryset = ( - queryset - .values('subquota_description') - .annotate(**metrics) - .order_by('-total_value') - ) - - chart_grouping = self.get_chart_grouping(request) - if chart_grouping == 'year': - chart_grouping_key = 'year' - summary_over_time = ( - queryset - .values('year') - .annotate(total=Sum('total_net_value')) - .order_by('year') - ) - else: - chart_grouping_key = 'chart_grouping' - summary_over_time = ( - queryset - .annotate(chart_grouping=Concat('year', 'month')) - .values('chart_grouping') - .annotate(total=Sum('total_net_value')) - .order_by('year', 'month') - ) - - summary_over_time = tuple(summary_over_time) - totals = tuple(row['total'] for row in summary_over_time) - over_time_args = { - 'chart_grouping': chart_grouping, - 'chart_grouping_key': chart_grouping_key, - 'low': min(totals, default=0), - 'high': max(totals, default=0) - } - - context = { - 'year': request.GET.get('year'), - 'month': request.GET.get('month'), - 'chart_grouping': chart_grouping, - 'summary': tuple(queryset), - 'summary_total': dict(queryset.aggregate(**metrics)), - 'summary_over_time': tuple( - self.serialize_summary_over_time(row, **over_time_args) - for row in summary_over_time - ) - } - - cache.set(key, context, 60 * 60 * 6) - return context - - def changelist_view(self, request, extra=None): - response = super().changelist_view(request, extra_context=extra) - - try: - queryset = response.context_data['cl'].queryset - except (AttributeError, KeyError): - return response - - context = self.get_cached_context(request, queryset) - response.context_data.update(context) - return response - - public_admin.register( - Reimbursement, + ChamberOfDeputiesReimbursement, ChamberOfDeputiesReimbursementModelAdmin ) public_admin.register( FederalSenateReimbursement, - FederalSenateReimbursementAdmin + FederalSenateReimbursementModelAdmin ) public_admin.register(ReimbursementSummary, ReimbursementSummaryModelAdmin) diff --git a/jarbas/dashboard/admin/chamber_of_deputies.py b/jarbas/dashboard/admin/chamber_of_deputies.py new file mode 100644 index 000000000..3f8ff52e4 --- /dev/null +++ b/jarbas/dashboard/admin/chamber_of_deputies.py @@ -0,0 +1,279 @@ +from decimal import Decimal, InvalidOperation +from hashlib import md5 + +from django.contrib.postgres.search import SearchQuery, SearchRank +from django.core.cache import cache +from django.db.models import Count, F, Sum +from django.db.models.functions import Concat +from django.utils.safestring import mark_safe + +from jarbas.chamber_of_deputies.models import ( + Reimbursement, + SocialMedia, +) + +from jarbas.chamber_of_deputies.serializers import clean_cnpj_cpf +from jarbas.dashboard.admin import list_filters, widgets +from jarbas.dashboard.admin.paginators import CachedCountPaginator +from jarbas.dashboard.admin.subquotas import Subquotas +from jarbas.dashboard.admin.reimbursement_admin_model import ReimbursementAdmin +from jarbas.public_admin.admin import PublicAdminModelAdmin + + +ALL_FIELDS = sorted(Reimbursement._meta.fields, key=lambda f: f.verbose_name) +CUSTOM_WIDGETS = ('receipt_url', 'subquota_description', 'suspicions') +READONLY_FIELDS = (f.name for f in ALL_FIELDS if f.name not in CUSTOM_WIDGETS) + + +class ChamberOfDeputiesReimbursementModelAdmin(ReimbursementAdmin): + + list_display = ( + 'short_document_id', + 'jarbas', + 'social_profile', + 'rosies_tweet', + 'receipt_link', + 'congressperson_name', + 'year', + 'subquota_translated', + 'supplier_info', + 'value', + 'suspicious' + ) + + search_fields = ('search_vector',) + + list_filter = ( + list_filters.SuspiciousListFilter, + list_filters.HasReceiptFilter, + list_filters.HasReimbursementNumberFilter, + list_filters.StateListFilter, + list_filters.YearListFilter, + list_filters.MonthListFilter, + list_filters.DocumentTypeListFilter, + list_filters.SubquotaListFilter, + ) + + fields = tuple(f.name for f in ALL_FIELDS) + readonly_fields = tuple(READONLY_FIELDS) + list_select_related = ('tweet',) + paginator = CachedCountPaginator + + def jarbas(self, obj): + base_url = '/layers/#/documentId/{}/' + url = base_url.format(obj.document_id) + image = 'Ver no Jarbas' + return mark_safe('{}'.format(url, image)) + + jarbas.short_description = '' + + def social_profile(self, obj): + social_media = SocialMedia.objects.filter(congressperson_id=obj.congressperson_id).first() + if not social_media: + return '' + + tw_link = '' + tw_img = '/static/image/twitter-icon.png' + tw_profile = social_media.twitter_profile or social_media.secondary_twitter_profile + if tw_profile: + tw_link = ''.format( + tw_profile, tw_img + ) + + fb_link = '' + fb_img = '/static/image/facebook-icon.png' + if social_media.facebook_page: + fb_link = ''.format( + social_media.facebook_page, fb_img + ) + + return mark_safe(f'{tw_link} {fb_link}') + + social_profile.short_description = 'Social' + + def rosies_tweet(self, obj): + try: + return mark_safe('🤖'.format(obj.tweet.get_url())) + except Reimbursement.tweet.RelatedObjectDoesNotExist: + return '' + + rosies_tweet.short_description = '' + + def receipt_link(self, obj): + if not obj.receipt_url: + return '' + image = '' + return mark_safe('{}'.format(obj.receipt_url, image)) + + receipt_link.short_description = '' + + def has_receipt_url(self, obj): + return obj.receipt_url is not None + + has_receipt_url.short_description = 'recibo' + has_receipt_url.boolean = True + + def value(self, obj): + return self._format_currency(obj.total_net_value) + + value.short_description = 'valor' + value.admin_order_field = 'total_net_value' + + def subquota_translated(self, obj): + return Subquotas.pt_br(obj.subquota_description) + + def get_object(self, request, object_id, from_field=None): + obj = super().get_object(request, object_id, from_field) + if obj and not obj.receipt_fetched: + obj.get_receipt_url() + return obj + + def formfield_for_dbfield(self, db_field, **kwargs): + if db_field.name in CUSTOM_WIDGETS: + custom_widgets = dict( + subquota_description=widgets.SubquotaWidget, + receipt_url=widgets.ReceiptUrlWidget, + suspicions=widgets.SuspiciousWidget + ) + kwargs['widget'] = custom_widgets.get(db_field.name) + return super().formfield_for_dbfield(db_field, **kwargs) + + def get_search_results(self, request, queryset, search_term): + queryset, distinct = super(ChamberOfDeputiesReimbursementModelAdmin, self) \ + .get_search_results(request, queryset, None) + + if search_term: + # if a cnpj/cpf strip characters other than digits + search_term = clean_cnpj_cpf(search_term) + query = SearchQuery(search_term, config='portuguese') + rank = SearchRank(F('search_vector'), query) + queryset = queryset.annotate(rank=rank).filter(search_vector=query) + + if not queryset.was_ordered(): + queryset.order_by('-rank') + + return queryset, distinct + + +class ReimbursementSummaryModelAdmin(PublicAdminModelAdmin): + change_list_template = 'dashboard/reimbursement_summary_change_list.html' + list_filter = ( + list_filters.SuspiciousListFilter, + list_filters.HasReimbursementNumberFilter, + list_filters.StateListFilter, + list_filters.YearListFilter, + list_filters.MonthListFilter, + list_filters.DocumentTypeListFilter, + ) + + def get_chart_grouping(self, request): + """Depending on the year selected on the sidebar filters, returns the + grouping criteria for the bottom bar chart: + * if user is seeing a page with no year filter, the chart shows + reimbursements grouped by year + * if the user is seeing a page filtered by a specific year, the chart + shows reimbursements grouped by month + """ + if 'year' in request.GET: + return 'month' + return 'year' + + @staticmethod + def serialize_summary_over_time(row, minimum_percentage='0.05', **kwargs): + low = kwargs.get('low') or Decimal('0') + high = kwargs.get('high') or Decimal('0') + chart_grouping = kwargs.get('chart_grouping') + chart_grouping_key = kwargs.get('chart_grouping_key') + minimum_percentage = Decimal(minimum_percentage) + total = row['total'] + + try: + percentage = (total - low) / (high - low) + except InvalidOperation: + percentage = Decimal('0') + + ratio = Decimal('1') - minimum_percentage + corrected_percentage = minimum_percentage + (ratio * percentage) + bar_height = Decimal('100') * corrected_percentage + + return { + 'label': chart_grouping, + 'chart_grouping': row[chart_grouping_key], + 'total': row['total'] or 0, + 'percent': bar_height + } + + def get_cached_context(self, request, queryset): + url = request.build_absolute_uri() + hashed = md5(url.encode('utf-8')).hexdigest() + key = f'cached_reimbursement_summary_context_{hashed}' + context = cache.get(key) + + if context is not None: + return context + + metrics = { + 'total_reimbursements': Count('id'), + 'total_value': Sum('total_net_value'), + } + queryset = ( + queryset + .values('subquota_description') + .annotate(**metrics) + .order_by('-total_value') + ) + + chart_grouping = self.get_chart_grouping(request) + if chart_grouping == 'year': + chart_grouping_key = 'year' + summary_over_time = ( + queryset + .values('year') + .annotate(total=Sum('total_net_value')) + .order_by('year') + ) + else: + chart_grouping_key = 'chart_grouping' + summary_over_time = ( + queryset + .annotate(chart_grouping=Concat('year', 'month')) + .values('chart_grouping') + .annotate(total=Sum('total_net_value')) + .order_by('year', 'month') + ) + + summary_over_time = tuple(summary_over_time) + totals = tuple(row['total'] for row in summary_over_time) + over_time_args = { + 'chart_grouping': chart_grouping, + 'chart_grouping_key': chart_grouping_key, + 'low': min(totals, default=0), + 'high': max(totals, default=0) + } + + context = { + 'year': request.GET.get('year'), + 'month': request.GET.get('month'), + 'chart_grouping': chart_grouping, + 'summary': tuple(queryset), + 'summary_total': dict(queryset.aggregate(**metrics)), + 'summary_over_time': tuple( + self.serialize_summary_over_time(row, **over_time_args) + for row in summary_over_time + ) + } + + cache.set(key, context, 60 * 60 * 6) + return context + + def changelist_view(self, request, extra=None): + response = super().changelist_view(request, extra_context=extra) + + try: + queryset = response.context_data['cl'].queryset + except (AttributeError, KeyError): + return response + + context = self.get_cached_context(request, queryset) + response.context_data.update(context) + return response diff --git a/jarbas/dashboard/admin/federal_senate.py b/jarbas/dashboard/admin/federal_senate.py new file mode 100644 index 000000000..056925e74 --- /dev/null +++ b/jarbas/dashboard/admin/federal_senate.py @@ -0,0 +1,18 @@ +from jarbas.dashboard.admin.reimbursement_admin_model import ReimbursementAdmin + + +class FederalSenateReimbursementModelAdmin(ReimbursementAdmin): + list_display = ( + 'short_document_id', + 'congressperson_name', + 'year', + 'expense_type', + 'supplier_info', + 'value', + 'suspicious', + ) + + def value(self, obj): + return self._format_currency(obj.reimbursement_value) + + value.short_description = 'valor' diff --git a/jarbas/dashboard/admin/reimbursement_admin_model.py b/jarbas/dashboard/admin/reimbursement_admin_model.py new file mode 100644 index 000000000..7de602c83 --- /dev/null +++ b/jarbas/dashboard/admin/reimbursement_admin_model.py @@ -0,0 +1,38 @@ + +from brazilnum.cnpj import format_cnpj +from brazilnum.cpf import format_cpf +from django.utils.safestring import mark_safe + +from jarbas.public_admin.admin import PublicAdminModelAdmin + + +class ReimbursementAdmin(PublicAdminModelAdmin): + + def short_document_id(self, obj): + return obj.document_id + + short_document_id.short_description = 'Reembolso' + + def suspicious(self, obj): + return obj.suspicions is not None + + suspicious.short_description = 'suspeito' + suspicious.boolean = True + + def supplier_info(self, obj): + return mark_safe(f'{obj.supplier}
{self._format_document(obj)}') + + supplier_info.short_description = 'Fornecedor' + + def _format_document(self, obj): + if obj.cnpj_cpf: + if len(obj.cnpj_cpf) == 14: + return format_cnpj(obj.cnpj_cpf) + + if len(obj.cnpj_cpf) == 11: + return format_cpf(obj.cnpj_cpf) + + return obj.cnpj_cpf + + def _format_currency(self, value): + return 'R$ {:.2f}'.format(value).replace('.', ',') diff --git a/jarbas/dashboard/tests/test_dashboard_admin.py b/jarbas/dashboard/tests/test_dashboard_admin.py index ec417a7d6..c47031515 100644 --- a/jarbas/dashboard/tests/test_dashboard_admin.py +++ b/jarbas/dashboard/tests/test_dashboard_admin.py @@ -4,9 +4,13 @@ from django.test import TestCase from jarbas.chamber_of_deputies.models import Reimbursement -from jarbas.dashboard.admin import ChamberOfDeputiesReimbursementModelAdmin +from jarbas.dashboard.admin.chamber_of_deputies import ( + ChamberOfDeputiesReimbursementModelAdmin +) from jarbas.dashboard.admin.list_filters import SubquotaListFilter -from jarbas.dashboard.admin.widgets import ReceiptUrlWidget, SubquotaWidget, SuspiciousWidget +from jarbas.dashboard.admin.widgets import ( + ReceiptUrlWidget, SubquotaWidget, SuspiciousWidget +) Request = namedtuple('Request', ('method',))