Skip to content

Commit 10c4f92

Browse files
committed
Added code for simulated run and Slack notifications
1 parent 010a0e1 commit 10c4f92

File tree

4 files changed

+393
-1
lines changed

4 files changed

+393
-1
lines changed

+ww/Params.m

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ function save(obj)
101101
'minLastSent', 2, ...
102102
'Email_format', 'html', ...
103103
'Email_admins', {''}, ...
104-
'Email_recipients', {''});
104+
'Email_recipients', {''}, ...
105+
'Slack_webhook', '', ...
106+
'Slack_admins', {''}, ...
107+
'Slack_recipients', {''} ...
108+
);
105109
end
106110

107111
function parspath = path

+ww/formatSimTable.m

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
function out = formatSimTable(data, format)
2+
% WW.FORMATTABLE Returns and/or saves the water table in a given format
3+
% Returns tabular data in a number of optional formats. Note: Only 'html'
4+
% is currently used and supported.
5+
%
6+
% Inputs:
7+
% data (table): A table of Subject names, percent weights and water
8+
% amounts for given dates
9+
% format (char): Formats include 'png', 'tsv', 'html'. If no format is
10+
% specified, a tab separated string is returned. If
11+
% 'tsv' a file is also saved to disk.
12+
%
13+
% TODO Improve debugging mode
14+
15+
if nargin == 1; format = ''; end
16+
dataInCells = table2cell(data);
17+
18+
nDays = size(dataInCells{1, 3}, 2);
19+
nAnimals = height(data);
20+
waterAmounts = reshape([dataInCells{:, 3}], nDays, nAnimals)';
21+
dataInCells = cat(2, dataInCells(:, 1:2), waterAmounts);
22+
columnHeaders = {' Animal '; [' Weight % on ', datestr(now, 'dddd ')]};
23+
for iDay = 1:nDays
24+
columnHeaders{iDay+2} = datestr(now+iDay, ' ddd, dd-mmm-yyyy ');
25+
end
26+
switch lower(format)
27+
case 'png'
28+
hFig = figure;
29+
hTable = uitable(hFig, 'Data', dataInCells);
30+
hTable.ColumnName = columnHeaders;
31+
% Fit the table nicely inside the figure
32+
hTable.Units = 'normalized';
33+
hTable.FontWeight = 'bold';
34+
hTable.Position = [0 0 1 1];
35+
extent = hTable.Extent;
36+
pos = hTable.Parent.Position;
37+
pos(1) = 100;
38+
pos(2) = 100;
39+
pos(3) = pos(3)*extent(3);
40+
pos(4) = pos(4)*extent(4);
41+
hTable.Parent.Position = pos;
42+
out = print(hFig, 'water', '-dpng', '-r300');
43+
case 'tsv'
44+
filename = fullfile(iff(ispc, getenv('APPDATA'), getenv('HOME')), 'water.tsv');
45+
fid = fopen(filename, 'wt');
46+
out = strrep(evalc('disp(dataInCells)'), '\n', '\r');
47+
fwrite(fid, out);
48+
fclose(fid);
49+
case 'txt'
50+
dataInCells(cellfun('isempty', dataInCells)) = {' '};
51+
columnWidth = cellfun(@length, columnHeaders);
52+
[nRows, nColumns] = size(dataInCells);
53+
dataInCellsPadded = cell(nRows, nColumns);
54+
for iColumn = 1:nColumns
55+
for iRow = 1:nRows
56+
dataInCellsPadded{iRow, iColumn} = pad(dataInCells{iRow, iColumn}, columnWidth(iColumn), 'both');
57+
end
58+
end
59+
% Print the headers
60+
out = sprintf('%s', cell2mat(columnHeaders(:)'));
61+
% Print each row -
62+
for iRow = 1:nRows
63+
out = sprintf('%s\n%s', out, cell2mat(dataInCellsPadded(iRow, :)));
64+
end
65+
%%% FOR DEBUGGING %%%
66+
if ww.Params().get('Mode') > 0
67+
save(fullfile(userpath, 'printWeekendWaterVars.mat'))
68+
end
69+
70+
71+
case 'html'
72+
dataInCells(cellfun('isempty', dataInCells)) = {' '};
73+
% Print the headers
74+
out = sprintf(...
75+
'<table style="width:100%%">\n<tr>\n\t<th style="padding:5px">%s</th>\n</tr>',....
76+
strjoin(strip(columnHeaders), '</th>\n\t<th style="padding:5px">'));
77+
% Print each row -
78+
% NB: Line breaks are essential to avoid 1000 charecter line limit.
79+
for i = 1:size(dataInCells,1)
80+
rowStr = sprintf(...
81+
'\n<tr>\n\t<td style="padding:5px">%s</td>\n</tr>',...
82+
strjoin(dataInCells(i,:), '</td>\n\t<td style="padding:5px">'));
83+
out = [out, rowStr];
84+
end
85+
out = [out, newline, '</table>'];
86+
%%% FOR DEBUGGING %%%
87+
if ww.Params().get('Mode') > 0
88+
save(fullfile(userpath, 'printWeekendWaterVars.mat'))
89+
end
90+
%%%
91+
otherwise
92+
columnHeaders = cellfun(@strtrim,columnHeaders,'uni',0);
93+
out = sprintf([repmat('\t%s',1,length(columnHeaders)), '\r'],columnHeaders{:});
94+
for row = 1:size(dataInCells,1)
95+
for col = 1:size(dataInCells,2)
96+
out = [out sprintf('\t%s',dataInCells{row,col})];
97+
end
98+
out = sprintf('%s\r', out);
99+
end
100+
end

+ww/simulate.m

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
function simulate(nDays, varargin)
2+
% GENERATE Create a list of subjects and dates to be trained on weekend
3+
%
4+
% Inputs:
5+
% nDays: The number of day in the future to post water for. If empty or
6+
% not defined, the 'nDaysInFuture' parameter is used (adjusted
7+
% for bank holidays)
8+
%
9+
% Named Parameters:
10+
% force: When true the script is run regardless of the 'minLastSent'
11+
% parameter (default: false)
12+
% test: When true the script is run in test mode (default: false)
13+
%
14+
% Examples:
15+
% % Run using standard defaults (2 days in future)
16+
% ww.generate
17+
%
18+
% % Post water for 3 days in future, regardless of when it was last run
19+
% ww.generate(3, 'force', true)
20+
21+
% Ensure we're on the correct branch and up-to-date
22+
% git.runCmd({'checkout dev', 'pull'}, 'dir', getOr(dat.paths, 'rigbox'));
23+
24+
send_emails = false;
25+
26+
% Load the parameters
27+
params = ww.Params;
28+
admins = strip(lower(params.get('Email_admins')));
29+
admins_slack = params.get('Slack_admins');
30+
webhook = params.get('Slack_webhook');
31+
mySlackName = 'Weekend Water';
32+
mySlackEmoji = ':potable_water:';
33+
34+
% Parse input arguments (override params)
35+
p = inputParser;
36+
p.addParameter('force', false, @islogical)
37+
p.addParameter('test', params.get('Mode') == 2, @islogical)
38+
p.parse(varargin{:})
39+
force = p.Results.force;
40+
test = p.Results.test;
41+
debug = params.get('Mode') > 0 || test;
42+
% Temp dir for saving log and email
43+
tmpdir = iff(ispc, getenv('APPDATA'), getenv('HOME'));
44+
if debug
45+
% In debug mode we activate the log
46+
diaryState = get(0, 'Diary');
47+
diaryFile = get(0, 'DiaryFile');
48+
diary(fullfile(tmpdir, 'ww.log'))
49+
end
50+
51+
% Get list of mice to be trained over the weekend. These will be marked as
52+
% 'PIL' on the list (so long as they've been weighed)
53+
excl = dat.loadParamProfiles('WeekendWater');
54+
if isempty(fieldnames(excl)), excl = struct; end
55+
56+
% Path to email file which will be sent
57+
filename = sprintf('mail%s.txt', iff(test, '-test', ''));
58+
mail = fullfile(tmpdir, filename);
59+
60+
% Check when email was last generated and potentially return if too soon
61+
mod = file.modDate(mail);
62+
minLastSent = params.get('minLastSent'); % min number of days before next
63+
if ~force && ~test && ~isempty(mod) && (now - mod < minLastSent)
64+
fprintf('Email already sent in last %.2g days\n', minLastSent)
65+
return
66+
end
67+
68+
if send_emails
69+
% Set email prefs for sending the email
70+
% TODO These may no longer be required as we use curl
71+
props = java.lang.System.getProperties;
72+
props.setProperty('mail.smtp.auth','true');
73+
props.setProperty('mail.smtp.port', num2str(params.get('SMTP_Port')));
74+
props.setProperty('mail.smtp.starttls.enable','true');
75+
% We use MATLAB to send plain text warnings
76+
internetPrefs = getpref('Internet');
77+
for prop = string(fieldnames(internetPrefs))'
78+
setpref('Internet', prop, params.get(prop))
79+
end
80+
end
81+
82+
if nargin == 0 || isempty(nDays)
83+
% use 2 for usual weekends, 3 for long weekends etc.
84+
[nDays, fail] = ww.getNumDays();
85+
if fail && ~test
86+
if send_emails
87+
recipients = admins;
88+
for iRecipient = 1:numel(recipients)
89+
sendmail(recipients{iRecipient}, 'Action required: Days may be incorrect',...
90+
['Weekend water script failed to determine whether there are '...
91+
'any upcoming Bank holidays. Investigate.']);
92+
end
93+
end
94+
% send Slack notification to admins
95+
recipients = admins_slack;
96+
str2send = sprintf('%s\n%s\n%s', 'Action required: Days may be incorrect',...
97+
['Weekend water script failed to determine whether there are '...
98+
'any upcoming Bank holidays.'], 'Investigate.');
99+
for iRecipient = 1:numel(recipients)
100+
SendSlackNotification(webhook, str2send, recipients{iRecipient}, mySlackName, [], mySlackEmoji)
101+
end
102+
103+
end
104+
end
105+
106+
% Alyx instance
107+
ai = Alyx('','');
108+
if test
109+
ai.BaseURL = params.get('ALYX_DEV_URL');
110+
fprintf('Using test database: %s\n', ai.BaseURL);
111+
end
112+
ai = ai.login(params.get('ALYX_Login'), params.get('ALYX_Password'));
113+
114+
% Table of users and their emails from database
115+
users = ai.getData('users');
116+
117+
% Extract the data from alyx and give water to whomever needs it
118+
[data, skipped] = ww.simulateWeekendWater(ai, nDays, excl);
119+
if height(data) == 0, return, end % Return if there are no restricted mice
120+
121+
if ~isempty(skipped) && ~test
122+
msg = sprintf(['The following mice have no weekend water information:\n\r %s\n\r',...
123+
'This occured because a weight for today was not inputted into Alyx before 6pm. \n',...
124+
'Please manually write the weight and water to be given on the paper sheet upstairs. ',...
125+
'For the days you will be training, please write ''PIL''.'], strjoin({skipped.subject}, '\n'));
126+
[~,I] = intersect({users.username}, {skipped.user});
127+
% sendmail(vertcat(users(I).email, admins),...
128+
% 'Action required: Weekend information missing', msg);
129+
% Single-instance message headers must be included only once in a
130+
% message (RFC 5322). Multiple recipients to sendmail duplicates 'To'
131+
% field, so we'll send one email per recipient instead.
132+
if send_emails
133+
recipients = vertcat(users(I).email, admins);
134+
for iRecipient = 1:numel(recipients)
135+
sendmail(recipients{iRecipient}, 'Action required: Weekend information missing', msg);
136+
end
137+
end
138+
% recipients = vertcat(params.get('Slack_recipients'), admins_slack);
139+
% for iRecipient = 1:numel(recipients)
140+
% SendSlackNotification(webhook, sprintf('%s\n%s', 'Action required: Weekend information missing', msg), ...
141+
% recipients{iRecipient}, mySlackName, [], mySlackEmoji);
142+
% end
143+
144+
end
145+
146+
% print nicely, 'water.png' will be saved in the current folder
147+
slackData = ww.formatSimTable(data, 'txt');
148+
msg = sprintf('%s\n%s', 'This is the expected weekend water table for later today.', ...
149+
'You still have time to fix things (weights/PIL/override water amounts)');
150+
slackMessage = sprintf('%s\n```%s```', msg, slackData);
151+
152+
data = ww.formatTable(data, params.get('Email_format'));
153+
% Get list of email recipients
154+
recipients = strip(lower(params.get('Email_recipients')));
155+
% In test mode only send to admin, otherwise send to all recipients and admins
156+
to = iff(test, admins(1), union(recipients, admins));
157+
158+
159+
%% 'Weekend water',...
160+
% Write email to file
161+
fid = fopen(mail, 'w', 'n', 'UTF-8');
162+
fprintf(fid, ['From: Alyx Database <%s>\n',...
163+
'Reply-To: Alyx Database <%s>\n',...
164+
'To: %s\nSubject: Weekend Water\n',...
165+
'Content-Type: text/html; charset="utf-8"\n',...
166+
'Content-Transfer-Encoding: quoted-printable\n',...
167+
'Mime-version: 1.0\n\n<!DOCTYPE html><html lang="en-GB"><head>',...
168+
'<title>Weekend Water Email</title>'...
169+
'</head><body>\n',...
170+
'Please find below the water table for this weekend. ',...
171+
'Any blank spaces must be filled in manually by the respective ',...
172+
'PILs on the paper copy. Let us know if they fail to do so. \n \r',...
173+
'%s\n</body></html>'],...
174+
getpref('Internet','E_mail'), getpref('Internet','E_mail'),...
175+
strjoin(to, ', '), data);
176+
fclose(fid);
177+
178+
% Construct curl command
179+
cmd = sprintf(['curl "%s:%i" -v --mail-from "%s" ',...
180+
'--mail-rcpt "%s" --ssl -u %s:%s -T "%s" -k --anyauth'],...
181+
params.get('SMTP_Server'), params.get('SMTP_Port'), params.get('E_mail'), ...
182+
strjoin(to, '" --mail-rcpt "'), getpref('Internet','E_mail'),...
183+
getpref('Internet','SMTP_Password'), strrep(mail, '\', '/'));
184+
185+
% Wrap in call to git bash
186+
gitExe = getOr(dat.paths, 'gitExe');
187+
bashPath = fullfile(gitExe(1:end-11), 'git-bash.exe');
188+
bash = @(cmd)['"',bashPath,'" -c "',cmd,'"'];
189+
190+
if send_emails
191+
failed = system(bash(cmd), '-echo'); % Send email
192+
assert(~failed, 'failed to send email')
193+
194+
% Restore previous preferences
195+
for prop = string(fieldnames(internetPrefs))'
196+
setpref('Internet', prop, internetPrefs.(prop))
197+
end
198+
end
199+
% Send Slack message to the channel
200+
recipients = vertcat(params.get('Slack_recipients'), admins_slack);
201+
% recipients = vertcat(admins_slack);
202+
for iRecipient = 1:numel(recipients)
203+
SendSlackNotification(webhook, slackMessage, recipients{iRecipient}, mySlackName, [], mySlackEmoji)
204+
end
205+
% Restore diary state
206+
if debug, diary(diaryState), set(0, 'DiaryFile', diaryFile), end

0 commit comments

Comments
 (0)