Skip to content
Snippets Groups Projects
Commit d32fb8ee authored by Allen Gilliland's avatar Allen Gilliland
Browse files

remove email reports from the app. the database tables are still in the migrations for now.

parent 31bff4f1
Branches
Tags
No related merge requests found
Showing
with 1 addition and 1361 deletions
'use strict';
var EmailReportControllers = angular.module('corvusadmin.emailreport.controllers', [
'corvus.metabase.services',
'metabase.forms'
]);
EmailReportControllers.controller('EmailReportList', ['$scope', '$routeParams', '$location', 'EmailReport',
function($scope, $routeParams, $location, EmailReport) {
// $scope.reports
$scope.filterMode = 'all';
$scope.sortMode = 'name';
$scope.filter = function(mode) {
$scope.filterMode = mode;
$scope.$watch('currentOrg', function(org) {
if (!org) return;
EmailReport.list({
'orgId': org.id,
'filterMode': mode
}, function(result) {
$scope.reports = result;
$scope.sort();
});
});
};
$scope.sort = function() {
if ('date' == $scope.sortMode) {
$scope.reports.sort(function(a, b) {
a = new Date(a.updated_at);
b = new Date(b.updated_at);
return a > b ? -1 : a < b ? 1 : 0;
});
} else if ('org' == $scope.sortMode) {
$scope.reports.sort(function(a, b) {
return a.organization.name.localeCompare(b.organization.name);
});
} else if ('owner' == $scope.sortMode) {
$scope.reports.sort(function(a, b) {
return a.creator.email.localeCompare(b.creator.email);
});
} else {
$scope.reports.sort(function(a, b) {
return a.name.localeCompare(b.name);
});
}
};
$scope.executeReport = function(reportId) {
var call = EmailReport.execute({
'reportId': reportId
});
return call.$promise;
};
$scope.deleteReport = function(index) {
if ($scope.reports) {
var removeReport = $scope.reports[index];
EmailReport.delete({
'reportId': removeReport.id
}, function(result) {
$scope.reports.splice(index, 1);
}, function(error) {
$scope.alertError('failed to remove job');
console.log('error deleting report', error);
});
}
};
$scope.recipientList = function(report) {
var addrs = [];
if (report) {
report.recipients.forEach(function(recipient) {
addrs.push(recipient.email);
});
addrs = addrs.concat(report.email_addresses).sort();
}
return addrs;
};
// start by showing all
$scope.filter($scope.filterMode);
}
]);
EmailReportControllers.controller('EmailReportDetail', ['$scope', '$routeParams', '$location', 'EmailReport', 'EmailReportUtils', 'Metabase',
function($scope, $routeParams, $location, EmailReport, EmailReportUtils, Metabase) {
// $scope.report
// $scope.success_message
// $scope.error_message
$scope.save = function(reportDetail) {
$scope.$broadcast("form:reset");
// we need to ensure our recipients list is properly set on the report
var recipients = [];
$scope.form_input.users.forEach(function(user) {
if (user.incl) recipients.push(user.id);
});
reportDetail.recipients = recipients;
if ($scope.report.id) {
// if there is already an ID associated with the report then we are updating
EmailReport.update(reportDetail, function (result) {
$scope.report = result;
$scope.$broadcast("form:api-success", "Successfully saved!");
}, function (error) {
$scope.$broadcast("form:api-error", error);
});
} else {
// otherwise we are creating a new report
reportDetail.organization = $scope.currentOrg.id;
EmailReport.create(reportDetail, function(result) {
$scope.report = result;
// move the user over the the actual page for the new report
$location.path('/' + $scope.currentOrg.slug + '/admin/emailreport/' + result.id);
}, function (error) {
$scope.$broadcast("form:api-error", error);
});
}
};
$scope.executeReport = function() {
var call = EmailReport.execute({
'reportId': $scope.report.id
});
return call.$promise;
};
$scope.refreshTableList = function(dbId) {
Metabase.db_tables({
'dbId': dbId
}, function(tables) {
$scope.tables = tables;
}, function(error) {
console.log('error getting tables', error);
});
};
$scope.$watch('report.schedule', function(schedule) {
if (!schedule) return;
$scope.human_readable_schedule = EmailReportUtils.humanReadableSchedule(schedule);
}, true);
$scope.$watch('currentOrg', function(org) {
if (!org) return;
EmailReport.form_input({
'orgId': org.id
}, function(form_input) {
$scope.form_input = form_input;
if ($routeParams.reportId) {
// fetch the Job data
EmailReport.get({
'reportId': $routeParams.reportId
}, function(result) {
// Short term hack. need to convert dataset_query into string
$scope.report = result;
// initialize our recipients controls
// TODO: this is a little annoying, but I didn't see an easier way to do this
$scope.form_input.users.forEach(function(user) {
result.recipients.forEach(function(recipient) {
if (recipient.id === user.id) {
user.incl = true;
}
});
});
// we also need our table list at this point
$scope.refreshTableList(result.dataset_query.database);
}, function(error) {
console.log(error);
if (error.status == 404) {
$location.path('/');
}
});
} else {
// user must be creating a new EmailReport, so start them off with a basic template
$scope.report = {
"name": "",
"mode": form_input.modes[0].id,
"public_perms": form_input.permissions[2].id,
"email_addresses": "",
"recipients": [],
"dataset_query": {
"type": "query",
"query": {
"source_table": 0,
"filter": [null, null],
"aggregation": ["rows"],
"breakout": [null],
"limit": null
}
},
"schedule": {
"days_of_week": {
"mon": true,
"tue": true,
"wed": true,
"thu": true,
"fri": true,
"sat": false,
"sun": false
},
"time_of_day": "morning",
"timezone": ""
}
};
// initialize all of our possible team members as not being recipients right now
$scope.form_input.users.forEach(function(user) {
user.incl = false;
});
}
}, function(error) {
console.log('error getting EmailReport form_input', error);
});
});
}
]);
EmailReportControllers.controller('EmailReportExecList', ['$scope', '$routeParams', '$interval', 'EtlJobExec',
function($scope, $routeParams, $interval, EtlJobExec) {
// $scope.jobexecs
$scope.filterMode = 'all';
$scope.sortMode = 'lastexec';
var jobMonitor;
$scope.filter = function(mode) {
$scope.filterMode = mode;
EtlJobExec.list({
'filterMode': mode
}, function(result) {
$scope.jobexecs = result;
$scope.sort();
});
};
$scope.sort = function() {
if ('table_name' == $scope.sortMode) {
$scope.jobexecs.sort(function(a, b) {
return a.details.table_name.localeCompare(b.details.table_name);
});
} else if ('owner' == $scope.sortMode) {
$scope.jobexecs.sort(function(a, b) {
return a.job.creator.email.localeCompare(b.job.creator.email);
});
} else if ('name' == $scope.sortMode) {
$scope.jobexecs.sort(function(a, b) {
return a.job.name.localeCompare(b.job.name);
});
} else {
// default mode is by last exec descending
$scope.jobexecs.sort(function(a, b) {
a = new Date(a.created_at);
b = new Date(b.created_at);
return a > b ? -1 : a < b ? 1 : 0;
});
}
};
$scope.canceljobMonitor = function() {
if (angular.isDefined(jobMonitor)) {
$interval.cancel(jobMonitor);
jobMonitor = undefined;
}
};
// determine the appropriate filter to start with
$scope.filter($scope.filterMode);
$scope.$on('$destroy', function() {
$scope.canceljobMonitor();
});
// start an interval which will refresh our listing periodically
jobMonitor = $interval(function() {
$scope.filter($scope.filterMode);
}, 30000);
}
]);
\ No newline at end of file
'use strict';
var EmailReport = angular.module('corvusadmin.emailreport', [
'corvusadmin.emailreport.controllers',
'corvusadmin.emailreport.services'
]);
EmailReport.config(['$routeProvider', function ($routeProvider) {
$routeProvider.when('/:orgSlug/admin/emailreport/', {templateUrl: '/app/admin/emailreport/partials/emailreport_list.html', controller: 'EmailReportList'});
$routeProvider.when('/:orgSlug/admin/emailreport/create', {templateUrl: '/app/admin/emailreport/partials/emailreport_detail.html', controller: 'EmailReportDetail'});
$routeProvider.when('/:orgSlug/admin/emailreport/executions/', {templateUrl: '/app/admin/emailreport/partials/emailreportexec_list.html', controller: 'EmailReportExecList'});
$routeProvider.when('/:orgSlug/admin/emailreport/:reportId', {templateUrl: '/app/admin/emailreport/partials/emailreport_detail.html', controller: 'EmailReportDetail'});
}]);
/*global _*/
'use strict';
var EmailReportServices = angular.module('corvusadmin.emailreport.services', ['ngResource', 'ngCookies']);
EmailReportServices.service('EmailReportUtils', function () {
this.weekdayFullName = function (abbr) {
var days_of_week = {
"sun": "Sunday",
"mon": "Monday",
"tue": "Tuesday",
"wed": "Wednesday",
"thu": "Thursday",
"fri": "Friday",
"sat": "Saturday"
};
if (abbr in days_of_week) {
return days_of_week[abbr];
} else {
return abbr;
}
};
this.humanReadableSchedule = function (schedule) {
// takes in a dictionary representation of a 'schedule' from an EmailReport
// and provides a human readable representation of what's going to take place
// start with a crappy message for the user
var msg = "Your report will be sent ";
// first part of the message is what days of the week we are running on
if (schedule.days_of_week) {
var msg_day = "";
var cnt = 0;
var selected = [];
Object.keys(schedule.days_of_week).forEach(function (key) {
if (schedule.days_of_week[key]) {
cnt++;
selected.push(key);
}
});
if (cnt === 0) {
return "You're report will not run because you haven't selected any days to run it on.";
} else if (cnt === 1) {
msg_day = "on "+this.weekdayFullName(selected[0])+"s";
} else if (cnt === 2) {
msg_day = "on "+this.weekdayFullName(selected[0])+"s and "+this.weekdayFullName(selected[1])+"s";
} else if (cnt === 7) {
msg_day = "every day";
} else {
// last case is 3-6 days selected
msg_day = cnt+" days a week";
}
msg = msg + msg_day;
}
// add on some indication of what time of the day it will run
if (schedule.time_of_day) {
var msg_time = "";
if (_.contains(['morning', 'evening', 'afternoon'], schedule.time_of_day)) {
msg_time = " in the "+schedule.time_of_day;
} else {
msg_time = " at "+schedule.time_of_day;
}
msg = msg + msg_time;
}
// lastly, add on our timezone
if (schedule.timezone) {
msg = msg + " ("+schedule.timezone+")";
}
return msg;
};
});
EmailReportServices.factory('EmailReport', ['$resource', '$cookies', function($resource, $cookies) {
return $resource('/api/emailreport/:reportId', {}, {
form_input: {
url:'/api/emailreport/form_input?org=:orgId',
method:'GET',
},
list: {
url:'/api/emailreport/?org=:orgId&f=:filterMode',
method:'GET',
isArray:true
},
create: {
url:'/api/emailreport/',
method:'POST',
headers: {'X-CSRFToken': function() { return $cookies.csrftoken; }},
},
get: {
method:'GET',
params:{reportId:'@reportId'}
},
update: {
method:'PUT',
params:{reportId:'@id'},
headers: {'X-CSRFToken': function() { return $cookies.csrftoken; }},
},
delete: {
method:'DELETE',
params:{reportId:'@reportId'},
headers: {'X-CSRFToken': function() { return $cookies.csrftoken; }},
},
execute: {
method:'POST',
params:{reportId:'@reportId'},
headers: {'X-CSRFToken': function() { return $cookies.csrftoken; }},
},
recent_execs: {
url:'/url/emailreport/@reportId/executions',
method:'GET',
params:{reportId:'@reportId'}
}
});
}]);
EmailReportServices.factory('EmailReportExec', ['$resource', '$cookies', function($resource, $cookies) {
return $resource('/api/etl/jobexec/:execId', {}, {
list: {
url:'/api/etl/jobexec',
method:'GET',
isArray:true
},
get: {
method:'GET',
params:{execId:'@execId'}
}
});
}]);
<div class="wrapper">
<section class="Breadcrumbs">
<a class="Breadcrumb Breadcrumb--path" cv-org-href="/admin/emailreport/">Email Reports</a>
<cv-chevron-right-icon class="Breadcrumb-divider" width="12px" height="12px"></cv-chevron-right-icon>
<h2 class="Breadcrumb Breadcrumb--page" ng-if="!report.id">Create a report</h2>
<h2 class="Breadcrumb Breadcrumb--page" ng-if="report.id">{{report.name}}</h2>
</section>
<section class="Grid Grid--gutters Grid--full">
<!-- form -->
<div class="Grid-cell Cell--2of3">
<form class="Form-new bordered rounded shadowed" name="form" novalidate>
<div class="Form-field" mb-form-field="name">
<mb-form-label display-name="Name" field-name="name"></mb-form-label>
<input class="Form-input Form-offset full" name="name" placeholder="How would you like to refer to this report?" ng-model="report.name" required autofocus/>
<span class="Form-charm"></span>
</div>
<div class="Form-field" mb-form-field="description">
<mb-form-label display-name="Description" field-name="description"></mb-form-label>
<input class="Form-input Form-offset full" name="description" placeholder="What should people know about this report?" ng-model="report.description" />
<span class="Form-charm"></span>
</div>
<div class="Form-field" mb-form-field="mode">
<mb-form-label display-name="Mode" field-name="mode"></mb-form-label>
<div class="Form-offset">
<div class="Button-group">
<!-- TODO: hardcoding values :( -->
<button class="Button" ng-class="{'Button--active': report.mode === 1}" ng-click="report.mode = 1" ng-disabled>Active</button>
<button class="Button" ng-class="{'Button--danger': report.mode === 2}" ng-click="report.mode = 2">Disabled</button>
</div>
</div>
</div>
<div class="Form-field" mb-form-field="public_perms">
<mb-form-label display-name="Permissions" field-name="public_perms"></mb-form-label>
<label class="Select Form-offset">
<select name="public_perms" ng-model="report.public_perms" ng-options="perm.id as perm.name for perm in form_input.permissions" required ></select>
</label>
</div>
<div class="Form-field" mb-form-field="recipients">
<mb-form-label display-name="Recipients" field-name="recipients"></mb-form-label>
<ul class="Checkbox-group Form-offset">
<li ng-repeat="user in form_input.users">
<input type="checkbox" id="{{user.id}}" ng-model="user.incl" /> <label for="{{user.id}}">{{user.name}}</label>
</li>
</ul>
</div>
<div class="Form-field" mb-form-field="email_addresses">
<mb-form-label display-name="Email aliases" field-name="email_addresses"></mb-form-label>
<textarea class="Form-input Form-offset full" name="email_addresses" ng-model="report.email_addresses" placeholder="Enter emails (separated by commas) of other recipients, who may need this email"></textarea>
<span class="Form-charm"></span>
</div>
<div class="Form-field" mb-form-field="dataset_query">
<mb-form-label display-name="Email contents" field-name="dataset_query"></mb-form-label>
<div class="mb1">
<label class="Select Form-offset block">
<select ng-model="report.dataset_query.database" ng-options="db.id as db.name for db in form_input.databases" ng-change="refreshTableList(report.dataset_query.database)">
<option value="">Select a database</option>
</select>
</label>
</div>
<div class="mb1">
<label class="Select Form-offset" ng-class="{'Select--disabled': !report.dataset_query.hasOwnProperty('database')}">
<select class="Select" name="dataset_query" ng-model="report.dataset_query.query.source_table" ng-options="tbl.id as tbl.name for tbl in tables" ng-disabled="!report.dataset_query.database">
<option value="">Select a table</option>
</select>
</label>
</div>
</div>
<div class="Form-field" mb-form-field="schedule">
<mb-form-label display-name="Schedule" field-name="schedule"></mb-form-label>
<div class="Form-offset">
<div class="Button-group">
<button class="Button" ng-class="{ 'Button--active' : report.schedule.days_of_week[day.id] }" ng-model="report.schedule.days_of_week[day.id]" ng-repeat="day in form_input.days_of_week" btn-checkbox>{{day.name}}</button>
</div>
</div>
<div class="Form-offset mt1">
<label class="Select">
<select ng-model="report.schedule.time_of_day" ng-options="tod.id as tod.name for tod in form_input.times_of_day"></select>
</label>
<label class="Select">
<select ng-model="report.schedule.timezone" ng-options="tz for tz in form_input['timezones']">
<option value="">--- default timezone ---</option>
</select>
</label>
</div>
<span class="Form-offset block py1 ">{{human_readable_schedule}}</span>
</div>
<div class="Form-actions">
<button class="Button" ng-class="{'Button--primary': form.$valid}" ng-click="save(report)" ng-disabled="!form.$valid || report.dataset_query.query.source_table == ''">
Save
</button>
<mb-form-message></mb-form-message>
</div>
</form>
</div>
<!-- actions -->
<div class="Grid-cell Cell--1of3" ng-if="report.id">
<div class="Actions">
<h3>Actions</h3>
<div class="Actions-group">
<button class="Button" mb-action-button="executeReport" success-text="Message sent!" failed-text="Failed to send" active-text="Sending">
Send now
</button>
</div>
</div>
</div>
</section>
</div>
<div class="wrapper">
<section class="PageHeader clearfix">
<a class="Button Button--primary float-right" cv-org-href="/admin/emailreport/create">Add report</a>
<h2 class="PageTitle">Email Reports</h2>
</section>
<section>
<table class="ContentTable">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Recipients</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-show="!reports">
<td colspan=4>
<cv-loading-icon></cv-loading-icon>
<h3>Loading ...</h3>
</td>
</tr>
<tr ng-repeat="report in reports">
<td>
<a class="text-bold link" cv-org-href="/admin/emailreport/{{report.id}}">{{report.name}}</a>
</td>
<td>
{{report.description}}
</td>
<td>
<ul>
<li class="my1" ng-repeat="recipient in recipientList(report)">
{{recipient}}
</li>
</ul>
</td>
<td class="Table-actions">
<button class="Button" mb-action-button="executeReport" fn-arg="{{report.id}}" success-text="Message sent!" failed-text="Failed to send" active-text="Sending ...">Send Now</button>
<button class="Button Button--danger" ng-click="deleteReport($index)" delete-confirm>Delete</button>
</td>
</tr>
</tbody>
</table>
</section>
</div>
......@@ -27,7 +27,6 @@ var Corvus = angular.module('corvus', [
'corvus.setup',
'corvusadmin.organization',
'corvusadmin.databases',
'corvusadmin.emailreport',
'corvusadmin.people',
'corvusadmin.query',
'superadmin.settings',
......
......@@ -114,11 +114,6 @@
<span class="NavItem-text">Databases</span>
</a>
</li>
<li>
<a class="NavItem" ng-class="{ 'is--selected' : isActive('/admin/emailreport')}" cv-org-href="/admin/emailreport/">
<span class="NavItem-text">Email Reports</span>
</a>
</li>
<li>
<a class="NavItem" ng-class="{ 'is--selected' : isActive('/admin/query')}" cv-org-href="/admin/query/run">
<span class="NavItem-text">SQL Query</span>
......
(ns metabase.api.emailreport
"/api/emailreport endpoints."
(:require [korma.core :refer [where subselect fields order limit]]
[compojure.core :refer [defroutes GET PUT POST DELETE]]
[medley.core :refer :all]
[metabase.api.common :refer :all]
[metabase.db :refer :all]
(metabase.models [common :as common]
[hydrate :refer :all]
[database :refer [databases-for-org]]
[emailreport :refer [EmailReport modes-input days-of-week times-of-day] :as model]
[emailreport-executions :refer [EmailReportExecutions]]
[org :refer [Org]]
[user :refer [users-for-org]])
[metabase.task.email-report :as report]
[metabase.util :as util]))
(defannotation EmailReportMode
"Check that param is a value int ID for an email report mode."
[symb value :nillable]
(annotation:Integer symb value)
(checkp-contains? (set (map :id (vals model/modes))) symb value))
(defendpoint GET "/form_input"
"Values of options for the create/edit `EmailReport` UI."
[org]
{org Required}
(read-check Org org)
(let [dbs (databases-for-org org)
users (users-for-org org)]
{:permissions common/permissions
:modes modes-input
:days_of_week days-of-week
:times_of_day times-of-day
:timezones common/timezones
:databases dbs
:users users}))
(defendpoint GET "/"
"Fetch `EmailReports` for ORG. With filter option F (default: `:all`):
* `all` - return reports created by current user + any other publicly visible reports
* `mine` - return reports created by current user"
[org f]
{org Required, f FilterOptionAllOrMine}
(read-check Org org)
(let [all? (= (or (keyword f) :all) :all)]
(-> (sel :many EmailReport
(where {:organization_id org})
(where (or {:creator_id *current-user-id*}
(when all?
{:public_perms [> common/perms-none]})))
(order :name :ASC))
(hydrate :creator :organization :can_read :can_write :recipients))))
(defendpoint POST "/"
"Create a new `EmailReport`."
[:as {{:keys [dataset_query description email_addresses mode name organization public_perms schedule recipients] :as body} :body}]
{dataset_query Required
name Required
organization Required
schedule Required
mode EmailReportMode
public_perms PublicPerms
recipients ArrayOfIntegers}
(read-check Org organization)
(let-500 [report (ins EmailReport
:creator_id *current-user-id*
:dataset_query dataset_query
:description description
:email_addresses email_addresses
:mode mode
:name name
:organization_id organization
:public_perms public_perms
:schedule schedule)]
(model/update-recipients report recipients)
(hydrate report :recipients)))
(defendpoint GET "/:id"
"Fetch `EmailReport` with ID."
[id]
(->404 (sel :one EmailReport :id id)
read-check
(hydrate :creator :organization :can_read :can_write :recipients)))
(defendpoint PUT "/:id"
"Update an `EmailReport`."
[id :as {{:keys [dataset_query description email_addresses mode name public_perms schedule recipients] :as body} :body}]
{name NonEmptyString
mode EmailReportMode
public_perms PublicPerms
recipients ArrayOfIntegers}
(clojure.pprint/pprint recipients)
(let-404 [report (sel :one EmailReport :id id)]
(write-check report)
(model/update-recipients report recipients)
(check-500 (upd-non-nil-keys EmailReport id
:dataset_query dataset_query
:description description
:email_addresses email_addresses
:mode mode
:name name
:public_perms public_perms
:schedule schedule
:version (inc (:version report)))))
(-> (sel :one EmailReport :id id)
(hydrate :creator :database :can_read :can_write)))
(defendpoint DELETE "/:id"
"Delete an `EmailReport`."
[id]
(write-check EmailReport id)
(cascade-delete EmailReport :id id))
(defendpoint POST "/:id"
"Execute and send an `EmailReport`."
[id]
(read-check EmailReport id)
(->> (report/execute-and-send id)
(sel :one EmailReportExecutions :id)))
(defendpoint GET "/:id/executions"
"Get the `EmailReportExecutions` for an `EmailReport`."
[id]
(read-check EmailReport id)
(-> (sel :many EmailReportExecutions :report_id id (order :created_at :DESC) (limit 25))
(hydrate :organization)))
(define-routes)
......@@ -3,7 +3,6 @@
[compojure.route :as route]
(metabase.api [card :as card]
[dash :as dash]
[emailreport :as emailreport]
[notify :as notify]
[org :as org]
[qs :as qs]
......@@ -36,7 +35,6 @@
(defroutes routes
(context "/card" [] (+auth card/routes))
(context "/dash" [] (+auth dash/routes))
(context "/emailreport" [] (+auth emailreport/routes))
(GET "/health" [] {:status 200 :body {:status "ok"}})
(context "/meta/dataset" [] (+auth dataset/routes))
(context "/meta/db" [] (+auth db/routes))
......
......@@ -45,39 +45,3 @@
:html message-body)
;; return the message body we sent
message-body))
(defn send-email-report
"Format and Send an `EmailReport` email."
[subject recipients query-result]
{:pre [(string? subject)
(vector? recipients)
(map? query-result)]}
(let [format-cell (fn [cell]
(cond
(nil? cell) "N/A"
(number? cell) (u/format-num cell)
:else (str cell)))
html-header-row (fn [cols]
(into [:tr {:style "background-color: #f4f4f4;"}]
(map (fn [col]
[:td {:style "text-align: left; padding: 0.5em; border: 1px solid #ddd; font-size: 12px;"} col]) cols)))
html-data-row (fn [row]
(into [:tr]
(map (fn [cell]
[:td {:style "border: 1px solid #ddd; padding: 0.5em;"} (format-cell cell)]) row)))
message-body (html [:html
[:head]
[:body {:style "font-family: Helvetica Neue, Helvetica, sans-serif; width: 100%; margin: 0 auto; max-width: 800px; font-size: 12px;"}
[:div {:class "wrapper" :style "padding: 10px; background-color: #ffffff;"}
(into [:table {:style "border: 1px solid #cccccc; width: 100%; border-collapse: collapse;"}]
(vector
;; table header row
(html-header-row (get-in query-result [:data :columns]))
;; actual data rows
(map (fn [row] (html-data-row row)) (get-in query-result [:data :rows]))))]]])]
(email/send-message
subject
(into {} (map (fn [email] {email email}) recipients))
:html message-body)
;; return the message body we sent
message-body))
(ns metabase.models.emailreport
(:require [clojure.set :as set]
[korma.core :refer :all]
[metabase.api.common :refer [check]]
[metabase.db :refer :all]
(metabase.models [common :refer [assoc-permissions-sets perms-none]]
[emailreport-recipients :refer [EmailReportRecipients]]
[org :refer [Org org-can-read org-can-write]]
[user :refer [User]])
[metabase.util :as u]))
;; ## Static Definitions
(def modes
{:active {:id 1
:name "Active"}
:disabled {:id 2
:name "Disabled"}})
(def mode-kws
(set (keys modes)))
(defn mode->id [mode]
{:pre [(contains? mode-kws mode)]}
(:id (modes mode)))
(defn mode->name [mode]
{:pre [(contains? mode-kws mode)]}
(:name (modes mode)))
(def modes-input
[{:id (mode->id :active), :name (mode->name :active)}
{:id (mode->id :disabled), :name (mode->name :disabled)}])
(def days-of-week
"Simple `vector` of the days in the week used for reference and lookups.
NOTE: order is important here!!
these indexes match the values from clj-time `day-of-week` function (0 = Sunday, 6 = Saturday)"
[{:id "sun" :name "Sun"},
{:id "mon" :name "Mon"},
{:id "tue" :name "Tue"},
{:id "wed" :name "Wed"},
{:id "thu" :name "Thu"},
{:id "fri" :name "Fri"},
{:id "sat" :name "Sat"}])
(def times-of-day
[{:id "morning" :name "Morning" :realhour 8},
{:id "midday" :name "Midday" :realhour 12},
{:id "afternoon" :name "Afternoon" :realhour 16},
{:id "evening" :name "Evening" :realhour 20},
{:id "midnight" :name "Midnight" :realhour 0}])
(defn time-of-day->realhour
"Time-of-day to realhour"
[time-of-day]
(-> (filter #(= time-of-day (:id %)) times-of-day)
first
:realhour))
;; ## Entity
(defentity EmailReport
(table :report_emailreport)
(types {:dataset_query :json
:schedule :json})
timestamped)
(def execution-details-fields [EmailReport
:id
:organization_id
:creator_id
:name
:description
:mode
:public_perms
:version
:dataset_query
:schedule
:created_at
:updated_at
:email_addresses])
(defmethod pre-insert EmailReport [_ report]
(let [defaults {:public_perms perms-none
:mode (mode->id :active)
:version 1}]
(merge defaults report)))
(defmethod post-select EmailReport [_ {:keys [id creator_id organization_id] :as report}]
(-> (u/assoc* report
:creator (delay (check creator_id 500 "Can't get creator: Query doesn't have a :creator_id.")
(sel :one User :id creator_id))
:organization (delay (check organization_id 500 "Can't get database: Query doesn't have a :database_id.")
(sel :one Org :id organization_id))
:recipients (delay (sel :many User
(where {:id [in (subselect EmailReportRecipients (fields :user_id) (where {:emailreport_id id}))]}))))
assoc-permissions-sets))
(defmethod pre-cascade-delete EmailReport [_ {:keys [id]}]
(cascade-delete EmailReportRecipients :emailreport_id id))
;; ## Related Functions
(defn update-recipients
"Update the `EmailReportRecipients` for EMAIL-REPORT.
USER-IDS should be a definitive collection of *all* IDs of users who should receive the report.
* If an ID in USER-IDS has no corresponding existing `EmailReportRecipients` object, one will be created.
* If an existing `EmailReportRecipients` has no corresponding ID in USER-IDs, it will be deleted."
{:arglists '([email-report user-ids])}
[{:keys [id]} user-ids]
{:pre [(integer? id)
(coll? user-ids)
(every? integer? user-ids)]}
(let [recipients-old (set (sel :many :field [EmailReportRecipients :user_id] :emailreport_id id))
recipients-new (set user-ids)
recipients+ (set/difference recipients-new recipients-old)
recipients- (set/difference recipients-old recipients-new)]
(when (seq recipients+)
(let [vs (map #(assoc {:emailreport_id id} :user_id %)
recipients+)]
(insert EmailReportRecipients
(values vs))))
(when (seq recipients-)
(delete EmailReportRecipients
(where {:emailreport_id id
:user_id [in recipients-]})))))
(ns metabase.models.emailreport-executions
(:require [korma.core :refer :all]
[metabase.db :refer :all]
(metabase.models [emailreport-recipients :refer [EmailReportRecipients]]
[org :refer [Org]])))
(defentity EmailReportExecutions
(table :report_emailreportexecutions)
(types {:details :json}))
(defmethod post-select EmailReportExecutions [_ {:keys [organization_id] :as execution}]
(assoc execution :organization (delay (sel :one Org :id organization_id))))
(ns metabase.models.emailreport-recipients
(:require [korma.core :refer :all]))
(defentity EmailReportRecipients
(table :report_emailreport_recipients))
......@@ -70,8 +70,7 @@
(defmethod pre-cascade-delete User [_ {:keys [id]}]
(cascade-delete 'metabase.models.org-perm/OrgPerm :user_id id)
(cascade-delete 'metabase.models.session/Session :user_id id)
(cascade-delete 'metabase.models.emailreport-recipients/EmailReportRecipients :user_id id))
(cascade-delete 'metabase.models.session/Session :user_id id))
;; ## Related Functions
......
(ns metabase.task.email-report
"Tasks related to running `EmailReports`."
(:require [clojure.tools.logging :as log]
[clj-time.core :as time]
[korma.core :refer :all]
[metabase.db :refer :all]
[metabase.driver :as driver]
[metabase.email.messages :refer [send-email-report]]
(metabase.models [emailreport :refer [EmailReport
days-of-week
execution-details-fields
mode->id
time-of-day->realhour]]
[emailreport-executions :refer [EmailReportExecutions]]
[emailreport-recipients :refer [EmailReportRecipients]]
[hydrate :refer :all])
[metabase.task :as task]
[metabase.util :as u]))
(declare execute-scheduled-reports
execute-if-scheduled
execute-and-send
execute
report-fail
report-complete)
(defn execute-reports-hourly-job
"Simple wrapper for `execute-scheduled-reports` function which we can place on a `task/hourly-tasks-hook`"
[_]
(log/debug "Executing EmailReports hourly job")
(execute-scheduled-reports))
;; this adds our email report executions to our hourly task runner
(task/add-hook! #'task/hourly-tasks-hook execute-reports-hourly-job)
(defn execute-scheduled-reports
"Execute and Send all `EmailReports` in the system.
This function checks the schedule on all :active email reports and runs them if appropriate."
[]
(log/debug "Executing ALL scheduled EmailReports")
(->> (sel :many :fields [EmailReport :id :schedule] :mode (mode->id :active))
(map execute-if-scheduled)
dorun))
(defn- execute-if-scheduled
"Test if a given report is scheduled to run at the current time and if so execute it."
[{{:keys [days_of_week time_of_day timezone]} :schedule id :id}]
(log/debug "Processing: " id days_of_week time_of_day timezone)
(let [now (time/to-time-zone (time/now) (time/time-zone-for-id (or timezone "UTC"))) ; NOTE this is in LOCAL timezone
curr-hour (time/hour now)
curr-weekday (:id (get days-of-week (time/day-of-week now)))]
;; report schedule should look like:
;; `{:days_of_week {:mon true :tue true :wed false ...} :time_of_day "morning" :timezone "US/Pacific"}`
(when (and (get days_of_week (keyword curr-weekday)) ; scheduled weekdays include curr-weekday
(= curr-hour (time-of-day->realhour time_of_day))) ; scheduled hour matches curr-hour
(try
(execute-and-send id)
(catch Throwable t
(log/error (format "Error executing email report: %d" id) t))))))
(defn execute-and-send
"Execute and Send an `EmailReport`. This includes running the data query behind the report, formatting the email,
and sending the email to any specified recipients."
[report-id]
{:pre [(integer? report-id)]}
(let [email-report (-> (sel :one :fields execution-details-fields :id report-id)
;; TODO - this feels heavy handed. need to check `sel` macro about clob handling
(u/assoc* :description (u/jdbc-clob->str (:description <>))
:email_addresses (u/jdbc-clob->str (:email_addresses <>))
:recipients (sel :many :fields ['metabase.models.user/User :id :email]
(where {:id [in (subselect EmailReportRecipients
(fields :user_id)
(where {:emailreport_id report-id}))]}))))
report-execution (ins EmailReportExecutions
:report_id report-id
:organization_id (:organization_id email-report)
:details email-report
:status "running"
:created_at (u/new-sql-timestamp)
:started_at (u/new-sql-timestamp)
:error ""
:sent_email "")]
(log/debug (format "Starting EmailReport Execution: %d" (:id report-execution)))
(execute report-execution)
(log/debug (format "Finished EmailReport Execution: %d" (:id report-execution)))
;; return the id of the report-execution
(:id report-execution)))
(defn- execute
"Internal handling of EmailReport sending."
[{execution-id :id {:keys [name creator_id dataset_query recipients email_addresses]} :details}]
(let [email-subject (str (or name "Your Email Report") " - " (u/now-with-format "MMMM dd, YYYY"))
email-recipients (->> (filter identity (map (fn [recip] (:email recip)) recipients))
(into (clojure.string/split email_addresses #","))
(filter u/is-email?)
(into []))
email-data (driver/dataset-query dataset_query {:executed_by creator_id
:synchronously true})]
(if (= :completed (:status email-data))
(->> (send-email-report email-subject email-recipients email-data)
(report-complete execution-id))
(report-fail execution-id (format "dataset_query() failed for email report (%d): %s" execution-id (:error email-data))))))
(defn- report-fail
"Record report failure"
[execution-id msg]
(upd EmailReportExecutions execution-id
:status "failed"
:error msg
:finished_at (u/new-sql-timestamp)))
(defn- report-complete
"Record report completion"
[execution-id sent-email]
(upd EmailReportExecutions execution-id
:status "completed"
:finished_at (u/new-sql-timestamp)
:sent_email (or sent-email "")))
(ns metabase.api.emailreport-test
"Tests for /api/emailreport endpoints."
(:require [clojure.tools.macro :refer [symbol-macrolet]]
[expectations :refer :all]
[korma.core :refer :all]
[metabase.db :refer :all]
(metabase.models [common :as common]
[database :refer [Database]]
[emailreport :refer [EmailReport] :as emailreport])
[metabase.test.util :refer [match-$ expect-eval-actual-first random-name with-temp]]
[metabase.test-data :refer :all]))
;; ## Helper Fns
(defn create-email-report [& {:as kwargs}]
((user->client :rasta) :post 200 "emailreport"
(merge {:name "My Cool Email Report"
:mode (emailreport/mode->id :active)
:public_perms common/perms-readwrite
:email_addresses ""
:recipients [(user->id :lucky)]
:dataset_query {:type "query"
:query {:source_table (table->id :venues)
:filter [nil nil]
:aggregation ["rows"]
:breakout [nil]
:limit nil}
:database (:id @test-db)}
:schedule {:days_of_week {:mon true
:tue true
:wed true
:thu true
:fri true
:sat true
:sun true}
:time_of_day "morning"
:timezone ""}
:organization @org-id}
kwargs)))
;; ## GET /api/emailreport/form_input
;; Test that we can get the form input options for the Test Org
(expect-let [_ @test-db ; force lazy loading of Test Data / Metabase DB
_ (cascade-delete Database :name [not= "Test Database"])] ; Delete all Databases that aren't the Test DB
{:users #{{:id (user->id :rasta), :name "Rasta Toucan"}
{:id (user->id :crowberto), :name "Crowberto Corv"}
{:id (user->id :lucky), :name "Lucky Pigeon"}
{:id (user->id :trashbird), :name "Trash Bird"}}
:databases [{:id (:id @test-db)
:name "Test Database"}],
:timezones ["GMT"
"UTC"
"US/Alaska"
"US/Arizona"
"US/Central"
"US/Eastern"
"US/Hawaii"
"US/Mountain"
"US/Pacific"
"America/Costa_Rica"]
:times_of_day [{:id "morning", :realhour 8, :name "Morning"}
{:id "midday", :realhour 12, :name "Midday"}
{:id "afternoon", :realhour 16, :name "Afternoon"}
{:id "evening", :realhour 20, :name "Evening"}
{:id "midnight", :realhour 0, :name "Midnight"}]
:days_of_week [{:id "sun", :name "Sun"}
{:id "mon", :name "Mon"}
{:id "tue", :name "Tue"}
{:id "wed", :name "Wed"}
{:id "thu", :name "Thu"}
{:id "fri", :name "Fri"}
{:id "sat", :name "Sat"}]
:modes [{:name "Active", :id 1}
{:name "Disabled", :id 2}]
:permissions [{:name "None", :id 0}
{:name "Read Only", :id 1}
{:name "Read & Write", :id 2}]}
(-> ((user->client :rasta) :get 200 "emailreport/form_input" :org @org-id) ; convert to a set so test doesn't fail if order differs
(update-in [:users] set)))
;; ## POST /api/emailreport
(expect-eval-actual-first
(match-$ (sel :one EmailReport (order :id :DESC))
{:description nil
:email_addresses ""
:schedule {:days_of_week {:mon true
:tue true
:wed true
:thu true
:fri true
:sat true
:sun true}
:timezone ""
:time_of_day "morning"}
:recipients [(match-$ (fetch-user :lucky)
{:email "lucky@metabase.com"
:first_name "Lucky"
:last_login $
:is_superuser false
:id $
:last_name "Pigeon"
:date_joined $
:common_name "Lucky Pigeon"})]
:organization_id @org-id
:name "My Cool Email Report"
:mode (emailreport/mode->id :active)
:creator_id (user->id :rasta)
:updated_at $
:dataset_query {:database (:id @test-db)
:query {:limit nil
:breakout [nil]
:aggregation ["rows"]
:filter [nil nil]
:source_table (table->id :venues)}
:type "query"}
:id $
:version 1
:public_perms common/perms-readwrite
:created_at $})
(create-email-report))
;; ## GET /api/emailreport/:id
(expect-eval-actual-first
(match-$ (sel :one EmailReport (order :id :DESC))
{:description nil
:email_addresses ""
:can_read true
:schedule {:days_of_week {:mon true
:tue true
:wed true
:thu true
:fri true
:sat true
:sun true}
:timezone ""
:time_of_day "morning"}
:creator (match-$ (fetch-user :rasta)
{:common_name "Rasta Toucan"
:date_joined $
:last_name "Toucan"
:id $
:is_superuser false
:last_login $
:first_name "Rasta"
:email "rasta@metabase.com"})
:recipients [(match-$ (fetch-user :lucky)
{:email "lucky@metabase.com"
:first_name "Lucky"
:last_login $
:is_superuser false
:id $
:last_name "Pigeon"
:date_joined $
:common_name "Lucky Pigeon"})]
:can_write true
:organization_id @org-id
:name "My Cool Email Report"
:mode (emailreport/mode->id :active)
:organization {:id @org-id
:slug "test"
:name "Test Organization"
:description nil
:logo_url nil
:report_timezone nil
:inherits true}
:creator_id (user->id :rasta)
:updated_at $
:dataset_query {:database (:id @test-db)
:query {:limit nil
:breakout [nil]
:aggregation ["rows"]
:filter [nil nil]
:source_table (table->id :venues)}
:type "query"}
:id $
:version 1
:public_perms common/perms-readwrite
:created_at $})
(let [{id :id} (create-email-report)]
((user->client :rasta) :get 200 (format "emailreport/%d" id))))
;; ## DELETE /api/emailreport/:id
(let [er-name (random-name)
er-exists? (fn [] (exists? EmailReport :name er-name))]
(expect-eval-actual-first
[false
true
false]
[(er-exists?)
(do (create-email-report :name er-name)
(er-exists?))
(let [{id :id} (sel :one EmailReport :name er-name)]
((user->client :rasta) :delete 204 (format "emailreport/%d" id))
(er-exists?))]))
;; ## RECPIENTS-RELATED TESTS
;; * Check that recipients are returned by GET /api/emailreport/:id
;; * Check that we can set them via PUT /api/emailreport/:id
(expect
[#{}
#{:rasta}
#{:crowberto :lucky}
#{}]
(with-temp EmailReport [{:keys [id]} {:creator_id (user->id :rasta)
:name (random-name)
:organization_id @org-id
:dataset_query {}
:schedule {}}]
(symbol-macrolet [get-recipients (->> ((user->client :rasta) :get 200 (format "emailreport/%d" id))
:recipients
(map :id)
(map id->user)
set)]
(let [put-recipients (fn [& user-kws]
((user->client :rasta) :put 200 (format "emailreport/%d" id) {:recipients (map user->id user-kws)})
get-recipients)]
[get-recipients
(put-recipients :rasta)
(put-recipients :lucky :crowberto)
(put-recipients)]))))
......@@ -16,34 +16,3 @@
"It can be safely ignored if you did not request a password reset. Click the link below to reset your password.</p>"
"<p><a href=\"http://localhost/some/url\">http://localhost/some/url</a></p></body></html>")
(send-password-reset-email "test@test.com" "test.domain.com" "http://localhost/some/url"))
;; email report
(expect
(str "<html><head></head>"
"<body style=\"font-family: Helvetica Neue, Helvetica, sans-serif; width: 100%; margin: 0 auto; max-width: 800px; font-size: 12px;\">"
"<div class=\"wrapper\" style=\"padding: 10px; background-color: #ffffff;\">"
"<table style=\"border: 1px solid #cccccc; width: 100%; border-collapse: collapse;\">"
"<tr style=\"background-color: #f4f4f4;\">"
"<td style=\"text-align: left; padding: 0.5em; border: 1px solid #ddd; font-size: 12px;\">first</td>"
"<td style=\"text-align: left; padding: 0.5em; border: 1px solid #ddd; font-size: 12px;\">second</td>"
"</tr>"
"<tr>"
"<td style=\"border: 1px solid #ddd; padding: 0.5em;\">N/A</td>"
"<td style=\"border: 1px solid #ddd; padding: 0.5em;\">N/A</td>"
"</tr>"
"<tr>"
"<td style=\"border: 1px solid #ddd; padding: 0.5em;\">abc</td>"
"<td style=\"border: 1px solid #ddd; padding: 0.5em;\">def</td>"
"</tr>"
"<tr>"
"<td style=\"border: 1px solid #ddd; padding: 0.5em;\">1,000</td>"
"<td style=\"border: 1px solid #ddd; padding: 0.5em;\">17.45</td>"
"</tr>"
"</table>"
"</div>"
"</body>"
"</html>")
(send-email-report "My Email Report" ["test.domain.com"] {:data {:columns ["first" "second"]
:rows [[nil nil]
["abc" "def"]
[1000 17.45234]]}}))
(ns metabase.models.emailreport-test
(:require [clojure.tools.macro :refer [symbol-macrolet]]
[expectations :refer :all]
[medley.core :as m]
[metabase.db :refer :all]
(metabase.models [emailreport :refer :all]
[emailreport-recipients :refer :all])
[metabase.test-data :refer :all]
[metabase.test.util :as tu]))
;; ## UPDATE-RECIPIENTS
;; Check that update-recipients inserts/deletes EmailReportRecipeints as we expect
(expect
[#{}
#{:rasta :lucky}
#{:rasta :lucky :trashbird}
#{:trashbird}
#{:crowberto :lucky}
#{}
#{:rasta}]
(tu/with-temp EmailReport [report {:creator_id (user->id :rasta)
:name (tu/random-name)
:organization_id @org-id
:dataset_query {}
:schedule {}}]
(symbol-macrolet [recipients (->> (sel :many :field [EmailReportRecipients :user_id] :emailreport_id (:id report))
(map id->user)
set)]
(let [upd-recipients (fn [& recipient-ids]
(update-recipients report (map user->id recipient-ids))
recipients)]
[recipients
(upd-recipients :rasta :lucky)
(upd-recipients :rasta :lucky :trashbird)
(upd-recipients :trashbird)
(upd-recipients :crowberto :lucky)
(upd-recipients)
(upd-recipients :rasta)]))))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment