Skip to content

Commit

Permalink
[POC] Implement sending report via email feature
Browse files Browse the repository at this point in the history
  • Loading branch information
zuhdil committed Aug 3, 2022
1 parent 689b34b commit 69d06c5
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 9 deletions.
8 changes: 8 additions & 0 deletions akvo/rsr/management/commands/send_report_via_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.core.management.base import BaseCommand
from akvo.rsr.views.py_reports.email_report import run_job


class Command(BaseCommand):

def handle(self, *args, **options):
run_job()
26 changes: 26 additions & 0 deletions akvo/rsr/migrations/0219_emailreportjob.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.2.10 on 2022-08-02 22:45

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('rsr', '0218_project_thumbnail_cache'),
]

operations = [
migrations.CreateModel(
name='EmailReportJob',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True, null=True)),
('started_at', models.DateTimeField(null=True)),
('finished_at', models.DateTimeField(null=True)),
('attempts', models.PositiveSmallIntegerField(default=0)),
('report', models.CharField(max_length=100)),
('payload', models.JSONField(default=dict)),
('recipient', models.EmailField(max_length=254)),
],
),
]
2 changes: 2 additions & 0 deletions akvo/rsr/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .crs_add import CrsAdd, CrsAddOtherFlag
from .category import Category
from .employment import Employment
from .email_report_job import EmailReportJob
from .focus_area import FocusArea
from .fss import Fss, FssForecast
from .goal import Goal
Expand Down Expand Up @@ -102,6 +103,7 @@
'CrsAddOtherFlag',
'DefaultPeriod',
'Employment',
'EmailReportJob',
'FocusArea',
'Fss',
'FssForecast',
Expand Down
21 changes: 21 additions & 0 deletions akvo/rsr/models/email_report_job.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.db import models
from django.utils.timezone import now


class EmailReportJob(models.Model):
created_at = models.DateTimeField(null=True, auto_now_add=True, db_index=True, editable=False)
started_at = models.DateTimeField(null=True)
finished_at = models.DateTimeField(null=True)
attempts = models.PositiveSmallIntegerField(default=0)
report = models.CharField(max_length=100)
payload = models.JSONField(default=dict)
recipient = models.EmailField()

def mark_started(self):
self.started_at = now()
self.attempts = self.attempts + 1
self.save(update_fields=['started_at', 'attempts'])

def mark_finished(self):
self.finished_at = now()
self.save(update_fields=['finished_at'])
8 changes: 4 additions & 4 deletions akvo/rsr/views/py_reports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
render_report as render_results_indicators_excel_report
from .organisation_projects_overview_report import \
render_report as render_org_projects_overview_report
from .program_overview_excel_report import render_report as render_program_overview_excel_report
from .program_overview_pdf_report import render_report as render_program_overview_pdf_report
from .program_overview_excel_report import add_email_report_job as add_program_overview_excel_report_email_job
from .program_overview_pdf_report import add_email_report_job as add_program_overview_pdf_report_email_job
from .program_period_labels_overview_pdf_report import render_program_period_lables_overview
from .nuffic_country_level_map_report import \
render_country_level_report as render_nuffic_country_level_report
Expand Down Expand Up @@ -67,8 +67,8 @@ def check(request):
'render_eutf_project_results_table_excel_report',
'render_results_indicators_excel_report',
'render_org_projects_overview_report',
'render_program_overview_excel_report',
'render_program_overview_pdf_report',
'add_program_overview_excel_report_email_job',
'add_program_overview_pdf_report_email_job',
'render_program_period_lables_overview',
'render_nuffic_country_level_report',
'render_project_overview_pdf_report',
Expand Down
43 changes: 43 additions & 0 deletions akvo/rsr/views/py_reports/email_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import logging

from datetime import timedelta
from django.db.models import Q
from django.utils.timezone import now
from akvo.rsr.models import EmailReportJob

from .program_overview_pdf_report import email_report as handle_program_overview_pdf_report

TIMEOUT = timedelta(minutes=30)
MAX_ATTEMPTS = 3
HANDLER = {
'program_overview_pdf_report': handle_program_overview_pdf_report
}

logger = logging.getLogger(__name__)


def run_job():
pending_jobs = _get_pending_jobs()
if not pending_jobs.exists():
return
job = pending_jobs.first()
job.mark_started()
try:
handler = _get_report_handler(job.report)
if handler:
handler(job.payload, job.recipient)
job.mark_finished()
except Exception:
logger.exception(f'Failed to genereate report {job.report} for {job.recipient}')


def _get_pending_jobs():
started_timeout = now() - TIMEOUT
return EmailReportJob.objects\
.order_by('created_at')\
.filter(finished_at__isnull=True)\
.exclude(Q(attempts__gte=MAX_ATTEMPTS) | Q(started_at__gte=started_timeout))


def _get_report_handler(report):
return HANDLER[report] if report in HANDLER else None
27 changes: 26 additions & 1 deletion akvo/rsr/views/py_reports/program_overview_excel_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
ContributorData, DisaggregationTargetData, group_results_by_types
)
from akvo.rsr.project_overview import is_aggregating_targets, get_disaggregations
from akvo.rsr.models import Project, IndicatorPeriod, DisaggregationTarget, Sector
from akvo.rsr.models import Project, IndicatorPeriod, DisaggregationTarget, Sector, EmailReportJob
from akvo.rsr.models.result.utils import calculate_percentage
from akvo.rsr.decorators import with_download_indicator
from akvo.utils import ensure_decimal, maybe_decimal
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from pyexcelerate import Workbook, Style, Font, Color, Fill, Alignment

Expand Down Expand Up @@ -195,6 +196,30 @@ def get_dynamic_column_start(aggregate_targets):
return AGGREGATED_TARGET_VALUE_COLUMN + 1 if aggregate_targets else AGGREGATED_TARGET_VALUE_COLUMN


@login_required
def add_email_report_job(request, program_id):
user = request.user
EmailReportJob.objects.create(
report='program_overview_pdf_report',
payload={
'program_id': program_id,
'period_start': request.GET.get('period_start', '').strip(),
'period_end': request.GET.get('period_end', '').strip(),
},
recipient=user.email
)
return HttpResponse('The report you requested will be sent to your email address', status=202)


def send_report(params, recipient):
program = Project.objects.prefetch_related('results').get(pk=params['program_id'])
start_date = utils.parse_date(params.get('period_start', ''))
end_date = utils.parse_date(params.get('period_end', ''))
wb = generate_workbok(program, start_date, end_date)
filename = '{}-{}-program-overview-report.xlsx'.format(datetime.today().strftime('%Y%b%d'), program.id)
utils.send_excel_report(wb, recipient, filename)


@login_required
@with_download_indicator
def render_report(request, program_id):
Expand Down
38 changes: 37 additions & 1 deletion akvo/rsr/views/py_reports/program_overview_pdf_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
see < http://www.gnu.org/licenses/agpl.html >.
"""

from akvo.rsr.models import Project, IndicatorPeriod
from akvo.rsr.models import Project, IndicatorPeriod, EmailReportJob
from akvo.rsr.project_overview import get_periods_with_contributors, is_aggregating_targets, merge_unique
from akvo.rsr.staticmap import get_staticmap_url, Coordinate, Size
from akvo.rsr.decorators import with_download_indicator
Expand Down Expand Up @@ -83,6 +83,42 @@ def build_view_object(project, start_date=None, end_date=None):
return utils.make_project_proxies(periods_with_contribution, ProgramProxy)[0]


@login_required
def add_email_report_job(request, program_id):
user = request.user
EmailReportJob.objects.create(
report='program_overview_pdf_report',
payload={
'program_id': program_id,
'period_start': request.GET.get('period_start', '').strip(),
'period_end': request.GET.get('period_end', '').strip(),
},
recipient=user.email
)
return HttpResponse('The report you requested will be sent to your email address', status=202)


def email_report(params, recipient):
now = datetime.today()
program = Project.objects.prefetch_related('results').get(pk=params['program_id'])
start_date = utils.parse_date(params.get('period_start', ''))
end_date = utils.parse_date(params.get('period_end', ''))
program_view = build_view_object(program, start_date or datetime(1900, 1, 1), end_date or (datetime.today() + relativedelta(years=10)))
coordinates = [
Coordinate(loc.latitude, loc.longitude)
for loc in program_view.locations
if loc and loc.latitude and loc.longitude
]
html = render_to_string('reports/program-overview.html', context={
'program': program_view,
'staticmap': get_staticmap_url(coordinates, Size(900, 600)),
'start_date': start_date,
'end_date': end_date,
})
filename = '{}-program-{}-overview.pdf'.format(now.strftime('%Y%b%d'), program.id)
utils.send_pdf_report(html, recipient, filename)


@login_required
@with_download_indicator
def render_report(request, program_id):
Expand Down
31 changes: 30 additions & 1 deletion akvo/rsr/views/py_reports/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,36 @@
from akvo.rsr.models import Partnership
from akvo.rsr.project_overview import DisaggregationTarget, IndicatorType
from akvo.rsr.models.result.utils import QUANTITATIVE, QUALITATIVE, PERCENTAGE_MEASURE, calculate_percentage
from akvo.utils import ObjectReaderProxy, ensure_decimal
from akvo.utils import ObjectReaderProxy, ensure_decimal, rsr_send_mail


def send_pdf_report(html, recipient, filename='reports.pdf'):
font_config = FontConfiguration()
pdf = HTML(string=html).write_pdf(font_config=font_config)
attachments = [{'filename': filename, 'content': pdf, 'mimetype': 'application/pdf'}]
rsr_send_mail(
[recipient],
subject='reports/email/subject.txt',
message='reports/email/message.txt',
attachments=attachments
)


def send_excel_report(workbook, recipient, filename='report.xlsx'):
stream = io.BytesIO()
workbook.save(stream)
stream.seek(0)
attachments = [{
'filename': filename,
'content': stream.read(),
'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
}]
rsr_send_mail(
[recipient],
subject='reports/email/subject.txt',
message='reports/email/message.txt',
attachments=attachments
)


def make_pdf_response(html, filename='reports.pdf'):
Expand Down
1 change: 1 addition & 0 deletions akvo/settings/90-finish.conf
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ LOGGING = {
CRONTAB_COMMAND_SUFFIX = '> /proc/1/fd/1 2>/proc/1/fd/2'
CRONJOBS = [
('* * * * *', 'django.core.management.call_command', ['iati_export']),
('* * * * *', 'django.core.management.call_command', ['send_report_via_email']),
('10 2 * * *', 'django.core.management.call_command', ['a4a_optimy_import']),
('*/5 * * * *', 'django.core.management.call_command', ['perform_iati_checks']),
('0 0 * * *', 'django.core.management.call_command', ['cleanup_untitled_and_unpublished_projects']),
Expand Down
Empty file.
Empty file.
4 changes: 2 additions & 2 deletions akvo/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,11 @@
name='py-reports-organisation-projects-overview'),

url(r'^py-reports/program/(?P<program_id>\d+)/program-overview-table/$',
py_reports.render_program_overview_excel_report,
py_reports.add_program_overview_excel_report_email_job,
name='py-reports-program-overview-table'),

url(r'^py-reports/program/(?P<program_id>\d+)/program-overview/$',
py_reports.render_program_overview_pdf_report,
py_reports.add_program_overview_pdf_report_email_job,
name='py-reports-program-overview'),

url(r'^py-reports/program/(?P<program_id>\d+)/program-labeled-periods-overview/$',
Expand Down

0 comments on commit 69d06c5

Please sign in to comment.