dhis2-devs team mailing list archive
-
dhis2-devs team
-
Mailing list archive
-
Message #32760
[Branch ~dhis2-devs-core/dhis2/trunk] Rev 16696: tracker capture - overdue and upcoming reports now allow for pdf printing and export to CSV, exce...
------------------------------------------------------------
revno: 16696
committer: Abyot Asalefew Gizaw <abyota@xxxxxxxxx>
branch nick: dhis2
timestamp: Thu 2014-09-11 14:35:41 +0200
message:
tracker capture - overdue and upcoming reports now allow for pdf printing and export to CSV, excel is WIP
added:
dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/ng-csv.js
dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/angular/angular-sanitize.js
modified:
dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/overdue-events-controller.js
dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/overdue-events.html
dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/upcoming-events-controller.js
dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/upcoming-events.html
dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/i18n/en.json
dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/index.html
dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/app.js
dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/directives.js
dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/services.js
dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/styles/style.css
dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/views/column-modal.html
--
lp:dhis2
https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk
Your team DHIS 2 developers is subscribed to branch lp:dhis2.
To unsubscribe from this branch go to https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk/+edit-subscription
=== modified file 'dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/overdue-events-controller.js'
--- dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/overdue-events-controller.js 2014-09-10 07:13:29 +0000
+++ dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/overdue-events-controller.js 2014-09-11 12:35:41 +0000
@@ -2,9 +2,9 @@
function($scope,
$modal,
$location,
+ $translate,
orderByFilter,
DateUtils,
- EventUtils,
TEIService,
TEIGridService,
TranslationService,
@@ -17,7 +17,6 @@
$scope.today = DateUtils.format(moment());
- $scope.ouModes = [{name: 'SELECTED'}, {name: 'CHILDREN'}, {name: 'DESCENDANTS'}, {name: 'ACCESSIBLE'}];
$scope.selectedOuMode = 'SELECTED';
$scope.report = {};
$scope.displayMode = {};
@@ -79,7 +78,7 @@
if (angular.isObject($scope.selectedProgram)){
$scope.generateReport();
}
- });
+ });
$scope.generateReport = function(){
@@ -98,12 +97,11 @@
AttributesFactory.getByProgram($scope.selectedProgram).then(function(atts){
$scope.gridColumns = TEIGridService.generateGridColumns(atts, $scope.selectedOuMode);
- $scope.gridColumns.push({name: 'event_name', id: 'event_name', type: 'string', displayInListNoProgram: false, showFilter: false, show: true});
- $scope.filterTypes['event_name'] = 'string';
-
- $scope.gridColumns.push({name: 'due_date', id: 'due_date', type: 'date', displayInListNoProgram: false, showFilter: false, show: true});
- $scope.filterTypes['due_date'] = 'date';
- $scope.filterText['due_date']= {};
+ $scope.gridColumns.push({name: $translate('event_name'), id: 'eventName', type: 'string', displayInListNoProgram: false, showFilter: false, show: true});
+ $scope.filterTypes['eventName'] = 'string';
+ $scope.gridColumns.push({name: $translate('due_date'), id: 'dueDate', type: 'date', displayInListNoProgram: false, showFilter: false, show: true});
+ $scope.filterTypes['dueDate'] = 'date';
+ $scope.filterText['dueDate']= {};
});
//fetch TEIs for the selected program and orgunit/mode
@@ -116,61 +114,36 @@
false).then(function(data){
//process tei grid
- var teis = TEIGridService.format(data,true);
- $scope.teiList = [];
+ var teis = TEIGridService.format(data,true);
+ $scope.overdueEvents = [];
DHIS2EventFactory.getByOrgUnitAndProgram($scope.selectedOrgUnit.id, $scope.selectedOuMode, $scope.selectedProgram.id, null, null).then(function(eventList){
- $scope.dhis2Events = [];
+
angular.forEach(eventList, function(ev){
if(ev.dueDate){
ev.dueDate = DateUtils.format(ev.dueDate);
-
if( ev.trackedEntityInstance &&
!ev.eventDate &&
ev.dueDate < $scope.today){
-
- ev.name = $scope.programStages[ev.programStage].name;
- ev.programName = $scope.selectedProgram.name;
- ev.statusColor = EventUtils.getEventStatusColor(ev);
- ev.dueDate = DateUtils.format(ev.dueDate);
-
- if($scope.dhis2Events[ev.trackedEntityInstance]){
- if(teis.rows[ev.trackedEntityInstance]){
- $scope.teiList.push(teis.rows[ev.trackedEntityInstance]);
- delete teis.rows[ev.trackedEntityInstance];
- }
- $scope.dhis2Events[ev.trackedEntityInstance].push(ev);
- }
- else{
- if(teis.rows[ev.trackedEntityInstance]){
- $scope.teiList.push(teis.rows[ev.trackedEntityInstance]);
- delete teis.rows[ev.trackedEntityInstance];
- }
- $scope.dhis2Events[ev.trackedEntityInstance] = [ev];
- }
- ev = EventUtils.setEventOrgUnitName(ev);
- }
+
+ var overDue = {};
+ angular.copy(teis.rows[ev.trackedEntityInstance],overDue);
+ angular.extend(overDue,{eventName: $scope.programStages[ev.programStage].name, dueDate: ev.dueDate, followup: ev.followup});
+
+ $scope.overdueEvents.push(overDue);
+ }
}
});
-
- //incase a TEI happens to have more than one overdue, sort using duedate
- for(var tei in $scope.dhis2Events){
- $scope.dhis2Events[tei] = orderByFilter($scope.dhis2Events[tei], '-dueDate');
- $scope.dhis2Events[tei].reverse();
- }
-
- //make upcoming event name and its due date part of the grid column
- for(var i=0; i<$scope.teiList.length; i++){
- $scope.teiList[i].event_name = $scope.dhis2Events[$scope.teiList[i].id][0].name;
- $scope.teiList[i].due_date = $scope.dhis2Events[$scope.teiList[i].id][0].dueDate;
- $scope.teiList[i].followup = $scope.dhis2Events[$scope.teiList[i].id][0].followup;
- }
+
+ //sort overdue events by their due dates - this is default
+ $scope.overdueEvents = orderByFilter($scope.overdueEvents, '-dueDate');
+ $scope.overdueEvents.reverse();
$scope.reportFinished = true;
- $scope.reportStarted = false;
+ $scope.reportStarted = false;
});
- });
- }
- };
+ });
+ }
+ };
$scope.showHideColumns = function(){
@@ -201,13 +174,19 @@
});
};
- $scope.sortTEIGrid = function(gridHeader){
+ $scope.sortGrid = function(gridHeader){
if ($scope.sortHeader === gridHeader.id){
$scope.reverse = !$scope.reverse;
return;
}
$scope.sortHeader = gridHeader.id;
- $scope.reverse = false;
+ $scope.reverse = false;
+
+ $scope.overdueEvents = orderByFilter($scope.overdueEvents, $scope.sortHeader);
+
+ if($scope.reverse){
+ $scope.overdueEvents.reverse();
+ }
};
$scope.searchInGrid = function(gridColumn){
@@ -238,4 +217,12 @@
$location.path('/dashboard').search({tei: tei.id,
program: $scope.selectedProgram ? $scope.selectedProgram.id: null});
};
+
+ $scope.generateReportData = function(){
+ return TEIGridService.getData($scope.overdueEvents, $scope.gridColumns);
+ };
+
+ $scope.generateReportHeader = function(){
+ return TEIGridService.getHeader($scope.gridColumns);
+ };
});
\ No newline at end of file
=== modified file 'dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/overdue-events.html'
--- dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/overdue-events.html 2014-09-10 07:13:29 +0000
+++ dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/overdue-events.html 2014-09-11 12:35:41 +0000
@@ -21,13 +21,13 @@
<div class="row top-bar">
<div class="col-sm-12">
{{'overdue_events'| translate}}
- <div class="pull-right">
+ <div class="pull-right not-printable">
<div class="btn-group" dropdown is-open="status.isopen">
- <button type="button" class="btn btn-default dropdown-toggle" ng-disabled="!teiList.length">
+ <button type="button" class="btn btn-default dropdown-toggle" ng-disabled="!overdueEvents.length">
<i class="fa fa-cog" title="{{'settings'| translate}}"></i>
</button>
<ul class="dropdown-menu pull-right" role="menu">
- <li ng-show="teiList.length > 0"><a href ng-click="showHideColumns()">{{'show_hide_columns'| translate}}</a></li>
+ <li ng-show="overdueEvents.length > 0"><a href ng-click="showHideColumns()">{{'show_hide_columns'| translate}}</a></li>
</ul>
</div>
</div>
@@ -39,18 +39,18 @@
<form name="outerForm" novalidate>
<div class="row">
<div class="col-sm-8 col-md-6">
- <table class="table table-borderless table-striped">
- <tr ng-show='printMode'>
- <td class='col-sm-4 col-md-3 vertical-center'>{{'org_unit'| translate}}</td>
- <td class='col-sm-4 col-md-3'>
- <input type="text" selected-org-unit ng-model="selectedOrgUnit.name">
+ <table class="table-borderless table-striped">
+ <tr>
+ <td>{{'org_unit'| translate}}</td>
+ <td>
+ <input type="text" class="form-control" selected-org-unit ng-model="selectedOrgUnit.name" value="{{selectedOrgUnit.name || 'please_select'| translate}}" ng-disabled="true">
</td>
</tr>
<tr>
- <td class='col-sm-4 col-md-3'>
+ <td>
{{'program'| translate}}
</td>
- <td class='col-sm-4 col-md-3'>
+ <td>
<select ng-model="selectedProgram"
class="form-control"
ng-options="program as program.name for program in programs | orderBy: 'name'"
@@ -60,8 +60,8 @@
</td>
</tr>
<tr>
- <td class='col-sm-4 col-md-3 vertical-center'>{{'org_unit_scope'| translate}}</td>
- <td class='col-sm-4 col-md-3'>
+ <td>{{'org_unit_scope'| translate}}</td>
+ <td>
<label><input type="radio" ng-model="selectedOuMode" name="selected" value="SELECTED"> {{'SELECTED'| translate}}</label><br/>
<label><input type="radio" ng-model="selectedOuMode" name="children" value="CHILDREN"> {{'CHILDREN'| translate}}</label><br/>
<label><input type="radio" ng-model="selectedOuMode" name="descendants" value="DESCENDANTS"> {{'DESCENDANTS'| translate}}</label><br/>
@@ -69,21 +69,18 @@
</td>
</tr>
<tr>
- <label>
- <td class='col-sm-4 col-md-3 vertical-center'>
+ <td>
{{'filter'| translate}}
</td>
- <td class='col-sm-4 col-md-3'>
+ <td>
<label>
<input type="checkbox" ng-change="markForFollowup()" ng-model="displayMode.onlyMarkedFollowup"/> {{'only_marked_for_followup'| translate}}
</label>
</td>
- </label>
</tr>
</table>
</div>
- </div>
-
+ </div>
<div class="row" ng-if="programs.length < 1">
<div class="col-sm-8 col-md-6 vertical-spacing">
<div class="alert alert-warning">{{'no_program_exists_report'| translate}}</div>
@@ -101,8 +98,7 @@
<!-- upcoming events list begins -->
<div ng-if="reportFinished">
-
- <div ng-switch="teiList.length">
+ <div ng-switch="overdueEvents.length">
<div ng-switch-when="undefined">
<div class="alert alert-warning vertical-spacing">
{{'no_data_found'| translate}}
@@ -115,22 +111,42 @@
</div>
<div ng-switch-default>
<!-- report begins -->
- <div class="vertical-spacing">
+
+ <div class='pull-right vertical-spacing not-printable'>
+ <button type="button"
+ class="btn btn-primary"
+ onclick="javascript:window.print()">
+ {{'print'| translate}}
+ </button>
+ <!--<button type="button"
+ class="btn btn-success small-horizonal-spacing"
+ ng-click="exportToExcel()">
+ {{'excel_export'| translate}}
+ </button>-->
+ <button type="button"
+ class="btn btn-success small-horizonal-spacing"
+ ng-csv="generateReportData()"
+ csv-header="generateReportHeader()"
+ filename="overdueEvents.csv">
+ {{'excel_export'| translate}}
+ </button>
+ </div>
+ <div class="vertical-spacing">
<table class="listTable dhis2-table-striped-border dhis2-table-hover">
<thead>
<tr>
<th ng-show="gridColumn.show" ng-repeat="gridColumn in gridColumns">
<!-- sort icon begins -->
- <span ng-click="sortTEIGrid(gridColumn)">
- {{gridColumn.name| translate}}
- <i ng-if="sortHeader == gridColumn.id && reverse" class="fa fa-sort-desc"></i>
- <i ng-if="sortHeader == gridColumn.id && !reverse" class="fa fa-sort-asc"></i>
+ <span ng-click="sortGrid(gridColumn)">
+ {{gridColumn.name}}
+ <i ng-if="sortHeader == gridColumn.id && reverse" class="fa fa-sort-desc not-printable"></i>
+ <i ng-if="sortHeader == gridColumn.id && !reverse" class="fa fa-sort-asc not-printable"></i>
</span>
<!-- sort icon ends -->
<!-- filter icon begins -->
- <span class='pull-right'>
+ <span class='pull-right not-printable'>
<span ng-show="gridColumn.type != 'date' && gridColumn.type != 'int'">
<a href ng-click="searchInGrid(gridColumn)" title="{{'search'| translate}}"><span ng-class="{true: 'filter - without - content', false: 'filter - with - content'} [filterText[gridColumn.id] == undefined || filterText[gridColumn.id] == '']"><i class="fa fa-search"></i></span></a>
</span>
@@ -141,7 +157,7 @@
<!-- filter icon ends -->
<!-- filter input field begins -->
- <span ng-show="gridColumn.showFilter">
+ <span ng-show="gridColumn.showFilter" class="not-printable">
<span ng-switch="gridColumn.type">
<span ng-switch-when="int">
<input style="width: 45%;" placeholder="{{'lower_limit'| translate}}" type="number" ng-model="filterText[gridColumn.id].start" ng-blur="searchInGrid(gridColumn)">
@@ -168,17 +184,18 @@
</tr>
</thead>
<tbody id="list">
- <tr ng-repeat="tei in teiList| orderBy:sortHeader:reverse | gridFilter:filterText:filterTypes"
- ng-click="showDashboard(tei)"
+ <tr ng-repeat="overdueEvent in overdueEvents | orderBy:sortHeader:reverse | gridFilter:filterText:filterTypes"
+ ng-click="showDashboard(overdueEvent)"
title="{{'go_to_dashboard'| translate}}">
<td ng-show="gridColumn.show"
- ng-repeat="gridColumn in gridColumns" ng-if='displayMode.onlyMarkedFollowup ? tei.followup:true'>
- {{tei[gridColumn.id]}}
+ ng-repeat="gridColumn in gridColumns" ng-if='displayMode.onlyMarkedFollowup ? overdueEvent.followup:true'>
+ {{overdueEvent[gridColumn.id]}}
</td>
</tr>
</tbody>
- </table>
+ </table>
</div>
+
<!-- report ends -->
</div>
</div>
=== modified file 'dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/upcoming-events-controller.js'
--- dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/upcoming-events-controller.js 2014-09-09 13:26:15 +0000
+++ dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/upcoming-events-controller.js 2014-09-11 12:35:41 +0000
@@ -2,9 +2,9 @@
function($scope,
$modal,
$location,
+ $translate,
orderByFilter,
- DateUtils,
- EventUtils,
+ DateUtils,
TEIService,
TEIGridService,
TranslationService,
@@ -17,10 +17,10 @@
$scope.today = DateUtils.format(moment());
- $scope.ouModes = [{name: 'SELECTED'}, {name: 'CHILDREN'}, {name: 'DESCENDANTS'}, {name: 'ACCESSIBLE'}];
- $scope.selectedOuMode = $scope.ouModes[0];
+ $scope.selectedOuMode = 'SELECTED';
$scope.report = {};
$scope.displayMode = {};
+ $scope.printMode = false;
//watch for selection of org unit from tree
$scope.$watch('selectedOrgUnit', function() {
@@ -72,19 +72,18 @@
});
AttributesFactory.getByProgram($scope.selectedProgram).then(function(atts){
- $scope.gridColumns = TEIGridService.generateGridColumns(atts, $scope.selectedOuMode.name);
-
- $scope.gridColumns.push({name: 'event_name', id: 'event_name', type: 'string', displayInListNoProgram: false, showFilter: false, show: true});
- $scope.filterTypes['event_name'] = 'string';
-
- $scope.gridColumns.push({name: 'due_date', id: 'due_date', type: 'date', displayInListNoProgram: false, showFilter: false, show: true});
- $scope.filterTypes['due_date'] = 'date';
- $scope.filterText['due_date']= {};
+ $scope.gridColumns = TEIGridService.generateGridColumns(atts, $scope.selectedOuMode);
+
+ $scope.gridColumns.push({name: $translate('event_name'), id: 'eventName', type: 'string', displayInListNoProgram: false, showFilter: false, show: true});
+ $scope.filterTypes['eventName'] = 'string';
+ $scope.gridColumns.push({name: $translate('due_date'), id: 'dueDate', type: 'date', displayInListNoProgram: false, showFilter: false, show: true});
+ $scope.filterTypes['dueDate'] = 'date';
+ $scope.filterText['dueDate']= {};
});
//fetch TEIs for the selected program and orgunit/mode
TEIService.search($scope.selectedOrgUnit.id,
- $scope.selectedOuMode.name,
+ $scope.selectedOuMode,
null,
'program=' + $scope.selectedProgram.id,
null,
@@ -93,9 +92,8 @@
//process tei grid
var teis = TEIGridService.format(data,true);
- $scope.teiList = [];
- DHIS2EventFactory.getByOrgUnitAndProgram($scope.selectedOrgUnit.id, $scope.selectedOuMode.name, $scope.selectedProgram.id, null, null).then(function(eventList){
- $scope.dhis2Events = [];
+ $scope.upcomingEvents = [];
+ DHIS2EventFactory.getByOrgUnitAndProgram($scope.selectedOrgUnit.id, $scope.selectedOuMode, $scope.selectedProgram.id, null, null).then(function(eventList){
angular.forEach(eventList, function(ev){
if(ev.dueDate){
ev.dueDate = DateUtils.format(ev.dueDate);
@@ -105,45 +103,21 @@
ev.dueDate >= report.startDate &&
ev.dueDate <= report.endDate){
- ev.name = $scope.programStages[ev.programStage].name;
- ev.programName = $scope.selectedProgram.name;
- ev.statusColor = EventUtils.getEventStatusColor(ev);
- ev.dueDate = DateUtils.format(ev.dueDate);
+ var upcomingEvent = {};
+ angular.copy(teis.rows[ev.trackedEntityInstance],upcomingEvent);
+ angular.extend(upcomingEvent,{eventName: $scope.programStages[ev.programStage].name, dueDate: ev.dueDate, followup: ev.followup});
- if($scope.dhis2Events[ev.trackedEntityInstance]){
- if(teis.rows[ev.trackedEntityInstance]){
- $scope.teiList.push(teis.rows[ev.trackedEntityInstance]);
- delete teis.rows[ev.trackedEntityInstance];
- }
- $scope.dhis2Events[ev.trackedEntityInstance].push(ev);
- }
- else{
- if(teis.rows[ev.trackedEntityInstance]){
- $scope.teiList.push(teis.rows[ev.trackedEntityInstance]);
- delete teis.rows[ev.trackedEntityInstance];
- }
- $scope.dhis2Events[ev.trackedEntityInstance] = [ev];
- }
- ev = EventUtils.setEventOrgUnitName(ev);
+ $scope.upcomingEvents.push(upcomingEvent);
}
}
});
-
- //incase a TEI happens to have more than one overdue, sort using duedate
- for(var tei in $scope.dhis2Events){
- $scope.dhis2Events[tei] = orderByFilter($scope.dhis2Events[tei], '-dueDate');
- $scope.dhis2Events[tei].reverse();
- }
-
- //make upcoming event name and its due date part of the grid column
- for(var i=0; i<$scope.teiList.length; i++){
- $scope.teiList[i].event_name = $scope.dhis2Events[$scope.teiList[i].id][0].name;
- $scope.teiList[i].due_date = $scope.dhis2Events[$scope.teiList[i].id][0].dueDate;
- $scope.teiList[i].followup = $scope.dhis2Events[$scope.teiList[i].id][0].followup;
- }
-
+
+ //sort upcoming events by their due dates - this is default
+ $scope.upcomingEvents = orderByFilter($scope.upcomingEvents, '-dueDate');
+ $scope.upcomingEvents.reverse();
+
$scope.reportFinished = true;
- $scope.reportStarted = false;
+ $scope.reportStarted = false;
});
});
};
@@ -177,13 +151,19 @@
});
};
- $scope.sortTEIGrid = function(gridHeader){
+ $scope.sortGrid = function(gridHeader){
if ($scope.sortHeader === gridHeader.id){
$scope.reverse = !$scope.reverse;
return;
}
$scope.sortHeader = gridHeader.id;
- $scope.reverse = false;
+ $scope.reverse = false;
+
+ $scope.upcomingEvents = orderByFilter($scope.upcomingEvents, $scope.sortHeader);
+
+ if($scope.reverse){
+ $scope.upcomingEvents.reverse();
+ }
};
$scope.searchInGrid = function(gridColumn){
@@ -214,4 +194,12 @@
$location.path('/dashboard').search({tei: tei.id,
program: $scope.selectedProgram ? $scope.selectedProgram.id: null});
};
+
+ $scope.generateReportData = function(){
+ return TEIGridService.getData($scope.upcomingEvents, $scope.gridColumns);
+ };
+
+ $scope.generateReportHeader = function(){
+ return TEIGridService.getHeader($scope.gridColumns);
+ };
});
\ No newline at end of file
=== modified file 'dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/upcoming-events.html'
--- dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/upcoming-events.html 2014-09-09 12:54:28 +0000
+++ dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/components/report/upcoming-events.html 2014-09-11 12:35:41 +0000
@@ -13,9 +13,6 @@
</ul>
</div>
<img id="ouwt_loader" src="../images/ajax-loader-bar.gif"/>
- <!--- selected org unit begins -->
- <input type="text" selected-org-unit ng-model="selectedOrgUnit.name" ng-hide=true>
- <!--- selected org unit ends -->
</div>
<div id="mainPage" class="page">
@@ -26,11 +23,11 @@
{{'upcoming_events'| translate}}
<div class="pull-right">
<div class="btn-group" dropdown is-open="status.isopen">
- <button type="button" class="btn btn-default dropdown-toggle" ng-disabled="!teiList.length">
+ <button type="button" class="btn btn-default dropdown-toggle" ng-disabled="!upcomingEvents.length">
<i class="fa fa-cog" title="{{'settings'| translate}}"></i>
</button>
<ul class="dropdown-menu pull-right" role="menu">
- <li ng-show="teiList.length > 0"><a href ng-click="showHideColumns()">{{'show_hide_columns'| translate}}</a></li>
+ <li ng-show="upcomingEvents.length > 0"><a href ng-click="showHideColumns()">{{'show_hide_columns'| translate}}</a></li>
</ul>
</div>
</div>
@@ -42,12 +39,18 @@
<form name="outerForm" novalidate>
<div class="row">
<div class="col-sm-8 col-md-6">
- <table class="table table-borderless table-striped">
- <tr>
- <td class='col-sm-4 col-md-3 vertical-center'>
+ <table class="table-borderless table-striped">
+ <tr>
+ <td>{{'org_unit'| translate}}</td>
+ <td>
+ <input type="text" class="form-control" selected-org-unit ng-model="selectedOrgUnit.name" value="{{selectedOrgUnit.name || 'please_select'| translate}}" ng-disabled="true">
+ </td>
+ </tr>
+ <tr>
+ <td>
{{'program'| translate}}
</td>
- <td class='col-sm-4 col-md-3'>
+ <td>
<select ng-model="selectedProgram"
class="form-control"
ng-options="program as program.name for program in programs | orderBy: 'name'"
@@ -57,25 +60,23 @@
</td>
</tr>
<tr>
- <td class='col-sm-4 col-md-3 vertical-center'>{{'org_unit'| translate}}</td>
- <td class='col-sm-4 col-md-3'>
- <label><input type="radio" ng-model="selectedOuMode.name" name="selected" value="SELECTED"> {{'SELECTED'| translate}}</label><br/>
- <label><input type="radio" ng-model="selectedOuMode.name" name="children" value="CHILDREN"> {{'CHILDREN'| translate}}</label><br/>
- <label><input type="radio" ng-model="selectedOuMode.name" name="descendants" value="DESCENDANTS"> {{'DESCENDANTS'| translate}}</label><br/>
- <label><input type="radio" ng-model="selectedOuMode.name" name="accessible" value="ACCESSIBLE"> {{'ACCESSIBLE'| translate}}</label>
+ <td>{{'org_unit_scope'| translate}}</td>
+ <td>
+ <label><input type="radio" ng-model="selectedOuMode" name="selected" value="SELECTED"> {{'SELECTED'| translate}}</label><br/>
+ <label><input type="radio" ng-model="selectedOuMode" name="children" value="CHILDREN"> {{'CHILDREN'| translate}}</label><br/>
+ <label><input type="radio" ng-model="selectedOuMode" name="descendants" value="DESCENDANTS"> {{'DESCENDANTS'| translate}}</label><br/>
+ <label><input type="radio" ng-model="selectedOuMode" name="accessible" value="ACCESSIBLE"> {{'ACCESSIBLE'| translate}}</label>
</td>
</tr>
<tr>
- <label>
- <td class='col-sm-4 col-md-3 vertical-center'>
- {{'filter'| translate}}
- </td>
- <td>
- <label>
- <input type="checkbox" ng-change="markForFollowup()" ng-model="displayMode.onlyMarkedFollowup"/> {{'only_marked_for_followup' | translate}}
- </label>
- </td>
- </label>
+ <td>
+ {{'filter'| translate}}
+ </td>
+ <td>
+ <label>
+ <input type="checkbox" ng-change="markForFollowup()" ng-model="displayMode.onlyMarkedFollowup"/> {{'only_marked_for_followup'| translate}}
+ </label>
+ </td>
</tr>
</table>
</div>
@@ -103,7 +104,22 @@
</table>
</div>
<div class="col-md-6 trim">
- <button type="button" class="btn btn-primary" ng-click="generateReport(selectedProgram, report, selectedOuMode)" ng-disabled="!selectedProgram">{{'go'| translate}}</button>
+ <button type="button" class="btn btn-primary" ng-click="generateReport(selectedProgram, report, selectedOuMode)" ng-disabled="!selectedProgram">{{'go'| translate}}</button>
+ <button type="button"
+ class="btn btn-success small-horizonal-spacing"
+ ng-if="upcomingEvents.length > 0"
+ class="btn btn-primary"
+ onclick="javascript:window.print()">
+ {{'print'| translate}}
+ </button>
+ <button type="button"
+ class="btn btn-info small-horizonal-spacing"
+ ng-if="upcomingEvents.length > 0"
+ ng-csv="generateReportData()"
+ csv-header="generateReportHeader()"
+ filename="upcomingEvents.csv">
+ {{'excel_export'| translate}}
+ </button>
</div>
</div>
@@ -124,8 +140,7 @@
<!-- upcoming events list begins -->
<div ng-if="reportFinished">
-
- <div ng-switch="teiList.length">
+ <div ng-switch="upcomingEvents.length">
<div ng-switch-when="undefined">
<div class="alert alert-warning vertical-spacing">
{{'no_data_found'| translate}}
@@ -145,7 +160,7 @@
<th ng-show="gridColumn.show" ng-repeat="gridColumn in gridColumns">
<!-- sort icon begins -->
- <span ng-click="sortTEIGrid(gridColumn)">
+ <span ng-click="sortGrid(gridColumn)">
{{gridColumn.name| translate}}
<i ng-if="sortHeader == gridColumn.id && reverse" class="fa fa-sort-desc"></i>
<i ng-if="sortHeader == gridColumn.id && !reverse" class="fa fa-sort-asc"></i>
@@ -191,12 +206,12 @@
</tr>
</thead>
<tbody id="list">
- <tr ng-repeat="tei in teiList| orderBy:sortHeader:reverse | gridFilter:filterText:filterTypes"
- ng-click="showDashboard(tei)"
+ <tr ng-repeat="upcomingEvent in upcomingEvents| orderBy:sortHeader:reverse | gridFilter:filterText:filterTypes"
+ ng-click="showDashboard(upcomingEvent)"
title="{{'go_to_dashboard'| translate}}">
<td ng-show="gridColumn.show"
- ng-repeat="gridColumn in gridColumns" ng-if='displayMode.onlyMarkedFollowup ? tei.followup:true'>
- {{tei[gridColumn.id]}}
+ ng-repeat="gridColumn in gridColumns" ng-if='displayMode.onlyMarkedFollowup ? upcomingEvent.followup:true'>
+ {{upcomingEvent[gridColumn.id]}}
</td>
</tr>
</tbody>
=== modified file 'dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/i18n/en.json'
--- dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/i18n/en.json 2014-09-10 07:13:29 +0000
+++ dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/i18n/en.json 2014-09-11 12:35:41 +0000
@@ -163,6 +163,7 @@
"close": "Close",
"generate": "Generate",
"print": "Print",
+ "excel_export": "Excel export",
"list_programs": "List programs",
"program_stage": "Program stage",
"due_date": "Due date",
=== modified file 'dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/index.html'
--- dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/index.html 2014-09-08 09:08:39 +0000
+++ dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/index.html 2014-09-11 12:35:41 +0000
@@ -23,7 +23,8 @@
<script type="text/javascript" src="../dhis-web-commons/javascripts/angular/angular-resource.js"></script>
<script type="text/javascript" src="../dhis-web-commons/javascripts/angular/angular-route.js"></script>
<script type="text/javascript" src="../dhis-web-commons/javascripts/angular/angular-cookies.js"></script>
- <script type="text/javascript" src="../dhis-web-commons/javascripts/angular/angular-animate.js"></script>
+ <script type="text/javascript" src="../dhis-web-commons/javascripts/angular/angular-animate.js"></script>
+ <script type="text/javascript" src="../dhis-web-commons/javascripts/angular/angular-sanitize.js"></script>
<script type="text/javascript" src="../dhis-web-commons/javascripts/angular/ui-bootstrap-tpls-0.10.0-draggable-modal.js"></script>
@@ -58,6 +59,7 @@
<script type="text/javascript" src="../dhis-web-commons/javascripts/angular/plugins/angular-translate-loader-static-files.min.js"></script>
<script type="text/javascript" src="../dhis-web-commons/javascripts/angular/plugins/angular-translate-loader-url.min.js"></script>
<script type="text/javascript" src="../dhis-web-commons/javascripts/angular/plugins/select2.js"></script>
+ <script type="text/javascript" src="scripts/ng-csv.js"></script>
<script type="text/javascript" src="scripts/app.js"></script>
<script type="text/javascript" src="scripts/services.js"></script>
=== modified file 'dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/app.js'
--- dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/app.js 2014-08-27 12:55:29 +0000
+++ dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/app.js 2014-09-11 12:35:41 +0000
@@ -5,7 +5,8 @@
var trackerCapture = angular.module('trackerCapture',
['ui.bootstrap',
'ngRoute',
- 'ngCookies',
+ 'ngCookies',
+ 'ngSanitize',
'trackerCaptureServices',
'trackerCaptureFilters',
'trackerCaptureDirectives',
@@ -13,6 +14,7 @@
'angularLocalStorage',
'ui.select2',
'd2Menu',
+ 'ngCsv',
'pascalprecht.translate'])
.value('DHIS2URL', '..')
=== modified file 'dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/directives.js'
--- dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/directives.js 2014-09-09 12:06:50 +0000
+++ dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/directives.js 2014-09-11 12:35:41 +0000
@@ -17,8 +17,7 @@
};
})
-.directive('selectedOrgUnit', function() {
-
+.directive('selectedOrgUnit', function() {
return {
restrict: 'A',
link: function(scope, element, attrs){
=== added file 'dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/ng-csv.js'
--- dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/ng-csv.js 1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/ng-csv.js 2014-09-11 12:35:41 +0000
@@ -0,0 +1,236 @@
+(function(window, document) {
+
+// Create all modules and define dependencies to make sure they exist
+// and are loaded in the correct order to satisfy dependency injection
+// before all nested files are concatenated by Grunt
+
+// Config
+angular.module('ngCsv.config', []).
+ value('ngCsv.config', {
+ debug: true
+ }).
+ config(['$compileProvider', function($compileProvider){
+ if (angular.isDefined($compileProvider.urlSanitizationWhitelist)) {
+ $compileProvider.urlSanitizationWhitelist(/^\s*(https?|ftp|mailto|file|data):/);
+ } else {
+ $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|file|data):/);
+ }
+ }]);
+
+// Modules
+angular.module('ngCsv.directives', ['ngCsv.services']);
+angular.module('ngCsv.services', []);
+angular.module('ngCsv',
+ [
+ 'ngCsv.config',
+ 'ngCsv.services',
+ 'ngCsv.directives',
+ 'ngSanitize'
+ ]);
+/**
+ * Created by asafdav on 15/05/14.
+ */
+angular.module('ngCsv.services').
+ service('CSV', ['$q', function($q) {
+
+ var EOL = encodeURIComponent('\r\n');
+ var DATA_URI_PREFIX = "data:text/csv;charset=utf-8,";
+
+ /**
+ * Stringify one field
+ * @param data
+ * @param delimier
+ * @returns {*}
+ */
+ this.stringifyField = function(data, delimier, quoteText) {
+ if (typeof data === 'string') {
+ data = data.replace(/"/g, '""'); // Escape double qoutes
+ if (quoteText || data.indexOf(',') > -1 || data.indexOf('\n') > -1 || data.indexOf('\r') > -1) data = delimier + data + delimier;
+ return encodeURIComponent(data);
+ }
+
+ if (typeof data === 'boolean') {
+ return data ? 'TRUE' : 'FALSE';
+ }
+
+ return data;
+ };
+
+ /**
+ * Creates a csv from a data array
+ * @param data
+ * @param options
+ * * header - Provide the first row (optional)
+ * * fieldSep - Field separator, default: ','
+ * @param callback
+ */
+ this.stringify = function (data, options)
+ {
+ var def = $q.defer();
+
+ var that = this;
+ var csv;
+ var csvContent = "";
+
+ var dataPromise = $q.when(data).then(function (responseData)
+ {
+ responseData = angular.copy(responseData);
+ // Check if there's a provided header array
+ if (angular.isDefined(options.header) && options.header)
+ {
+ var encodingArray, headerString;
+
+ encodingArray = [];
+ angular.forEach(options.header, function(title, key)
+ {
+ this.push(that.stringifyField(title, options.txtDelim, options.quoteStrings));
+ }, encodingArray);
+
+ headerString = encodingArray.join(options.fieldSep ? options.fieldSep : ",");
+ csvContent += headerString + EOL;
+ }
+
+ var arrData;
+
+ if (angular.isArray(responseData)) {
+ arrData = responseData;
+ }
+ else {
+ arrData = responseData();
+ }
+
+ angular.forEach(arrData, function(row, index)
+ {
+ var dataString, infoArray;
+
+ infoArray = [];
+
+ angular.forEach(row, function(field, key)
+ {
+ this.push(that.stringifyField(field, options.txtDelim, options.quoteStrings));
+ }, infoArray);
+
+ dataString = infoArray.join(options.fieldSep ? options.fieldSep : ",");
+ csvContent += index < arrData.length ? dataString + EOL : dataString;
+ });
+
+ if(window.navigator.msSaveOrOpenBlob) {
+ csv = csvContent;
+ }else{
+ csv = DATA_URI_PREFIX + csvContent;
+ }
+ def.resolve(csv);
+ });
+
+ if (typeof dataPromise.catch === 'function') {
+ dataPromise.catch(function(err) {
+ def.reject(err);
+ });
+ }
+
+ return def.promise;
+ };
+ }]);/**
+ * ng-csv module
+ * Export Javascript's arrays to csv files from the browser
+ *
+ * Author: asafdav - https://github.com/asafdav
+ */
+angular.module('ngCsv.directives').
+ directive('ngCsv', ['$parse', '$q', 'CSV', '$document', '$timeout', function ($parse, $q, CSV, $document, $timeout) {
+ return {
+ restrict: 'AC',
+ scope: {
+ data:'&ngCsv',
+ filename:'@filename',
+ header: '&csvHeader',
+ txtDelim: '@textDelimiter',
+ quoteStrings: '@quoteStrings',
+ fieldSep: '@fieldSeparator',
+ lazyLoad: '@lazyLoad',
+ ngClick: '&'
+ },
+ controller: [
+ '$scope',
+ '$element',
+ '$attrs',
+ '$transclude',
+ function ($scope, $element, $attrs, $transclude) {
+ $scope.csv = '';
+
+ if (!angular.isDefined($scope.lazyLoad) || $scope.lazyLoad != "true")
+ {
+ if (angular.isArray($scope.data))
+ {
+ $scope.$watch("data", function (newValue) {
+ $scope.buildCSV();
+ }, true);
+ }
+ }
+
+ $scope.getFilename = function ()
+ {
+ return $scope.filename || 'download.csv';
+ };
+
+ function getBuildCsvOptions() {
+ var options = {
+ txtDelim: $scope.txtDelim ? $scope.txtDelim : '"',
+ quoteStrings: $scope.quoteStrings
+ };
+ if (angular.isDefined($attrs.csvHeader)) options.header = $scope.$eval($scope.header);
+ options.fieldSep = $scope.fieldSep ? $scope.fieldSep : ",";
+
+ return options;
+ }
+
+ /**
+ * Creates the CSV and updates the scope
+ * @returns {*}
+ */
+ $scope.buildCSV = function() {
+ var deferred = $q.defer();
+
+ CSV.stringify($scope.data(), getBuildCsvOptions()).then(function(csv) {
+ $scope.csv = csv;
+ deferred.resolve(csv);
+ });
+ $scope.$apply(); // Old angular support
+
+ return deferred.promise;
+ };
+ }
+ ],
+ link: function (scope, element, attrs) {
+ function doClick() {
+ if(window.navigator.msSaveOrOpenBlob) {
+ var blob = new Blob([scope.csv],{
+ type: "text/csv;charset=utf-8;"
+ });
+ navigator.msSaveBlob(blob, scope.getFilename());
+ } else {
+
+ var downloadLink = angular.element('<a></a>');
+ downloadLink.attr('href',scope.csv);
+ downloadLink.attr('download',scope.getFilename());
+
+ $document.find('body').append(downloadLink);
+ $timeout(function() {
+ downloadLink[0].click();
+ downloadLink.remove();
+ }, null);
+ }
+
+ }
+
+ element.bind('click', function (e)
+ {
+ scope.buildCSV().then(function(csv) {
+ doClick();
+ });
+ scope.$apply();
+ });
+ }
+ };
+ }]);
+})(window, document);
\ No newline at end of file
=== modified file 'dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/services.js'
--- dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/services.js 2014-09-09 12:06:50 +0000
+++ dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/scripts/services.js 2014-09-11 12:35:41 +0000
@@ -1070,7 +1070,7 @@
})
-.service('TEIGridService', function(OrgUnitService, DateUtils){
+.service('TEIGridService', function(OrgUnitService, DateUtils, $translate){
return {
format: function(grid, map){
@@ -1130,8 +1130,8 @@
var columns = attributes ? angular.copy(attributes) : [];
//also add extra columns which are not part of attributes (orgunit for example)
- columns.push({id: 'orgUnitName', name: 'registering_unit', type: 'string', displayInListNoProgram: false});
- columns.push({id: 'created', name: 'registration_date', type: 'date', displayInListNoProgram: false});
+ columns.push({id: 'orgUnitName', name: $translate('registering_unit'), type: 'string', displayInListNoProgram: false});
+ columns.push({id: 'created', name: $translate('registration_date'), type: 'date', displayInListNoProgram: false});
//generate grid column for the selected program/attributes
angular.forEach(columns, function(column){
@@ -1146,6 +1146,28 @@
});
return columns;
+ },
+ getData: function(rows, columns){
+ var data = [];
+ angular.forEach(rows, function(row){
+ var d = {};
+ angular.forEach(columns, function(col){
+ if(col.show){
+ d[col.name] = row[col.id];
+ }
+ });
+ data.push(d);
+ });
+ return data;
+ },
+ getHeader: function(columns){
+ var header = [];
+ angular.forEach(columns, function(col){
+ if(col.show){
+ header.push($translate(col.name));
+ }
+ });
+ return header;
}
};
})
@@ -1230,7 +1252,7 @@
OrgUnitService.open().then(function(){
OrgUnitService.get(dhis2Event.orgUnit).then(function(ou){
if(ou){
- dhis2Event.orgUnitName = ou.n;
+ dhis2Event.eventOrgUnitName = ou.n;
return dhis2Event;
}
});
=== modified file 'dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/styles/style.css'
--- dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/styles/style.css 2014-09-09 12:06:50 +0000
+++ dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/styles/style.css 2014-09-11 12:35:41 +0000
@@ -793,5 +793,4 @@
#header, #leftBar, .not-printable {
display: none;
}
-
}
\ No newline at end of file
=== modified file 'dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/views/column-modal.html'
--- dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/views/column-modal.html 2014-09-09 12:06:50 +0000
+++ dhis-2/dhis-web/dhis-web-apps/src/main/webapp/dhis-web-tracker-capture/views/column-modal.html 2014-09-11 12:35:41 +0000
@@ -5,7 +5,7 @@
<table class="listTable dhis2-table-striped-border">
<tr ng-repeat="gridColumn in gridColumns">
<td>
- {{gridColumn.name | translate}}
+ {{gridColumn.name}}
</td>
<td>
<input type="checkbox" ng-model="gridColumn.show" ng-change="showHideColumns(gridColumn)" ng-disabled="hiddenGridColumns + 1 == gridColumns.length && gridColumn.show">
=== added file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/angular/angular-sanitize.js'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/angular/angular-sanitize.js 1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/angular/angular-sanitize.js 2014-09-11 12:35:41 +0000
@@ -0,0 +1,624 @@
+/**
+ * @license AngularJS v1.2.14
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {'use strict';
+
+var $sanitizeMinErr = angular.$$minErr('$sanitize');
+
+/**
+ * @ngdoc module
+ * @name ngSanitize
+ * @description
+ *
+ * # ngSanitize
+ *
+ * The `ngSanitize` module provides functionality to sanitize HTML.
+ *
+ *
+ * <div doc-module-components="ngSanitize"></div>
+ *
+ * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
+ */
+
+/*
+ * HTML Parser By Misko Hevery (misko@xxxxxxxxxx)
+ * based on: HTML Parser By John Resig (ejohn.org)
+ * Original code by Erik Arvidsson, Mozilla Public License
+ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
+ *
+ * // Use like so:
+ * htmlParser(htmlString, {
+ * start: function(tag, attrs, unary) {},
+ * end: function(tag) {},
+ * chars: function(text) {},
+ * comment: function(text) {}
+ * });
+ *
+ */
+
+
+/**
+ * @ngdoc service
+ * @name $sanitize
+ * @function
+ *
+ * @description
+ * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
+ * then serialized back to properly escaped html string. This means that no unsafe input can make
+ * it into the returned string, however, since our parser is more strict than a typical browser
+ * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
+ * browser, won't make it through the sanitizer.
+ * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
+ * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
+ *
+ * @param {string} html Html input.
+ * @returns {string} Sanitized html.
+ *
+ * @example
+ <example module="ngSanitize" deps="angular-sanitize.js">
+ <file name="index.html">
+ <script>
+ function Ctrl($scope, $sce) {
+ $scope.snippet =
+ '<p style="color:blue">an html\n' +
+ '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
+ 'snippet</p>';
+ $scope.deliberatelyTrustDangerousSnippet = function() {
+ return $sce.trustAsHtml($scope.snippet);
+ };
+ }
+ </script>
+ <div ng-controller="Ctrl">
+ Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
+ <table>
+ <tr>
+ <td>Directive</td>
+ <td>How</td>
+ <td>Source</td>
+ <td>Rendered</td>
+ </tr>
+ <tr id="bind-html-with-sanitize">
+ <td>ng-bind-html</td>
+ <td>Automatically uses $sanitize</td>
+ <td><pre><div ng-bind-html="snippet"><br/></div></pre></td>
+ <td><div ng-bind-html="snippet"></div></td>
+ </tr>
+ <tr id="bind-html-with-trust">
+ <td>ng-bind-html</td>
+ <td>Bypass $sanitize by explicitly trusting the dangerous value</td>
+ <td>
+ <pre><div ng-bind-html="deliberatelyTrustDangerousSnippet()">
+</div></pre>
+ </td>
+ <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
+ </tr>
+ <tr id="bind-default">
+ <td>ng-bind</td>
+ <td>Automatically escapes</td>
+ <td><pre><div ng-bind="snippet"><br/></div></pre></td>
+ <td><div ng-bind="snippet"></div></td>
+ </tr>
+ </table>
+ </div>
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should sanitize the html snippet by default', function() {
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
+ toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
+ });
+
+ it('should inline raw snippet if bound to a trusted value', function() {
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
+ toBe("<p style=\"color:blue\">an html\n" +
+ "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
+ "snippet</p>");
+ });
+
+ it('should escape snippet without any filter', function() {
+ expect(element(by.css('#bind-default div')).getInnerHtml()).
+ toBe("<p style=\"color:blue\">an html\n" +
+ "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
+ "snippet</p>");
+ });
+
+ it('should update', function() {
+ element(by.model('snippet')).clear();
+ element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
+ toBe('new <b>text</b>');
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
+ 'new <b onclick="alert(1)">text</b>');
+ expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
+ "new <b onclick=\"alert(1)\">text</b>");
+ });
+ </file>
+ </example>
+ */
+function $SanitizeProvider() {
+ this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
+ return function(html) {
+ var buf = [];
+ htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
+ return !/^unsafe/.test($$sanitizeUri(uri, isImage));
+ }));
+ return buf.join('');
+ };
+ }];
+}
+
+function sanitizeText(chars) {
+ var buf = [];
+ var writer = htmlSanitizeWriter(buf, angular.noop);
+ writer.chars(chars);
+ return buf.join('');
+}
+
+
+// Regular Expressions for parsing tags and attributes
+var START_TAG_REGEXP =
+ /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,
+ END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/,
+ ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
+ BEGIN_TAG_REGEXP = /^</,
+ BEGING_END_TAGE_REGEXP = /^<\s*\//,
+ COMMENT_REGEXP = /<!--(.*?)-->/g,
+ DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
+ CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
+ // Match everything outside of normal chars and " (quote character)
+ NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
+
+
+// Good source of info about elements and attributes
+// http://dev.w3.org/html5/spec/Overview.html#semantics
+// http://simon.html5.org/html-elements
+
+// Safe Void Elements - HTML5
+// http://dev.w3.org/html5/spec/Overview.html#void-elements
+var voidElements = makeMap("area,br,col,hr,img,wbr");
+
+// Elements that you can, intentionally, leave open (and which close themselves)
+// http://dev.w3.org/html5/spec/Overview.html#optional-tags
+var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
+ optionalEndTagInlineElements = makeMap("rp,rt"),
+ optionalEndTagElements = angular.extend({},
+ optionalEndTagInlineElements,
+ optionalEndTagBlockElements);
+
+// Safe Block Elements - HTML5
+var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
+ "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
+ "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
+
+// Inline Elements - HTML5
+var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
+ "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
+ "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
+
+
+// Special Elements (can contain anything)
+var specialElements = makeMap("script,style");
+
+var validElements = angular.extend({},
+ voidElements,
+ blockElements,
+ inlineElements,
+ optionalEndTagElements);
+
+//Attributes that have href and hence need to be sanitized
+var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap");
+var validAttrs = angular.extend({}, uriAttrs, makeMap(
+ 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
+ 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
+ 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
+ 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+
+ 'valign,value,vspace,width'));
+
+function makeMap(str) {
+ var obj = {}, items = str.split(','), i;
+ for (i = 0; i < items.length; i++) obj[items[i]] = true;
+ return obj;
+}
+
+
+/**
+ * @example
+ * htmlParser(htmlString, {
+ * start: function(tag, attrs, unary) {},
+ * end: function(tag) {},
+ * chars: function(text) {},
+ * comment: function(text) {}
+ * });
+ *
+ * @param {string} html string
+ * @param {object} handler
+ */
+function htmlParser( html, handler ) {
+ var index, chars, match, stack = [], last = html;
+ stack.last = function() { return stack[ stack.length - 1 ]; };
+
+ while ( html ) {
+ chars = true;
+
+ // Make sure we're not in a script or style element
+ if ( !stack.last() || !specialElements[ stack.last() ] ) {
+
+ // Comment
+ if ( html.indexOf("<!--") === 0 ) {
+ // comments containing -- are not allowed unless they terminate the comment
+ index = html.indexOf("--", 4);
+
+ if ( index >= 0 && html.lastIndexOf("-->", index) === index) {
+ if (handler.comment) handler.comment( html.substring( 4, index ) );
+ html = html.substring( index + 3 );
+ chars = false;
+ }
+ // DOCTYPE
+ } else if ( DOCTYPE_REGEXP.test(html) ) {
+ match = html.match( DOCTYPE_REGEXP );
+
+ if ( match ) {
+ html = html.replace( match[0] , '');
+ chars = false;
+ }
+ // end tag
+ } else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
+ match = html.match( END_TAG_REGEXP );
+
+ if ( match ) {
+ html = html.substring( match[0].length );
+ match[0].replace( END_TAG_REGEXP, parseEndTag );
+ chars = false;
+ }
+
+ // start tag
+ } else if ( BEGIN_TAG_REGEXP.test(html) ) {
+ match = html.match( START_TAG_REGEXP );
+
+ if ( match ) {
+ html = html.substring( match[0].length );
+ match[0].replace( START_TAG_REGEXP, parseStartTag );
+ chars = false;
+ }
+ }
+
+ if ( chars ) {
+ index = html.indexOf("<");
+
+ var text = index < 0 ? html : html.substring( 0, index );
+ html = index < 0 ? "" : html.substring( index );
+
+ if (handler.chars) handler.chars( decodeEntities(text) );
+ }
+
+ } else {
+ html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
+ function(all, text){
+ text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
+
+ if (handler.chars) handler.chars( decodeEntities(text) );
+
+ return "";
+ });
+
+ parseEndTag( "", stack.last() );
+ }
+
+ if ( html == last ) {
+ throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
+ "of html: {0}", html);
+ }
+ last = html;
+ }
+
+ // Clean up any remaining tags
+ parseEndTag();
+
+ function parseStartTag( tag, tagName, rest, unary ) {
+ tagName = angular.lowercase(tagName);
+ if ( blockElements[ tagName ] ) {
+ while ( stack.last() && inlineElements[ stack.last() ] ) {
+ parseEndTag( "", stack.last() );
+ }
+ }
+
+ if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) {
+ parseEndTag( "", tagName );
+ }
+
+ unary = voidElements[ tagName ] || !!unary;
+
+ if ( !unary )
+ stack.push( tagName );
+
+ var attrs = {};
+
+ rest.replace(ATTR_REGEXP,
+ function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
+ var value = doubleQuotedValue
+ || singleQuotedValue
+ || unquotedValue
+ || '';
+
+ attrs[name] = decodeEntities(value);
+ });
+ if (handler.start) handler.start( tagName, attrs, unary );
+ }
+
+ function parseEndTag( tag, tagName ) {
+ var pos = 0, i;
+ tagName = angular.lowercase(tagName);
+ if ( tagName )
+ // Find the closest opened tag of the same type
+ for ( pos = stack.length - 1; pos >= 0; pos-- )
+ if ( stack[ pos ] == tagName )
+ break;
+
+ if ( pos >= 0 ) {
+ // Close all the open elements, up the stack
+ for ( i = stack.length - 1; i >= pos; i-- )
+ if (handler.end) handler.end( stack[ i ] );
+
+ // Remove the open elements from the stack
+ stack.length = pos;
+ }
+ }
+}
+
+var hiddenPre=document.createElement("pre");
+var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
+/**
+ * decodes all entities into regular string
+ * @param value
+ * @returns {string} A string with decoded entities.
+ */
+function decodeEntities(value) {
+ if (!value) { return ''; }
+
+ // Note: IE8 does not preserve spaces at the start/end of innerHTML
+ // so we must capture them and reattach them afterward
+ var parts = spaceRe.exec(value);
+ var spaceBefore = parts[1];
+ var spaceAfter = parts[3];
+ var content = parts[2];
+ if (content) {
+ hiddenPre.innerHTML=content.replace(/</g,"<");
+ // innerText depends on styling as it doesn't display hidden elements.
+ // Therefore, it's better to use textContent not to cause unnecessary
+ // reflows. However, IE<9 don't support textContent so the innerText
+ // fallback is necessary.
+ content = 'textContent' in hiddenPre ?
+ hiddenPre.textContent : hiddenPre.innerText;
+ }
+ return spaceBefore + content + spaceAfter;
+}
+
+/**
+ * Escapes all potentially dangerous characters, so that the
+ * resulting string can be safely inserted into attribute or
+ * element text.
+ * @param value
+ * @returns {string} escaped text
+ */
+function encodeEntities(value) {
+ return value.
+ replace(/&/g, '&').
+ replace(NON_ALPHANUMERIC_REGEXP, function(value){
+ return '&#' + value.charCodeAt(0) + ';';
+ }).
+ replace(/</g, '<').
+ replace(/>/g, '>');
+}
+
+/**
+ * create an HTML/XML writer which writes to buffer
+ * @param {Array} buf use buf.jain('') to get out sanitized html string
+ * @returns {object} in the form of {
+ * start: function(tag, attrs, unary) {},
+ * end: function(tag) {},
+ * chars: function(text) {},
+ * comment: function(text) {}
+ * }
+ */
+function htmlSanitizeWriter(buf, uriValidator){
+ var ignore = false;
+ var out = angular.bind(buf, buf.push);
+ return {
+ start: function(tag, attrs, unary){
+ tag = angular.lowercase(tag);
+ if (!ignore && specialElements[tag]) {
+ ignore = tag;
+ }
+ if (!ignore && validElements[tag] === true) {
+ out('<');
+ out(tag);
+ angular.forEach(attrs, function(value, key){
+ var lkey=angular.lowercase(key);
+ var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
+ if (validAttrs[lkey] === true &&
+ (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
+ out(' ');
+ out(key);
+ out('="');
+ out(encodeEntities(value));
+ out('"');
+ }
+ });
+ out(unary ? '/>' : '>');
+ }
+ },
+ end: function(tag){
+ tag = angular.lowercase(tag);
+ if (!ignore && validElements[tag] === true) {
+ out('</');
+ out(tag);
+ out('>');
+ }
+ if (tag == ignore) {
+ ignore = false;
+ }
+ },
+ chars: function(chars){
+ if (!ignore) {
+ out(encodeEntities(chars));
+ }
+ }
+ };
+}
+
+
+// define ngSanitize module and register $sanitize service
+angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
+
+/* global sanitizeText: false */
+
+/**
+ * @ngdoc filter
+ * @name linky
+ * @function
+ *
+ * @description
+ * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
+ * plain email address links.
+ *
+ * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
+ *
+ * @param {string} text Input text.
+ * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
+ * @returns {string} Html-linkified text.
+ *
+ * @usage
+ <span ng-bind-html="linky_expression | linky"></span>
+ *
+ * @example
+ <example module="ngSanitize" deps="angular-sanitize.js">
+ <file name="index.html">
+ <script>
+ function Ctrl($scope) {
+ $scope.snippet =
+ 'Pretty text with some links:\n'+
+ 'http://angularjs.org/,\n'+
+ 'mailto:us@xxxxxxxxxxxxx,\n'+
+ 'another@xxxxxxxxxxxxx,\n'+
+ 'and one more: ftp://127.0.0.1/.';
+ $scope.snippetWithTarget = 'http://angularjs.org/';
+ }
+ </script>
+ <div ng-controller="Ctrl">
+ Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
+ <table>
+ <tr>
+ <td>Filter</td>
+ <td>Source</td>
+ <td>Rendered</td>
+ </tr>
+ <tr id="linky-filter">
+ <td>linky filter</td>
+ <td>
+ <pre><div ng-bind-html="snippet | linky"><br></div></pre>
+ </td>
+ <td>
+ <div ng-bind-html="snippet | linky"></div>
+ </td>
+ </tr>
+ <tr id="linky-target">
+ <td>linky target</td>
+ <td>
+ <pre><div ng-bind-html="snippetWithTarget | linky:'_blank'"><br></div></pre>
+ </td>
+ <td>
+ <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
+ </td>
+ </tr>
+ <tr id="escaped-html">
+ <td>no filter</td>
+ <td><pre><div ng-bind="snippet"><br></div></pre></td>
+ <td><div ng-bind="snippet"></div></td>
+ </tr>
+ </table>
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should linkify the snippet with urls', function() {
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
+ toBe('Pretty text with some links: http://angularjs.org/, us@xxxxxxxxxxxxx, ' +
+ 'another@xxxxxxxxxxxxx, and one more: ftp://127.0.0.1/.');
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
+ });
+
+ it('should not linkify snippet without the linky filter', function() {
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
+ toBe('Pretty text with some links: http://angularjs.org/, mailto:us@xxxxxxxxxxxxx, ' +
+ 'another@xxxxxxxxxxxxx, and one more: ftp://127.0.0.1/.');
+ expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
+ });
+
+ it('should update', function() {
+ element(by.model('snippet')).clear();
+ element(by.model('snippet')).sendKeys('new http://link.');
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
+ toBe('new http://link.');
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
+ .toBe('new http://link.');
+ });
+
+ it('should work with the target property', function() {
+ expect(element(by.id('linky-target')).
+ element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
+ toBe('http://angularjs.org/');
+ expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
+ });
+ </file>
+ </example>
+ */
+angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
+ var LINKY_URL_REGEXP =
+ /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,
+ MAILTO_REGEXP = /^mailto:/;
+
+ return function(text, target) {
+ if (!text) return text;
+ var match;
+ var raw = text;
+ var html = [];
+ var url;
+ var i;
+ while ((match = raw.match(LINKY_URL_REGEXP))) {
+ // We can not end in these as they are sometimes found at the end of the sentence
+ url = match[0];
+ // if we did not match ftp/http/mailto then assume mailto
+ if (match[2] == match[3]) url = 'mailto:' + url;
+ i = match.index;
+ addText(raw.substr(0, i));
+ addLink(url, match[0].replace(MAILTO_REGEXP, ''));
+ raw = raw.substring(i + match[0].length);
+ }
+ addText(raw);
+ return $sanitize(html.join(''));
+
+ function addText(text) {
+ if (!text) {
+ return;
+ }
+ html.push(sanitizeText(text));
+ }
+
+ function addLink(url, text) {
+ html.push('<a ');
+ if (angular.isDefined(target)) {
+ html.push('target="');
+ html.push(target);
+ html.push('" ');
+ }
+ html.push('href="');
+ html.push(url);
+ html.push('">');
+ addText(text);
+ html.push('</a>');
+ }
+ };
+}]);
+
+
+})(window, window.angular);