Skip to content

Commit

Permalink
Refine metabase a bit
Browse files Browse the repository at this point in the history
- Use standard port numbers
- Make it available through docker
- Define configuration options
- Add command to dump/load questions
- Have a dump of a question
  • Loading branch information
davidfischer committed Oct 20, 2021
1 parent e4f88d1 commit 13214fc
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 8 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ omit =
templates/admin/*.html
templates/account/*.html
templates/includes/*.html
adserver/management/commands/metabase.py
adserver/management/commands/adtype-templates/*.html

[report]
Expand Down
8 changes: 4 additions & 4 deletions .envs/local/django
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ DATABASE_URL=psql://localuser:localpass@postgres:5432/ethicaladserver

#
# ------------------------------------------------------------------------------
METABASE_SITE_URL=http://localhost:3001
# This is the sample secret key from the metabase docs
# Ensure this is set in http://localhost:3001/admin/settings/embedding_in_other_applications
METABASE_SECRET_KEY=40e0106db5156325d600c37a5e077f44a49be1db9d02c96271e7bd67cc9529fa
# This is the address in docker. Use http://localhost:3000 outside docker.
METABASE_SITE_URL=http://metabase:3000
# Ensure this is set in http://localhost:3000/admin/settings/embedding_in_other_applications
METABASE_SECRET_KEY=0000000000000000000000000000000000000000000000000000000000000000
152 changes: 152 additions & 0 deletions adserver/management/commands/metabase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Dump or load data from Metabase."""
import argparse
import getpass
import json

import requests
from django.conf import settings
from django.core.management import CommandError
from django.core.management.base import BaseCommand
from django.utils.translation import ugettext_lazy as _


class Command(BaseCommand):

"""Management command to dump/load data from Metabase."""

help = "Dump or load questions from metabase."

def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
"""Override to store the metabase session."""
super().__init__(stdout, stderr, no_color, force_color)
self.metabase_session = None

def add_arguments(self, parser):
"""Add command line args for this command."""
parser.add_argument(
"-d",
"--dump-questions",
type=argparse.FileType("w"),
help=_("Dump questions to file"),
)
parser.add_argument(
"-l",
"--load-questions",
type=argparse.FileType("r"),
help=_("Load questions from a file"),
)

def handle(self, *args, **kwargs):
"""Entrypoint to the command."""
self.authenticate_metabase()

if kwargs["dump_questions"]:
# Dump questions to a file
self.handle_dump_questions(kwargs["dump_questions"])
elif kwargs["load_questions"]:
# Load questions from a file
self.handle_load_questions(kwargs["load_questions"])

def authenticate_metabase(self):
"""Authenticate with metabase and store the session token temporarily."""
self.stdout.write(
_("Authenticating with Metabase (%s)...") % settings.METABASE_SITE_URL
)

metabase_user = input("Metabase username: ")
metabase_password = getpass.getpass("Metabase password: ")

resp = requests.post(
settings.METABASE_SITE_URL + "/api/session",
json={"username": metabase_user, "password": metabase_password},
)
if not resp.ok:
self.stdout.write(resp.text)
raise CommandError(
_("Could not authenticate with those credentials to Metabase")
)

self.stdout.write(
self.style.SUCCESS(_("Successfully authenticated to Metabase."))
)
data = resp.json()
self.metabase_session = data["id"]

def handle_dump_questions(self, outfile):
"""Dump questions from metabase to a file."""
self.stdout.write(_("Dumping questions from Metabase..."))

resp = requests.get(
settings.METABASE_SITE_URL + "/api/card/embeddable",
headers={"X-Metabase-Session": self.metabase_session},
)

if not resp.ok:
self.stdout.write(resp.text)
raise CommandError(_("Error getting questions"))

questions = []
for question in resp.json():
# https://www.metabase.com/docs/latest/api-documentation.html#post-apicard
card_resp = requests.get(
settings.METABASE_SITE_URL + "/api/card/" + str(question["id"]),
headers={"X-Metabase-Session": self.metabase_session},
)
card = card_resp.json()
questions.append(
{
"id": card["id"],
"name": card["name"],
"description": card["description"],
"result_metadata": card["result_metadata"],
"dataset_query": card["dataset_query"],
"display": card["display"],
"visualization_settings": card["visualization_settings"],
}
)

questions.sort(key=lambda q: q["id"])
outfile.write(json.dumps(questions, indent=2))
outfile.close()

self.stdout.write(
self.style.SUCCESS(_("Successfully dumped questions from Metabase."))
)

def handle_load_questions(self, infile):
"""Load questions from a file into metabase."""
self.stdout.write(_("Loading questions to Metabase..."))

# Check if there are existing cards in Metabase (warn if so)
resp = requests.get(
settings.METABASE_SITE_URL + "/api/card",
headers={"X-Metabase-Session": self.metabase_session},
)
if resp.json():
self.stdout.write(
self.style.WARNING(_("There are existing questions in this Metabase!!"))
)
proceed = input("Proceed? (y/N): ")
if not proceed.lower().startswith("y"):
return

errors = 0
for question in json.load(infile):
# https://www.metabase.com/docs/latest/api-documentation.html#post-apicard
del question["id"]
self.stdout.write(_(" - Loaded %s") % question["name"])
resp = requests.post(
settings.METABASE_SITE_URL + "/api/card",
headers={"X-Metabase-Session": self.metabase_session},
json=question,
)

if not resp.ok:
self.stdout.write(resp.text)
self.stdout.write(self.style.ERROR(_("Error loading question")))
errors += 1

if errors == 0:
self.stdout.write(
self.style.SUCCESS(_("Successfully loaded questions into Metabase."))
)
2 changes: 1 addition & 1 deletion adserver/templates/adserver/publisher/overview.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ <h6 class="text-muted text-center">{% trans 'Clicks' %}</h6>

<div class="row">
<div class="col" style="min-height: 50vh">
{% metabase_question_embed 2 publisher_slug=publisher.slug %}
{% metabase_question_embed metabase_publisher_performance publisher_slug=publisher.slug start_date=start_date end_date=end_date %}
</div>
</div>

Expand Down
11 changes: 10 additions & 1 deletion adserver/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2022,13 +2022,22 @@ def get_context_data(self, **kwargs):

# Get the beginning of the month so we can show month-to-date stats
start_date = timezone.now().replace(day=1, hour=0, minute=0, second=0)
end_date = start_date + timedelta(days=31)

queryset = self.get_queryset(publisher=self.publisher, start_date=start_date)
report = PublisherReport(queryset)
report.generate()

context.update(
{"publisher": self.publisher, "report": report, "start_date": start_date}
{
"publisher": self.publisher,
"report": report,
"start_date": start_date,
"end_date": end_date,
"metabase_publisher_performance": settings.METABASE_QUESTIONS.get(
"PUBLISHER_PERFORMANCE"
),
}
)
return context

Expand Down
6 changes: 5 additions & 1 deletion config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,12 @@
# Metabase
# Graphing and BI tool
# --------------------------------------------------------------------------
METABASE_SITE_URL = env("METABASE_SITE_URL", default="http://localhost:3000")
METABASE_SITE_URL = env("METABASE_SITE_URL", default="http://metabase:3000")
METABASE_SECRET_KEY = env("METABASE_SECRET_KEY", default=None)
# Maps metabase questions by name to the ID
METABASE_QUESTIONS = {
"PUBLISHER_PERFORMANCE": 1,
}


# Ad server specific settings
Expand Down
2 changes: 1 addition & 1 deletion docker-compose-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,6 @@ services:
image: metabase/metabase
restart: always
ports:
- 3001:3000
- 3000:3000
volumes:
- local_metabase_data:/metabase-data
150 changes: 150 additions & 0 deletions metabase/questions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
[
{
"id": 1,
"name": "Publisher Performance",
"description": "Show a single publisher's performance on a daily basis",
"result_metadata": [
{
"name": "Date",
"display_name": "Date",
"base_type": "type/Date",
"effective_type": "type/Date",
"field_ref": [
"field",
"Date",
{
"base-type": "type/Date"
}
],
"semantic_type": null,
"fingerprint": {
"global": {
"distinct-count": 1,
"nil%": 0
},
"type": {
"type/DateTime": {
"earliest": "2021-08-13T00:00:00Z",
"latest": "2021-08-13T00:00:00Z"
}
}
}
},
{
"name": "sum",
"display_name": "sum",
"base_type": "type/BigInteger",
"effective_type": "type/BigInteger",
"field_ref": [
"field",
"sum",
{
"base-type": "type/BigInteger"
}
],
"semantic_type": null,
"fingerprint": {
"global": {
"distinct-count": 1,
"nil%": 0
},
"type": {
"type/Number": {
"min": 1,
"q1": 1,
"q3": 1,
"max": 1,
"sd": null,
"avg": 1
}
}
}
},
{
"name": "sum_2",
"display_name": "sum_2",
"base_type": "type/BigInteger",
"effective_type": "type/BigInteger",
"field_ref": [
"field",
"sum_2",
{
"base-type": "type/BigInteger"
}
],
"semantic_type": null,
"fingerprint": {
"global": {
"distinct-count": 1,
"nil%": 0
},
"type": {
"type/Number": {
"min": 0,
"q1": 0,
"q3": 0,
"max": 0,
"sd": null,
"avg": 0
}
}
}
}
],
"dataset_query": {
"type": "native",
"native": {
"query": "SELECT \"public\".\"adserver_adimpression\".\"date\" AS \"Date\", sum(\"public\".\"adserver_adimpression\".\"views\") AS \"sum\", sum(\"public\".\"adserver_adimpression\".\"clicks\") AS \"sum_2\"\nFROM \"public\".\"adserver_adimpression\"\nINNER JOIN \"public\".\"adserver_publisher\" ON \"public\".\"adserver_publisher\".\"id\" = \"public\".\"adserver_adimpression\".\"publisher_id\"\nWHERE \"public\".\"adserver_adimpression\".\"date\" >= {{ start_date }}\n AND \"public\".\"adserver_adimpression\".\"date\" < {{ end_date }}\n AND \"public\".\"adserver_publisher\".\"slug\" = {{ publisher_slug }}\nGROUP BY \"public\".\"adserver_adimpression\".\"publisher_id\", \"public\".\"adserver_adimpression\".\"date\" \nORDER BY \"public\".\"adserver_adimpression\".\"publisher_id\", \"public\".\"adserver_adimpression\".\"date\" ASC",
"template-tags": {
"publisher_slug": {
"id": "f118a498-62c3-cf6d-6bc6-c060aaf96fb5",
"name": "publisher_slug",
"display-name": "Publisher slug",
"type": "text",
"required": true,
"default": "readthedocs"
},
"start_date": {
"id": "7f1144a1-ae40-8d20-a722-de53b03cc81a",
"name": "start_date",
"display-name": "Start date",
"type": "date",
"required": true
},
"end_date": {
"id": "d85c3237-3e49-b4c2-4e06-9f817d65d282",
"name": "end_date",
"display-name": "End date",
"type": "date",
"required": true
}
}
},
"database": 2
},
"display": "bar",
"visualization_settings": {
"table.pivot_column": "publisher_id",
"table.cell_column": "sum",
"graph.dimensions": [
"Date"
],
"series_settings": {
"undefined": {
"axis": "left"
},
"sum": {
"title": "Views"
},
"sum_2": {
"title": "Clicks"
}
},
"graph.x_axis.title_text": "",
"graph.metrics": [
"sum",
"sum_2"
]
}
}
]

0 comments on commit 13214fc

Please sign in to comment.