# Python imports import csv import io # Django imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string from django.utils.html import strip_tags from django.conf import settings # Third party imports from celery import shared_task from sentry_sdk import capture_exception # Module imports from plane.db.models import Issue from plane.utils.analytics_plot import build_graph_plot from plane.utils.issue_filters import issue_filters from plane.license.models import InstanceConfiguration from plane.license.utils.instance_value import get_configuration_value row_mapping = { "state__name": "State", "state__group": "State Group", "labels__id": "Label", "assignees__id": "Assignee Name", "start_date": "Start Date", "target_date": "Due Date", "completed_at": "Completed At", "created_at": "Created At", "issue_count": "Issue Count", "priority": "Priority", "estimate": "Estimate", "issue_cycle__cycle_id": "Cycle", "issue_module__module_id": "Module" } ASSIGNEE_ID = "assignees__id" LABEL_ID = "labels__id" STATE_ID = "state_id" CYCLE_ID = "issue_cycle__cycle_id" MODULE_ID = "issue_module__module_id" def send_export_email(email, slug, csv_buffer): """Helper function to send export email.""" subject = "Your Export is ready" html_content = render_to_string("emails/exports/analytics.html", {}) text_content = strip_tags(html_content) csv_buffer.seek(0) # Configure email connection from the database instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value") connection = get_connection( host=get_configuration_value(instance_configuration, "EMAIL_HOST"), port=int(get_configuration_value(instance_configuration, "EMAIL_PORT", "587")), username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"), password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"), use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")), use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), ) msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection) msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue()) msg.send(fail_silently=False) def get_assignee_details(slug, filters): """Fetch assignee details if required.""" return ( Issue.issue_objects.filter( workspace__slug=slug, **filters, assignees__avatar__isnull=False ) .distinct("assignees__id") .order_by("assignees__id") .values( "assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id", ) ) def get_label_details(slug, filters): """Fetch label details if required""" return ( Issue.objects.filter(workspace__slug=slug, **filters, labels__id__isnull=False) .distinct("labels__id") .order_by("labels__id") .values("labels__id", "labels__color", "labels__name") ) def get_state_details(slug, filters): return ( Issue.issue_objects.filter( workspace__slug=slug, **filters, ) .distinct("state_id") .order_by("state_id") .values("state_id", "state__name", "state__color") ) def get_module_details(slug, filters): return ( Issue.issue_objects.filter( workspace__slug=slug, **filters, issue_module__module_id__isnull=False, ) .distinct("issue_module__module_id") .order_by("issue_module__module_id") .values( "issue_module__module_id", "issue_module__module__name", ) ) def get_cycle_details(slug, filters): return ( Issue.issue_objects.filter( workspace__slug=slug, **filters, issue_cycle__cycle_id__isnull=False, ) .distinct("issue_cycle__cycle_id") .order_by("issue_cycle__cycle_id") .values( "issue_cycle__cycle_id", "issue_cycle__cycle__name", ) ) def generate_csv_from_rows(rows): """Generate CSV buffer from rows.""" csv_buffer = io.StringIO() writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) [writer.writerow(row) for row in rows] return csv_buffer def generate_segmented_rows( distribution, x_axis, y_axis, segment, key, assignee_details, label_details, state_details, cycle_details, module_details, ): segment_zero = list( set( item.get("segment") for sublist in distribution.values() for item in sublist ) ) segmented = segment row_zero = [ row_mapping.get(x_axis, "X-Axis"), row_mapping.get(y_axis, "Y-Axis"), ] + segment_zero rows = [] for item, data in distribution.items(): generated_row = [ item, sum(obj.get(key) for obj in data if obj.get(key) is not None), ] for segment in segment_zero: value = next((x.get(key) for x in data if x.get("segment") == segment), "0") generated_row.append(value) if x_axis == ASSIGNEE_ID: assignee = next( ( user for user in assignee_details if str(user[ASSIGNEE_ID]) == str(item) ), None, ) if assignee: generated_row[ 0 ] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}" if x_axis == LABEL_ID: label = next( (lab for lab in label_details if str(lab[LABEL_ID]) == str(item)), None, ) if label: generated_row[0] = f"{label['labels__name']}" if x_axis == STATE_ID: state = next( (sta for sta in state_details if str(sta[STATE_ID]) == str(item)), None, ) if state: generated_row[0] = f"{state['state__name']}" if x_axis == CYCLE_ID: cycle = next( (cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(item)), None, ) if cycle: generated_row[0] = f"{cycle['issue_cycle__cycle__name']}" if x_axis == MODULE_ID: module = next( (mod for mod in module_details if str(mod[MODULE_ID]) == str(item)), None, ) if module: generated_row[0] = f"{module['issue_module__module__name']}" rows.append(tuple(generated_row)) if segmented == ASSIGNEE_ID: for index, segm in enumerate(row_zero[2:]): assignee = next( ( user for user in assignee_details if str(user[ASSIGNEE_ID]) == str(segm) ), None, ) if assignee: row_zero[ index + 2 ] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}" if segmented == LABEL_ID: for index, segm in enumerate(row_zero[2:]): label = next( (lab for lab in label_details if str(lab[LABEL_ID]) == str(segm)), None, ) if label: row_zero[index + 2] = label["labels__name"] if segmented == STATE_ID: for index, segm in enumerate(row_zero[2:]): state = next( (sta for sta in state_details if str(sta[STATE_ID]) == str(segm)), None, ) if state: row_zero[index + 2] = state["state__name"] if segmented == MODULE_ID: for index, segm in enumerate(row_zero[2:]): module = next( (mod for mod in label_details if str(mod[MODULE_ID]) == str(segm)), None, ) if module: row_zero[index + 2] = module["issue_module__module__name"] if segmented == CYCLE_ID: for index, segm in enumerate(row_zero[2:]): cycle = next( (cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(segm)), None, ) if cycle: row_zero[index + 2] = cycle["issue_cycle__cycle__name"] return [tuple(row_zero)] + rows def generate_non_segmented_rows( distribution, x_axis, y_axis, key, assignee_details, label_details, state_details, cycle_details, module_details, ): rows = [] for item, data in distribution.items(): row = [item, data[0].get("count" if y_axis == "issue_count" else "estimate")] if x_axis == ASSIGNEE_ID: assignee = next( ( user for user in assignee_details if str(user[ASSIGNEE_ID]) == str(item) ), None, ) if assignee: row[ 0 ] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}" if x_axis == LABEL_ID: label = next( (lab for lab in label_details if str(lab[LABEL_ID]) == str(item)), None, ) if label: row[0] = f"{label['labels__name']}" if x_axis == STATE_ID: state = next( (sta for sta in state_details if str(sta[STATE_ID]) == str(item)), None, ) if state: row[0] = f"{state['state__name']}" if x_axis == CYCLE_ID: cycle = next( (cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(item)), None, ) if cycle: row[0] = f"{cycle['issue_cycle__cycle__name']}" if x_axis == MODULE_ID: module = next( (mod for mod in module_details if str(mod[MODULE_ID]) == str(item)), None, ) if module: row[0] = f"{module['issue_module__module__name']}" rows.append(tuple(row)) row_zero = [row_mapping.get(x_axis, "X-Axis"), row_mapping.get(y_axis, "Y-Axis")] return [tuple(row_zero)] + rows @shared_task def analytic_export_task(email, data, slug): try: filters = issue_filters(data, "POST") queryset = Issue.issue_objects.filter(**filters, workspace__slug=slug) x_axis = data.get("x_axis", False) y_axis = data.get("y_axis", False) segment = data.get("segment", False) distribution = build_graph_plot( queryset, x_axis=x_axis, y_axis=y_axis, segment=segment ) key = "count" if y_axis == "issue_count" else "estimate" assignee_details = ( get_assignee_details(slug, filters) if x_axis == ASSIGNEE_ID or segment == ASSIGNEE_ID else {} ) label_details = ( get_label_details(slug, filters) if x_axis == LABEL_ID or segment == LABEL_ID else {} ) state_details = ( get_state_details(slug, filters) if x_axis == STATE_ID or segment == STATE_ID else {} ) cycle_details = ( get_cycle_details(slug, filters) if x_axis == CYCLE_ID or segment == CYCLE_ID else {} ) module_details = ( get_module_details(slug, filters) if x_axis == MODULE_ID or segment == MODULE_ID else {} ) if segment: rows = generate_segmented_rows( distribution, x_axis, y_axis, segment, key, assignee_details, label_details, state_details, cycle_details, module_details, ) else: rows = generate_non_segmented_rows( distribution, x_axis, y_axis, key, assignee_details, label_details, state_details, cycle_details, module_details, ) csv_buffer = generate_csv_from_rows(rows) send_export_email(email, slug, csv_buffer) except Exception as e: if settings.DEBUG: print(e) capture_exception(e)