Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into 395-xml-form-version
Browse files Browse the repository at this point in the history
  • Loading branch information
Aidan Macdonald committed Apr 5, 2022
2 parents 180fe76 + 3c2f599 commit 60ef1f5
Show file tree
Hide file tree
Showing 61 changed files with 1,744 additions and 334 deletions.
19 changes: 9 additions & 10 deletions admin/src/js/controllers/images-branding.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,9 @@ angular.module('controllers').controller('ImagesBrandingCtrl',
const DOC_ID = 'branding';
const MAX_FILE_SIZE = 100000; // 100KB

$('#logo-upload .choose').on('click', _ev => {
_ev.preventDefault();
$('#logo-upload .uploader').click();
});

$('#favicon-upload .choose').on('click', _ev => {
_ev.preventDefault();
$('#favicon-upload .uploader').click();
$('#images-branding .choose').on('click', ev => {
ev.preventDefault();
$(ev.target).closest('.form-group').find('.uploader').click();
});

$scope.loading = true;
Expand All @@ -31,6 +26,7 @@ angular.module('controllers').controller('ImagesBrandingCtrl',
.then(doc => {
$scope.doc = doc;
$scope.favicon = doc._attachments[doc.resources.favicon];
$scope.icon = doc._attachments[doc.resources.icon];
})
.catch(err => {
$log.error('Error fetching resources file', err);
Expand Down Expand Up @@ -85,10 +81,12 @@ angular.module('controllers').controller('ImagesBrandingCtrl',

const updateFavicon = () => updateImage(getFile('#favicon-upload'), 'favicon');

const updateIcon = () => updateImage(getFile('#icon-upload'), 'icon');

const removeObsoleteAttachments = () => {
const current = $scope.doc._attachments;
const updated = {};
['logo', 'favicon'].forEach(key => {
['logo', 'favicon', 'icon'].forEach(key => {
const name = $scope.doc.resources[key];
if (name) {
updated[name] = current[name];
Expand All @@ -108,7 +106,8 @@ angular.module('controllers').controller('ImagesBrandingCtrl',

if (!validateTitle() ||
!updateLogo() ||
!updateFavicon()) {
!updateFavicon() ||
!updateIcon()) {
return;
}

Expand Down
21 changes: 20 additions & 1 deletion admin/src/templates/images_branding.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@
<input class="uploader" type="file" accept="image/*">
</div>
</div>
<span class="help-block" translate>branding.logo.field.help</span>
</div>
<div class="form-group required">
<label translate>branding.favicon.field</label>
<label translate>branding.favicon.field</label>
<div class="favicon">
<img src="data:{{favicon.content_type}};base64,{{favicon.data}}" />
</div>
Expand All @@ -40,6 +41,24 @@
<input class="uploader" type="file" accept="image/*">
</div>
</div>
<span class="help-block" translate>branding.favicon.field.help</span>
</div>
<div class="form-group required">
<label translate>branding.icon.field</label>
<div class="favicon">
<img src="data:{{icon.content_type}};base64,{{icon.data}}" />
</div>
<div id="icon-upload">
<button type="button" class="btn btn-default choose">
<i class="fa fa-arrow-up"></i>
<span translate>Choose file</span>
</button>
<div class="loader" ng-show="uploading"></div>
<div class="hide">
<input class="uploader" type="file" accept="image/*">
</div>
</div>
<span class="help-block" translate>branding.icon.field.help</span>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" ng-click="submit()" ng-disabled="submitting" translate>Submit</button>
Expand Down
5 changes: 5 additions & 0 deletions api/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ process
const serverUtils = require('./src/server-utils');
const uploadDefaultDocs = require('./src/upload-default-docs');
const generateServiceWorker = require('./src/generate-service-worker');
const manifest = require('./src/services/manifest');
const apiPort = process.env.API_PORT || 5988;

try {
Expand Down Expand Up @@ -61,6 +62,10 @@ process
await migrations.run();
logger.info('Database migrations completed successfully');

logger.info('Generating manifest');
await manifest.generate();
logger.info('Manifest generated successfully');

logger.info('Generating service worker');
await generateServiceWorker.run();
logger.info('Service worker generated successfully');
Expand Down
34 changes: 4 additions & 30 deletions api/src/controllers/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const logger = require('../logger');
const db = require('../db');
const localeUtils = require('locale');
const cookie = require('../services/cookie');
const brandingService = require('../services/branding');

const templates = {
login: {
Expand Down Expand Up @@ -192,36 +193,8 @@ const setCookies = (req, res, sessionRes) => {
});
};

const getInlineImage = (data, contentType) => `data:${contentType};base64,${data}`;

const getDefaultBranding = () => {
const logoPath = path.join(__dirname, '..', 'resources', 'logo', 'medic-logo-light-full.svg');
return promisify(fs.readFile)(logoPath, {}).then(logo => {
const data = Buffer.from(logo).toString('base64');
return {
name: 'Medic',
logo: getInlineImage(data, 'image/svg+xml')
};
});
};

const getBranding = () => {
return db.medic.get('branding', {attachments: true})
.then(doc => {
const image = doc._attachments[doc.resources.logo];
return {
name: doc.title,
logo: getInlineImage(image.data, image.content_type)
};
})
.catch(err => {
logger.warn('Could not find branding doc on CouchDB: %o', err);
return getDefaultBranding();
});
};

const renderTokenLogin = (req, res) => {
return getBranding()
return brandingService.get()
.then(branding => render('tokenLogin', req, branding, { tokenUrl: req.url }))
.then(body => res.send(body));
};
Expand Down Expand Up @@ -283,7 +256,8 @@ const loginByToken = (req, res) => {
};

const renderLogin = (req) => {
return getBranding().then(branding => render('login', req, branding));
return brandingService.get()
.then(branding => render('login', req, branding));
};

module.exports = {
Expand Down
52 changes: 52 additions & 0 deletions api/src/services/branding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const fs = require('fs');
const { promisify } = require('util');
const path = require('path');

const db = require('../db');
const logger = require('../logger');

const DEFAULT_LOGO_PATH = path.join(__dirname, '..', 'resources', 'logo', 'medic-logo-light-full.svg');

const getInlineImage = (data, contentType) => `data:${contentType};base64,${data}`;

const getBrandingDoc = async () => {
try {
return await db.medic.get('branding', { attachments: true });
} catch(e) {
logger.warn('Could not find branding doc on CouchDB: %o', e);
return;
}
};

const getName = (doc) => (doc && doc.title) || 'CHT';

const getLogo = async (doc) => {
let data;
let contentType;
const name = doc && doc.resources && doc.resources.logo;
if (name) {
const image = doc._attachments[name];
data = image.data;
contentType = image.content_type;
} else {
const logo = await promisify(fs.readFile)(DEFAULT_LOGO_PATH, {});
data = Buffer.from(logo).toString('base64');
contentType = 'image/svg+xml';
}
return getInlineImage(data, contentType);
};

const getIcon = (doc) => (doc && doc.resources && doc.resources.icon) || 'icon.png';

const getBranding = async () => {
const doc = await getBrandingDoc();
const logo = await getLogo(doc);
return {
name: getName(doc),
logo: logo,
icon: getIcon(doc),
doc: doc
};
};

module.exports.get = getBranding;
10 changes: 9 additions & 1 deletion api/src/services/config-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const ddocExtraction = require('../ddoc-extraction');
const resourceExtraction = require('../resource-extraction');
const generateXform = require('./generate-xform');
const generateServiceWorker = require('../generate-service-worker');
const manifest = require('./manifest');
const config = require('../config');

const MEDIC_DDOC_ID = '_design/medic';
Expand Down Expand Up @@ -119,7 +120,14 @@ const handleFormChange = (change) => {
};

const handleBrandingChanges = () => {
return updateServiceWorker();
return updateManifest()
.then(() => updateServiceWorker());
};

const updateManifest = () => {
return manifest.generate().catch(err => {
logger.error('Failed to generate manifest: %o', err);
});
};

const updateServiceWorker = () => {
Expand Down
35 changes: 35 additions & 0 deletions api/src/services/manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { promisify } = require('util');
const fs = require('fs');
const path = require('path');

const _ = require('lodash');

const environment = require('../environment');
const brandingService = require('./branding');

const EXTRACTED_RESOURCES_PATH = environment.getExtractedResourcesPath();
const MANIFEST_OUTPUT_PATH = path.join(EXTRACTED_RESOURCES_PATH, 'manifest.json');
const TEMPLATE_PATH = path.join(__dirname, '..', 'templates', 'manifest.json');

const writeJson = async (branding) => {
const file = await promisify(fs.readFile)(TEMPLATE_PATH, { encoding: 'utf-8' });
const template = _.template(file);
const json = template({ branding });
return await promisify(fs.writeFile)(MANIFEST_OUTPUT_PATH, json);
};

const writeIcon = async (doc) => {
const name = doc && doc.resources && doc.resources.icon;
const attachment = name && doc._attachments[name];
if (attachment) {
const contents = Buffer.from(attachment.data, 'base64');
const outputPath = path.join(EXTRACTED_RESOURCES_PATH, 'img', name);
await promisify(fs.writeFile)(outputPath, contents);
}
};

module.exports.generate = async () => {
const branding = await brandingService.get();
await writeJson(branding);
await writeIcon(branding.doc);
};
59 changes: 57 additions & 2 deletions api/src/services/report/smsparser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const mpParser = require('./mp-parser');
const javarosaParser = require('./javarosa-parser');
const textformsParser = require('./textforms-parser');
const logger = require('../../logger');
const moment = require('moment');
const bs = require('bikram-sambat');

const MUVUKU_REGEX = /^\s*([A-Za-z]?\d)!.+!.+/;

Expand Down Expand Up @@ -117,6 +119,25 @@ const parseNum = raw => {
return Number(std);
};

const bsToEpoch = (bsYear, bsMonth, bsDay) => {
try {
const gregDate = bs.toGreg_text(bsYear, bsMonth, bsDay);
return moment(gregDate).valueOf();
} catch (exception) {
logger.error('The provided date could not be converted: %o.', exception);
return null;//should be caught by validation in registration
}
};

const getFieldByType = (def, type) => {
if (!def || !def.fields) {
return;
}
return Object
.keys(def.fields)
.find(k => def.fields[k] && def.fields[k].type === type);
};

const lower = str => (str && str.toLowerCase ? str.toLowerCase() : str);

exports.parseField = (field, raw) => {
Expand Down Expand Up @@ -159,7 +180,15 @@ exports.parseField = (field, raw) => {
}
// YYYY-MM-DD assume muvuku format for now
// store in milliseconds since Epoch
return new Date(raw).valueOf();
return moment(raw).valueOf();
case 'bsDate': {
if (!raw) {
return null;
}
const separator = raw[raw.search(/[^0-9]/)];//non-numeric character
const dateParts = raw.split(separator);
return bsToEpoch(...dateParts);
}
case 'boolean': {
if (raw === undefined) {
return;
Expand Down Expand Up @@ -194,7 +223,7 @@ exports.parse = (def, doc) => {
let msgData;
const formData = {};
let addOmittedFields = false;

const aggregateBSDateField = getFieldByType(def, 'bsAggreDate');
if (!def || !doc || !doc.message || !def.fields) {
return {};
}
Expand Down Expand Up @@ -237,6 +266,32 @@ exports.parse = (def, doc) => {
}
}

if(aggregateBSDateField) {
let bsYear;
let bsMonth = 1;
let bsDay = 1;
for (const k of Object.keys(def.fields)) {
switch (def.fields[k].type) {
case 'bsYear':
bsYear = msgData[k];
break;
case 'bsMonth':
bsMonth = msgData[k];
break;
case 'bsDay':
bsDay = msgData[k];
break;
}
}

if(!bsYear) {
logger.error('Can not aggregate bsAggreDate without bsYear');
return;
}

formData[aggregateBSDateField] = bsToEpoch(bsYear, bsMonth, bsDay);
}

// pass along some system generated fields
if (msgData._extra_fields === true) {
formData._extra_fields = true;
Expand Down
2 changes: 2 additions & 0 deletions api/src/templates/login/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#323232">
<title>{{ branding.name }}</title>
<link href="/login/style.css" media="all" rel="stylesheet">
<link rel="shortcut icon" href="/favicon.ico">
<link rel="manifest" href="/manifest.json">
</head>
<body data-default-locale="{{ defaultLocale }}" data-translations="{{ translations }}">
<div class="center">
Expand Down
2 changes: 2 additions & 0 deletions api/src/templates/login/token-login.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#323232">
<title>{{ branding.name }}</title>
<link href="/login/style.css" media="all" rel="stylesheet">
<link rel="shortcut icon" href="/favicon.ico">
<link rel="manifest" href="/manifest.json">
</head>
<body data-default-locale="{{ defaultLocale }}" data-translations="{{ translations }}">
<div class="center">
Expand Down
Loading

0 comments on commit 60ef1f5

Please sign in to comment.