Skip to content

Backend

To add a new report to the Cubed Dashboard, first you will need to create a Tastypie resource in the backend, which will configure an api endpoint from which to recieve the data. Then you will need to create a report page layout and configuration in the frontend to display the data returned from the api.

Django Model

Locate the Django model for the data you wish to display in backend/client/models/report.py, for example ReportATLBaseTrafficForecast which inherits from Django's model.Model:

class ReportATLBaseTrafficForecast(models.Model):
    atl                 = models.ForeignKey('AttribATL', on_delete=models.PROTECT)
    atl_name            = models.CharField(max_length=200)
    referer             = models.ForeignKey('Referer', on_delete=models.PROTECT)
    visit_date          = models.DateTimeField(db_index=True)
    product             = models.ForeignKey('Product', on_delete=models.PROTECT) 
    baseline_traffic    = models.IntegerField()
    actual_traffic      = models.IntegerField()
    incremental_traffic = models.IntegerField()
    baseline_lc_sales   = models.DecimalField(max_digits=10, decimal_places=4, blank=True, null=True)
    actual_lc_sales     = models.DecimalField(max_digits=10, decimal_places=4, blank=True, null=True)
    incremental_lc_sales   = models.DecimalField(max_digits=10, decimal_places=4, blank=True, null=True)
    incremental_fm_sales   = models.DecimalField(max_digits=10, decimal_places=4, blank=True, null=True)
    baseline_lc_revenue     = models.DecimalField(max_digits=10, decimal_places=4, blank=True, null=True)
    actual_lc_revenue      = models.DecimalField(max_digits=10, decimal_places=4, blank=True, null=True)
    incremental_lc_revenue   = models.DecimalField(max_digits=10, decimal_places=4, blank=True, null=True)
    incremental_fm_revenue   = models.DecimalField(max_digits=10, decimal_places=4, blank=True, null=True)

    class Meta:
        db_table = 'report_atl_base_traffic_forecast'
        unique_together = ['atl', 'referer', 'visit_date', 'product']

Take note of the Django model field types, as your Tastypie resource will need to match these field types. Also, take note of any fields that are ForeignKeys to other models (e.g. 'atl', 'referer' and 'product'), you will need to define these relationships in your Tastypie resource.

The db_table attribute in the class Meta refers to the name of the database table used for the model.

Tastypie Resource

Create a new file for your Tastypie resource in backend/api/client/resources, for example atl.py. You can reference another file (such as seopt.py) for the structure.

  • The class name for the resource is the model name + 'Resource' (e.g. ReportATLBaseTrafficForecastResource).
  • The class should inherit from CubedReportResource, e.g. ReportATLBaseTrafficForecastResource(CubedReportResource).
  • Set the queryset to the correct model (e.g. ReportATLBaseTrafficForecast)
  • The resource_name will form part of the URL, e.g. report-atl-base-traffic-forecast.
class ReportATLBaseTrafficForecastResource(CubedReportResource):
    atl = fields.ForeignKey(AttribATLResource, 'atl', full=True)
    product = fields.ForeignKey(ProductLiteResource, 'product', full=True)
    referer = fields.ForeignKey(RefererLiteResource, 'referer', full=True)

    class Meta(ReportMeta):
        queryset = ReportATLBaseTrafficForecast.objects.prefetch_related("product", "referer")
        resource_name = 'report-atl-base-traffic-forecast'

        config_fields = BaseFields(show_totals=True, full_ordering=True)
        config_fields.install('atl', IntegerField(), filtering=ALL_WITH_RELATIONS, groupby=True)
        config_fields.install('atl_name', CharField(), filtering=ALL_WITH_RELATIONS, groupby=True)
        config_fields.install('referer', DictField(), filtering=ALL_WITH_RELATIONS, groupby=True)
        config_fields.install('visit_date', DateTimeField(), filtering=ALL_WITH_RELATIONS, groupby=True)
        config_fields.install('product', IntegerField(), filtering=ALL_WITH_RELATIONS, groupby=True)
        config_fields.install('baseline_traffic',IntegerField(show_percentages=True))
        config_fields.install('actual_traffic', IntegerField(show_percentages=True))
        config_fields.install('incremental_traffic', IntegerField(show_percentages=True))
        config_fields.install('baseline_lc_sales', DecimalField(show_percentages=True))
        config_fields.install('actual_lc_sales', DecimalField(show_percentages=True))
        config_fields.install('incremental_lc_sales', DecimalField(show_percentages=True))
        config_fields.install('incremental_fm_sales', DecimalField(show_percentages=True))
        config_fields.install('baseline_lc_revenue', DecimalField(show_percentages=True))
        config_fields.install('actual_lc_revenue', DecimalField(show_percentages=True))
        config_fields.install('incremental_lc_revenue', DecimalField(show_percentages=True))
        config_fields.install('incremental_fm_revenue', DecimalField(show_percentages=True))

Install each field in the class Meta using the config_fields.install() method. You will find all of these in the file expressions.py in case you need to check what function is being performed. Importantly, you may see some fields that begin with the prefix agg__. We have a custom handler for these that mimics the way Django aggregates expressions - so you may find unexpected behaviour occurs if adding a method with any sort of agg__ (or other xxx__) prefix.1 Where possible, you should try to add class methods on your specific resource rather than adding to or modifying expressions.py.

Note that fields that are ForeignKeys must also be assigned as class attributes:

class ReportATLBaseTrafficForecastResource(CubedReportResource):
    atl = fields.ForeignKey(AttribATLResource, 'atl', full=True)
    product = fields.ForeignKey(ProductLiteResource, 'product', full=True)
    referer = fields.ForeignKey(RefererLiteResource, 'referer', full=True)

You must check whether the corresponding Tastypie resources exist in order to set the relationship to other resources. If the resource doesn't already exist, you will need to create it:

class AttribATLResource(ModelResource):
    class Meta(ReportMeta):
        queryset = AttribATL.objects.all()

Sometimes one or more of the fields will need additional calculations that are performed before the data is returned by the api. In this example, the 'Traffic' columns in the database are duplicated by the number of products selected. Traffic cannot be assigned to a particular product, therefore the traffic is assigned to all the products and needs to be divided by the number of products before it is returned.

To perform these additional calculations, you will need to define some classmethods:

class ATLExpressions(object):
    product_field = F('product_id')
    baseline_traffic_field = F('baseline_traffic')
    actual_traffic_field = F('actual_traffic')
    incremental_traffic_field = F('incremental_traffic')

    @classmethod
    def baseline_traffic(c):
        return Sum(c.baseline_traffic_field) / Count(c.product_field, distinct=True, output_field=models.IntegerField())

    @classmethod
    def actual_traffic(c):
        return Sum(c.actual_traffic_field) / Count(c.product_field, distinct=True, output_field=models.IntegerField())

    @classmethod
    def incremental_traffic(c):
        return Sum(c.incremental_traffic_field) / Count(c.product_field, distinct=True, output_field=models.IntegerField())

You can then include these expressions in your class:

class ReportATLBaseTrafficForecastResource(CubedReportResource):
    custom_expressions = [ATLExpressions]
    atl = fields.ForeignKey(AttribATLResource, 'atl', full=True)
    product = fields.ForeignKey(ProductLiteResource, 'product', full=True)
    referer = fields.ForeignKey(RefererLiteResource, 'referer', full=True)

Register the Resource

You will need to register your new resource in backend/api/urls.py:

reports_api.register(EcommerceMbaVisitResource())
  • Don't forget to import the resource at the top of the file!

Finally, test the resource in the browser, for example:

http://localhost:8041/api/report/report-atl-base-traffic-forecast/?account=1

1 The error you will get will be along the lines of django.core.exceptions.FieldError: Cannot compute Avg('<CombinedExpression: F(clicks) / F(impressions)>'): '<Sum: F(clicks)>' is an aggregate. This is because part of our custom handling assumes that every field must be wrapped in a Sum() expression except where explicity given other instructions.