Skip to content

feat(#8462): support exporting users devices details #8797

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 44 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2d7a15e
first iteration that returns raw json
m5r Jan 8, 2024
e1ad10f
rough json > csv
m5r Jan 8, 2024
7d65cd6
refactor to use the common export service
m5r Jan 10, 2024
4b81989
remove unused view
m5r Jan 10, 2024
758869a
remove unused code
m5r Jan 10, 2024
ed1205e
couchdb 3.x uses SpiderMonkey 60 which supports ES6 - https://blog.co…
m5r Jan 11, 2024
aba77df
unit test the exporter
m5r Jan 11, 2024
b669bc7
rename usersDevices => user-devices
m5r Jan 11, 2024
236f6ab
translation strings
m5r Jan 11, 2024
c3d3d53
gate behind `can_export_feedback` permission
m5r Jan 11, 2024
b290769
revert whitespace changes
m5r Jan 15, 2024
68e5be5
use new `can_export_devices_details` permission
m5r Jan 18, 2024
016ed24
does this make sonar happy?
m5r Jan 18, 2024
563fbed
Revert "does this make sonar happy?"
m5r Jan 18, 2024
757748e
export user devices details to JSON format instead of CSV
m5r Jan 25, 2024
a96a449
fix export users devices test now returning JSON
m5r Jan 25, 2024
ae7ef6f
parse date strings to compare them correctly
m5r Feb 7, 2024
70b2be9
define $scope.exporting to leverage the `ng-disabled="exporting"` tha…
m5r Feb 7, 2024
1f043fc
define $scope.exporting in other exporter modules
m5r Feb 7, 2024
5f586ee
consistent name
m5r Feb 7, 2024
8c1b267
make reduce do less work
m5r Feb 8, 2024
68033b0
simplify date comparison by properly formatting date strings
m5r Feb 8, 2024
92de5b4
add null checks
m5r Feb 8, 2024
0a49de2
add spanish and french translations
m5r Feb 8, 2024
1719be1
replace translation key "User\ devices"
m5r Feb 8, 2024
1340a4e
export.button.download
m5r Feb 8, 2024
5914ccd
test permission mapping
m5r Feb 8, 2024
8877e08
define function `pad`
m5r Feb 13, 2024
467fff4
test the `$scope.exporting` logic
m5r Feb 13, 2024
c05dd7d
parse user agent with `bowser`, support all browsers instead of just …
m5r Feb 13, 2024
591857c
fix unit test
m5r Feb 13, 2024
27d6b8b
rename user-devices-mapper.js to user-devices.js to follow the curren…
m5r Feb 13, 2024
8ae975d
s/CSV/JSON/
m5r Feb 13, 2024
582a99f
update translations
m5r Feb 13, 2024
475d1f1
Switch up null-checks in map function.
jkuester Feb 15, 2024
238b05e
Tighten up logic for parsing userAgent.
jkuester Feb 15, 2024
cdbf2c6
Add unit tests for exporting
jkuester Feb 20, 2024
fabe2d3
Tweak ajax-download to try to be more tolerant of content-disposition
jkuester Feb 20, 2024
16f0886
Remove only from tests
jkuester Feb 20, 2024
118ed75
Add integration tests for user-devices
jkuester Feb 20, 2024
23f360d
Merge branch 'master' into 8462-export-user-devices-details-to-csv
jkuester Feb 20, 2024
0be605a
Map to user/deviceId instead of just user
jkuester Feb 21, 2024
80d37e8
Clean up formatting!
jkuester Feb 21, 2024
48c3517
Re-nest code in ajax-download to give access to response
jkuester Feb 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion admin/src/js/controllers/export-contacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ angular.module('controllers').controller('ExportContactsCtrl',
$scope.overwrite = false;

$scope.export = function() {
Export('contacts', {}, { humanReadable: true });
if ($scope.exporting) {
return;
}

$scope.exporting = true;
Export('contacts', {}, { humanReadable: true })
.finally(() => {
$scope.exporting = false;
$scope.$apply();
});
};

$scope.import = function() {
Expand Down
11 changes: 10 additions & 1 deletion admin/src/js/controllers/export-dhis.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ angular.module('controllers').controller('ExportDhisCtrl',
});

$scope.export = () => {
if ($scope.exporting) {
return;
}

const { dataSet, period, place } = $scope.selected;
const filters = {
dataSet,
Expand All @@ -75,6 +79,11 @@ angular.module('controllers').controller('ExportDhisCtrl',
filters.orgUnit = place;
}

Export('dhis', filters, {});
$scope.exporting = true;
Export('dhis', filters, {})
.finally(() => {
$scope.exporting = false;
$scope.$apply();
});
};
});
11 changes: 10 additions & 1 deletion admin/src/js/controllers/export-feedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ angular
});

$scope.export = function() {
Export('feedback');
if ($scope.exporting) {
return;
}

$scope.exporting = true;
Export('feedback')
.finally(() => {
$scope.exporting = false;
$scope.$apply();
});
};
});
11 changes: 10 additions & 1 deletion admin/src/js/controllers/export-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ angular.module('controllers').controller('ExportMessagesCtrl',
'ngInject';

$scope.export = function() {
Export('messages', {}, { humanReadable: true });
if ($scope.exporting) {
return;
}

$scope.exporting = true;
Export('messages', {}, { humanReadable: true })
.finally(() => {
$scope.exporting = false;
$scope.$apply();
});
};

});
11 changes: 10 additions & 1 deletion admin/src/js/controllers/export-reports.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ angular.module('controllers').controller('ExportReportsCtrl',
'ngInject';

$scope.export = function() {
Export('reports', {}, { humanReadable: true });
if ($scope.exporting) {
return;
}

$scope.exporting = true;
Export('reports', {}, { humanReadable: true })
.finally(() => {
$scope.exporting = false;
$scope.$apply();
});
};

});
23 changes: 23 additions & 0 deletions admin/src/js/controllers/export-user-devices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
angular.module('controllers').controller('ExportUserDevicesCtrl',
function (
$scope,
Export
) {

'use strict';
'ngInject';

$scope.export = function() {
if ($scope.exporting) {
return;
}

$scope.exporting = true;
Export('user-devices', {}, {})
.finally(() => {
$scope.exporting = false;
$scope.$apply();
});
};

});
10 changes: 10 additions & 0 deletions admin/src/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ require('./controllers/edit-translation');
require('./controllers/edit-user');
require('./controllers/export-contacts');
require('./controllers/export-dhis');
require('./controllers/export-user-devices');
require('./controllers/export-feedback');
require('./controllers/export-messages');
require('./controllers/export-reports');
Expand Down Expand Up @@ -277,6 +278,15 @@ angular.module('adminApp').config(function(
}
}
})
.state('export.user-devices', {
url: '/user-devices',
views: {
tab: {
controller: 'ExportUserDevicesCtrl',
templateUrl: 'templates/export_user-devices.html'
}
}
})
.state('export.feedback', {
url: '/feedback',
views: {
Expand Down
25 changes: 18 additions & 7 deletions admin/src/js/modules/ajax-download.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,27 @@

const DEFAULT_FILE_NAME = 'download';

const getFileName = (headers) => {
const contentDisposition = headers.get('content-disposition');
const match = /filename=(?:"|)(.*?)(?:"|;|$)/.exec(contentDisposition);
return match ? match[1].trim() : DEFAULT_FILE_NAME;
};

/**
* Prompts the user to download a file given a url.
*/
exports.download = function(url) {
const element = document.createElement('a');
element.setAttribute('href', url);
element.setAttribute('download', DEFAULT_FILE_NAME);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
return fetch(url)
.then(response => response.blob()
.then(blob => {
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.style.display = 'none';
link.href = blobUrl;
link.setAttribute('download', getFileName(response.headers));
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(blobUrl);
}));
};
}());
3 changes: 2 additions & 1 deletion admin/src/js/services/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const KNOWN_EXPORTS = [
'feedback',
'messages',
'reports',
'user-devices',
];

angular.module('inboxServices').factory('Export',
Expand All @@ -20,6 +21,6 @@ angular.module('inboxServices').factory('Export',
}
const params = '?' + $.param({ filters, options });
const url = '/api/v2/export/' + type + params;
ajaxDownload.download(url);
return ajaxDownload.download(url);
};
});
3 changes: 3 additions & 0 deletions admin/src/templates/export.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@
<li role="presentation" ui-sref-active="active" mm-auth="can_export_dhis">
<a ui-sref="export.dhis" translate>DHIS2</a>
</li>
<li role="presentation" ui-sref-active="active">
<a ui-sref="export.user-devices" translate>export.tabs.user_devices</a>
</li>
</ul>
<div class="tab-content" ui-view="tab" mm-auth="can_export_all"></div>
4 changes: 2 additions & 2 deletions admin/src/templates/export_contacts.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<div class="col-sm-12">

<div class="people-section">
<legend translate>Export</legend>
<p translate>export.people.description</p>
<a class="btn btn-default" ng-click="export()" ng-disabled="exporting">
<i class="fa fa-arrow-down"></i>
<span translate>Download</span>
<span translate>export.button.download</span>
</a>
</div>

Expand Down
2 changes: 1 addition & 1 deletion admin/src/templates/export_dhis.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

<a class="btn btn-default" ng-click="export()" ng-disabled="exporting || !selected.dataSet">
<i class="fa fa-arrow-down"></i>
<span translate>Download</span>
<span translate>export.button.download</span>
</a>
</div>
</div>
2 changes: 1 addition & 1 deletion admin/src/templates/export_feedback.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<p translate>export.feedback.description</p>
<a class="btn btn-default" ng-click="export()" ng-disabled="exporting">
<i class="fa fa-arrow-down"></i>
<span translate>Download</span>
<span translate>export.button.download</span>
</a>
</div>
<div class="section">
Expand Down
2 changes: 1 addition & 1 deletion admin/src/templates/export_messages.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<p translate>export.messages.description</p>
<a class="btn btn-default" ng-click="export()" ng-disabled="exporting">
<i class="fa fa-arrow-down"></i>
<span translate>Download</span>
<span translate>export.button.download</span>
</a>
</div>
</div>
2 changes: 1 addition & 1 deletion admin/src/templates/export_reports.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<p translate>export.reports.description</p>
<a class="btn btn-default" ng-click="export()" ng-disabled="exporting">
<i class="fa fa-arrow-down"></i>
<span translate>Download</span>
<span translate>export.button.download</span>
</a>
</div>
</div>
9 changes: 9 additions & 0 deletions admin/src/templates/export_user-devices.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="col-sm-12">
<div>
<p translate>export.user-devices.description</p>
<a class="btn btn-default" ng-click="export()" ng-disabled="exporting">
<i class="fa fa-arrow-down"></i>
<span translate>export.button.download</span>
</a>
</div>
</div>
16 changes: 16 additions & 0 deletions admin/tests/unit/controllers/export-dhis.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ describe('dhis2 export controller', () => {
let getService;
let query;
let Settings;
let Export;

beforeEach(() => {
module('adminApp');
sinon.useFakeTimers(NOW);
Settings = sinon.stub().resolves({ dhis_data_sets: dhisDataSets });
Export = sinon.stub().resolves();
query = sinon.stub().resolves({ rows: [
mockContact('p1'),
mockContact('p2', { dhis: { orgUnit: 'ou-p2'}}),
Expand All @@ -28,6 +30,7 @@ describe('dhis2 export controller', () => {

module($provide => {
$provide.value('Settings', Settings);
$provide.value('Export', Export);
$provide.value('$scope', scope);
});

Expand Down Expand Up @@ -79,6 +82,19 @@ describe('dhis2 export controller', () => {
expect(!!scope.dataSets).to.be.false;
});

it('doesn\'t trigger additional exports when an export is ongoing', async () => {
expect(scope.exporting).to.be.undefined;
await getService();
scope.export();
expect(scope.exporting).to.be.true;
scope.export();
scope.export();
scope.export();
expect(Export.returnValues.length).to.equal(1);
await Export.returnValues[0];
expect(scope.exporting).to.be.false;
});

const mockContact = (name, assign) => ({
id: `contact-${name}`,
doc: Object.assign({
Expand Down
55 changes: 55 additions & 0 deletions admin/tests/unit/controllers/export-user-devices.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
describe('User devices export controller', () => {
'use strict';

const { expect } = chai;

let scope;
let getService;
let Export;

beforeEach(() => {
module('adminApp');
Export = sinon.stub().resolves();
module($provide => {
$provide.value('Export', Export);
$provide.value('$scope', scope);
});
inject(($controller, _$rootScope_) => {
scope = _$rootScope_.$new();
getService = async () => {
const result = $controller('ExportUserDevicesCtrl', {
$scope: scope,
});
return result;
};
});
});

afterEach(() => sinon.restore());

it('exports user-devices', async () => {
sinon.spy(scope, '$apply');
await getService();
scope.export();
expect(scope.exporting).to.be.true;
expect(Export.returnValues.length).to.equal(1);
await Export.returnValues[0];
expect(scope.exporting).to.be.false;
expect(Export.callCount).to.equal(1);
expect(Export.args[0]).to.deep.equal(['user-devices', {}, {}]);
expect(scope.$apply.callCount).to.equal(1);
});

it('doesn\'t trigger additional exports when an export is ongoing', async () => {
expect(scope.exporting).to.be.undefined;
await getService();
scope.export();
expect(scope.exporting).to.be.true;
scope.export();
scope.export();
scope.export();
expect(Export.returnValues.length).to.equal(1);
await Export.returnValues[0];
expect(scope.exporting).to.be.false;
});
});
Loading