Merge pull request #2721 from FinnStutzenstein/AgendaTable

Agenda table
This commit is contained in:
Emanuel Schütze 2016-12-02 15:59:57 +01:00 committed by GitHub
commit 486f0601eb
14 changed files with 584 additions and 324 deletions

View File

@ -178,6 +178,7 @@ OpenSlides uses the following projects or parts of them:
* `angular-bootstrap <http://angular-ui.github.io/bootstrap>`_, License: MIT
* `angular-bootstrap-colorpicker <https://github.com/buberdds/angular-bootstrap-colorpicker>`_, License: MIT
* `angular-chosen-localytics <http://github.com/leocaseiro/angular-chosen>`_, License: MIT
* `angular-cookies <https://github.com/angular/bower-angular-cookies>`_, License: MIT
* `angular-csv-import <https://github.com/bahaaldine/angular-csv-import>`_, License: MIT
* `angular-formly <http://formly-js.github.io/angular-formly/>`_, License: MIT
* `angular-formly-templates-bootstrap <https://github.com/formly-js/angular-formly-templates-bootstrap>`_, License: MIT
@ -191,6 +192,7 @@ OpenSlides uses the following projects or parts of them:
* `angular-ui-router <http://angular-ui.github.io/ui-router/>`_, License: MIT
* `angular-ui-tinymce <http://angular-ui.github.com>`_, License: MIT
* `angular-ui-tree <https://github.com/angular-ui-tree/angular-ui-tree>`_, License: MIT
* `angular-xeditable <https://github.com/vitalets/angular-xeditable>`_, License: MIT
* `api-check <https://github.com/kentcdodds/api-check>`_, License: MIT
* `bootstrap <http://getbootstrap.com>`_, License: MIT
* `bootstrap-ui-datetime-picker <https://github.com/Gillardo/bootstrap-ui-datetime-picker>`_, License: MIT

View File

@ -9,6 +9,7 @@
"angular-bootstrap-colorpicker": "~3.0.25",
"angular-chosen-localytics": "~1.5.0",
"angular-csv-import": "0.0.36",
"angular-cookies": "~1.5.9",
"angular-file-saver": "~1.1.2",
"angular-formly": "~8.4.0",
"angular-formly-templates-bootstrap": "~6.2.0",
@ -21,6 +22,7 @@
"angular-ui-router": "~0.3.1",
"angular-ui-tinymce": "~0.0.17",
"angular-ui-tree": "~2.22.0",
"angular-xeditable": "~0.5.0",
"bootstrap-css-only": "~3.3.6",
"bootstrap-ui-datetime-picker": "~2.4.0",
"docxtemplater": "~2.1.5",

View File

@ -107,8 +107,12 @@ angular.module('OpenSlidesApp.agenda.site', [
'AgendaContentProvider',
'PdfMakeDocumentProvider',
'gettextCatalog',
'gettext',
'osTableFilter',
'AgendaCsvExport',
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm, AgendaTree, Projector,
ProjectionDefault, AgendaContentProvider, PdfMakeDocumentProvider, gettextCatalog) {
ProjectionDefault, AgendaContentProvider, PdfMakeDocumentProvider, gettextCatalog, gettext, osTableFilter,
AgendaCsvExport) {
// Bind agenda tree to the scope
$scope.$watch(function () {
return Agenda.lastModified();
@ -132,6 +136,86 @@ angular.module('OpenSlidesApp.agenda.site', [
});
$scope.alert = {};
// Filtering
$scope.filter = osTableFilter.createInstance('AgendaTableFilter');
if (!$scope.filter.existsCookie()) {
$scope.filter.booleanFilters = {
closed: {
value: undefined,
displayName: gettext('Closed items'),
choiceYes: gettext('Closed items'),
choiceNo: gettext('Open items'),
},
is_hidden: {
value: undefined,
displayName: gettext('Internal items'),
choiceYes: gettext('Internal items'),
choiceNo: gettext('No internal items'),
},
};
$scope.filter.save();
}
$scope.filter.propertyList = ['item_number', 'title', 'title_list_view', 'comment', 'duration'];
$scope.filter.propertyFunctionList = [
function (item) {return item.getListViewTitle();},
];
$scope.filter.propertyDict = {
'speakers' : function (speaker) {
return '';
},
};
// pagination
$scope.currentPage = 1;
$scope.itemsPerPage = 100;
$scope.limitBegin = 0;
$scope.pageChanged = function() {
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
};
// parse duration for inline editing
$scope.generateDurationText = function (item) {
//convert data from model format (m) to view format (hh:mm)
if (item.duration) {
var time = "",
totalminutes = item.duration;
if (totalminutes < 0) {
time = "-";
totalminutes = -totalminutes;
}
var hh = Math.floor(totalminutes / 60);
var mm = Math.floor(totalminutes % 60);
// Add leading "0" for double digit values
mm = ("0"+mm).slice(-2);
time += hh + ":" + mm;
item.durationText = time;
} else {
item.durationText = "";
}
};
$scope.setDurationText = function (item) {
//convert data from view format (hh:mm) to model format (m)
var time = item.durationText.replace('h', '').split(':');
var data;
if (time.length > 1 && !isNaN(time[0]) && !isNaN(time[1])) {
data = (+time[0]) * 60 + (+time[1]);
if (data < 0) {
data = "-"+data;
}
item.duration = parseInt(data);
} else if (time.length == 1 && !isNaN(time[0])) {
data = (+time[0]);
item.duration = parseInt(data);
} else {
item.duration = 0;
}
$scope.save(item);
};
/** Duration calculations **/
$scope.sumDurations = function () {
var totalDuration = 0;
$scope.items.forEach(function (item) {
@ -141,7 +225,6 @@ angular.module('OpenSlidesApp.agenda.site', [
});
return totalDuration;
};
$scope.calculateEndTime = function () {
var totalDuration = $scope.sumDurations();
var startTimestamp = $scope.config('agenda_start_event_date_time');
@ -156,22 +239,33 @@ angular.module('OpenSlidesApp.agenda.site', [
}
};
$scope.getUpdateStatePrefix = function (item) {
var prefix = item.content_object.collection.replace('/','.');
// Hotfix for Issue 2566.
// The changes could be reverted if Issue 2480 is closed.
prefix = prefix.replace('motion-block', 'motionBlock');
return prefix;
/** Agenda item functions **/
// open dialog for new topics // TODO Remove this. Don't forget import button in template.
$scope.newDialog = function () {
ngDialog.open(TopicForm.getDialog());
};
// pagination
$scope.currentPage = 1;
$scope.itemsPerPage = 100;
$scope.limitBegin = 0;
$scope.pageChanged = function() {
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
// save changed item
$scope.save = function (item) {
Agenda.save(item).then(
function(success) {
$scope.alert.show = false;
},
function(error){
var message = '';
for (var e in error.data) {
message += e + ': ' + error.data[e] + ' ';
}
$scope.alert = { type: 'danger', msg: message, show: true };
});
};
// delete related item
$scope.deleteRelatedItem = function (item) {
DS.destroy(item.content_object.collection, item.content_object.id);
};
// auto numbering of agenda items
$scope.autoNumbering = function() {
$http.post('/rest/agenda/item/numbering/', {});
};
// check open permission
// TODO: Use generic solution here.
$scope.isAllowedToSeeOpenLink = function (item) {
@ -189,47 +283,37 @@ angular.module('OpenSlidesApp.agenda.site', [
return false;
}
};
// open dialog for new topics // TODO Remove this. Don't forget import button in template.
$scope.newDialog = function () {
ngDialog.open(TopicForm.getDialog());
$scope.getUpdateStatePrefix = function (item) {
var prefix = item.content_object.collection.replace('/','.');
// Hotfix for Issue 2566.
// The changes could be reverted if Issue 2480 is closed.
prefix = prefix.replace('motion-block', 'motionBlock');
return prefix;
};
// cancel QuickEdit mode
$scope.cancelQuickEdit = function (item) {
// revert all changes by restore (refresh) original item object from server
Agenda.refresh(item);
item.quickEdit = false;
// export
$scope.pdfExport = function () {
var filename = gettextCatalog.getString('Agenda') + '.pdf';
var agendaContentProvider = AgendaContentProvider.createInstance($scope.itemsFiltered);
var documentProvider = PdfMakeDocumentProvider.createInstance(agendaContentProvider);
pdfMake.createPdf(documentProvider.getDocument()).download(filename);
};
// save changed item
$scope.save = function (item) {
Agenda.save(item).then(
function(success) {
item.quickEdit = false;
$scope.alert.show = false;
},
function(error){
var message = '';
for (var e in error.data) {
message += e + ': ' + error.data[e] + ' ';
}
$scope.alert = { type: 'danger', msg: message, show: true };
});
};
// delete related item
$scope.deleteRelatedItem = function (item) {
DS.destroy(item.content_object.collection, item.content_object.id);
$scope.csvExport = function () {
var element = document.getElementById('downloadLinkCSV');
AgendaCsvExport(element, $scope.itemsFiltered);
};
// *** delete mode functions ***
$scope.isDeleteMode = false;
/** select mode functions **/
$scope.isSelectMode = false;
// check all checkboxes
$scope.checkAll = function () {
$scope.selectedAll = !$scope.selectedAll;
angular.forEach($scope.items, function (item) {
item.selected = $scope.selectedAll;
});
};
// uncheck all checkboxes if isDeleteMode is closed
$scope.uncheckAll = function () {
if (!$scope.isDeleteMode) {
if (!$scope.isSelectMode) {
$scope.selectedAll = false;
angular.forEach($scope.items, function (item) {
item.selected = false;
@ -243,7 +327,7 @@ angular.module('OpenSlidesApp.agenda.site', [
DS.destroy(item.content_object.collection, item.content_object.id);
}
});
$scope.isDeleteMode = false;
$scope.isSelectMode = false;
$scope.uncheckAll();
};
@ -319,17 +403,6 @@ angular.module('OpenSlidesApp.agenda.site', [
});
return projectorIds;
};
// auto numbering of agenda items
$scope.autoNumbering = function() {
$http.post('/rest/agenda/item/numbering/', {});
};
$scope.makePDF = function() {
var filename = gettextCatalog.getString('Agenda') + '.pdf';
var agendaContentProvider = AgendaContentProvider.createInstance($scope.items);
var documentProvider = PdfMakeDocumentProvider.createInstance(agendaContentProvider);
pdfMake.createPdf(documentProvider.getDocument()).download(filename);
};
}
])
@ -613,6 +686,30 @@ angular.module('OpenSlidesApp.agenda.site', [
}
])
.factory('AgendaCsvExport', [
function () {
return function (element, agenda) {
var csvRows = [
['title', 'text', 'duration', 'comment', 'is_hidden'],
];
_.forEach(agenda, function (item) {
var row = [];
row.push('"' + (item.title || '') + '"');
row.push('"' + (item.text || '') + '"');
row.push('"' + (item.duration || '') + '"');
row.push('"' + (item.comment || '') + '"');
row.push('"' + (item.is_hidden ? '1' : '') + '"');
csvRows.push(row);
});
var csvString = csvRows.join("%0A");
element.href = 'data:text/csv;charset=utf-8,' + csvString;
element.download = 'agenda-export.csv';
element.target = '_blank';
};
}
])
//mark all agenda config strings for translation with Javascript
.config([
'gettext',

View File

@ -61,66 +61,61 @@
<div class="details">
<div class="row">
<div class="col-sm-7">
<div class="form-inline">
<!-- delete mode -->
<button os-perms="agenda.can_manage" class="btn btn-sm"
ng-class="$parent.isDeleteMode ? 'btn-primary' : 'btn-default'"
ng-click="$parent.isDeleteMode = !$parent.isDeleteMode; uncheckAll()">
<i class="fa fa-check-square-o"></i>
<translate>Select ...</translate>
</button>
<!-- sort button -->
<a ui-sref="agenda.item.sort" os-perms="agenda.can_manage" class="btn btn-default btn-sm">
<i class="fa fa-sitemap fa-lg"></i>
<translate>Sort ...</translate>
</a>
<!-- auto numbering button -->
<a os-perms="core.can_manage_projector"
class="btn btn-default btn-sm"
ng-click="autoNumbering()">
<i class="fa fa-sort-numeric-asc"></i>
<translate>Numbering</translate>
</a>
<!-- pdf -->
<a ng-click="makePDF()" class="btn btn-default btn-sm">
<i class="fa fa-file-pdf-o fa-lg"></i>
<translate>PDF</translate>
</a>
</div>
</div>
<div class="col-sm-5">
<div class="form-inline text-right">
<div class="form-group">
<div class="input-group">
<div class="input-group-addon"><i class="fa fa-search"></i></div>
<input type="text" ng-model="filter.search" class="form-control"
placeholder="{{ 'Search' | translate}}">
</div>
</div>
<button class="btn btn-default" ng-click="isFilterOpen = !isFilterOpen"
ng-class="isFilterOpen ? 'btn-primary' : 'btn-default'">
<i class="fa fa-filter"></i>
<translate>Filter ...</translate>
<div class="col-sm-12">
<!-- select mode -->
<button os-perms="agenda.can_manage" class="btn btn-sm"
ng-class="$parent.isSelectMode ? 'btn-primary' : 'btn-default'"
ng-click="$parent.isSelectMode = !$parent.isSelectMode; uncheckAll()">
<i class="fa fa-check-square-o"></i>
<translate>Select ...</translate>
</button>
<!-- sort button -->
<a ui-sref="agenda.item.sort" os-perms="agenda.can_manage" class="btn btn-default btn-sm">
<i class="fa fa-sitemap fa-lg"></i>
<translate>Sort ...</translate>
</a>
<!-- auto numbering button -->
<button os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
ng-click="autoNumbering()">
<i class="fa fa-sort-numeric-asc"></i>
<translate>Numbering</translate>
</button>
<!-- pdf -->
<div class="pull-right" uib-dropdown>
<button type="button" class="btn btn-default" id="dropdownExport" uib-dropdown-toggle>
<i class="fa fa-upload"></i>
<span ng-if="itemsFiltered.length == items.length" translate>
Export all
</span>
<span ng-if="itemsFiltered.length != items.length" translate>
Export filtered
</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownExport">
<!-- PDF export -->
<li>
<a href="" ng-click="pdfExport()">
<i class="fa fa-file-pdf-o fa-lg"></i>
PDF
</a>
</li>
<!-- CSV export -->
<li>
<a href="" id="downloadLinkCSV"
ng-click="csvExport()">
<i class="fa fa-file-text-o fa-lg"></i>
CSV
</a>
</li>
</ul>
</div>
</div>
</div>
<div uib-collapse="!isFilterOpen" class="row spacer">
<div class="col-sm-12 text-right">
<!-- hidden item filter -->
<input type="checkbox" ng-model="filter.showHiddenItems" ng-true-value="" ng-false-value="false">
<translate> Show internal items</translate>
<!-- closed filter -->
<input type="checkbox" ng-model="filter.showClosedItems" ng-true-value="" ng-false-value="false">
<translate> Show closed items</translate>
</div>
</div>
<div uib-collapse="!isDeleteMode" class="row spacer">
<div uib-collapse="!isSelectMode" class="row spacer">
<div class="col-sm-12 text-left">
<!-- delete button -->
<a ng-show="isDeleteMode" os-perms="agenda.can_manage"
<a ng-show="isSelectMode" os-perms="agenda.can_manage"
ng-bootbox-confirm="{{ 'Are you sure you want to delete all selected agenda items?' | translate }}"
ng-bootbox-confirm-action="deleteMultiple()"
class="btn btn-primary">
@ -134,148 +129,214 @@
<span os-perms="agenda.can_see_hidden_items">{{ itemsFiltered.length }} /</span>
{{ items.length }} {{ "items" | translate }}<span ng-if="(items|filter:{selected:true}).length > 0">,
{{(items|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
<span os-perms="agenda.can_see_hidden_items" class="optional">
<span ng-if="sumDurations() > 0">&middot;
<translate>Duration</translate>:
{{ sumDurations() | osMinutesToTime }}h
<span ng-if="config('agenda_start_event_date_time')">
(<translate>Estimated end:</translate> {{ calculateEndTime() }})
</span>
</span>
</div>
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<!-- projector column -->
<th ng-show="!isDeleteMode" os-perms="core.can_manage_projector" class="minimum"></th>
<!-- delete selection column -->
<th ng-show="isDeleteMode" os-perms="agenda.can_manage" class="minimum deleteColumn"
ng-click="$event.stopPropagation();">
<input type="checkbox" ng-model="$parent.selectedAll" ng-change="checkAll()">
<!-- agenda item column -->
<th>
<translate>Agenda item</translate>
<th os-perms="agenda.can_see_hidden_items" class="optional">
<translate>Duration</translate>
<span ng-if="sumDurations() > 0">
{{ sumDurations() | osMinutesToTime }}h
<span ng-if="config('agenda_start_event_date_time')">
(<translate>Estimated end:</translate> {{ calculateEndTime() }})
</span>
<div class="os-table container-fluid">
<div class="row header-row">
<div class="col-xs-1 centered" ng-show="isSelectMode">
<i class="fa text-danger pointer" ng-class="selectedAll ? 'fa-check-square-o' : 'fa-square-o'"
ng-click="checkAll()"></i>
</div>
<div class="col-xs-11 main-header">
<span class="form-inline text-right pull-right">
<!-- clear all filters -->
<span class="sort-spacer pointer" ng-click="filter.reset()"
ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>Filter</translate>
</span>
<th class="minimum optional">
<translate>Done</translate>
<tbody>
<tr ng-repeat="item in itemsFiltered = (items | filter: filter.search |
filter: {is_hidden: filter.showHiddenItems} | filter: {closed: filter.showClosedItems}) |
limitTo : itemsPerPage : limitBegin"
class="animate-item"
ng-class="{ 'activeline': item.isProjected().length, 'selected': item.selected, 'hiddenrow': item.is_hidden}">
<!-- projector column -->
<td ng-show="!isDeleteMode" os-perms="core.can_manage_projector">
<div class="btn-group" style="min-width:{{ (item.hasSubitems(items) || projectors.length > 1) ? '54' : '34' }}px;" uib-dropdown
uib-tooltip="{{ 'Projector' | translate }} {{ item.isProjected(item.tree)[0] || '' }}"
tooltip-enable="item.isProjected(item.tree).length">
<button class="btn btn-default btn-sm"
title="{{ 'Project item' | translate }}"
ng-click="item.project(getProjectionDefault(item), item.tree)"
ng-class="{ 'btn-primary': item.isProjected(item.tree).length && inArray(item.isProjected(item.tree), getProjectionDefault(item))}">
<i class="fa fa-video-camera"></i>
</button>
<button type="button" class="btn btn-default btn-sm slimDropDown"
ng-class="{ 'btn-primary': item.isProjected(item.tree).length && !inArray(item.isProjected(item.tree), getProjectionDefault(item))}"
ng-if="item.hasSubitems(items) || projectors.length > 1"
uib-dropdown-toggle>
<!-- boolean Filters -->
<span ng-repeat="(name, booleanFilter) in filter.booleanFilters" uib-dropdown>
<span class="pointer" id="dropdown{{ name }}" uib-dropdown-toggle
ng-class="{'bold': booleanFilter.value !== undefined, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
{{ booleanFilter.displayName }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button"
ng-if="item.hasSubitems(items) || projectors.length > 1">
<li role="menuitem" ng-show="item.hasSubitems(items)">
<a href="" ng-click="changeItemTree(item); $event.stopPropagation();">
<i class="fa" ng-class="item.tree ? 'fa-check-square-o' : 'fa-square-o'"></i>
<translate>Include all sub items</translate>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown{{ name }}">
<li>
<a href ng-click="booleanFilter.value = (booleanFilter.value ? undefined : true); filter.save();">
<i class="fa" ng-class="{'fa-check': booleanFilter.value === true}"></i>
{{ booleanFilter.choiceYes }}
</a>
</li>
<li class="divider" ng-show="item.hasSubitems(items)"></li>
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="item.project(projector.id, item.tree)"
ng-class="{ 'projected': inArray(item.isProjected(item.tree), projector.id) }">
<i class="fa fa-video-camera" ng-show="inArray(item.isProjected(item.tree), projector.id)"></i>
{{ projector.name | translate }}
<span ng-if="projector.id == getProjectionDefault(item)">(<translate>Default</translate>)</span>
<li>
<a href ng-click="booleanFilter.value = (booleanFilter.value === false) ? undefined : false; filter.save();">
<i class="fa" ng-class="{'fa-check': booleanFilter.value === false}"></i>
{{ booleanFilter.choiceNo }}
</a>
</li>
</ul>
</div>
<!-- delete selection column -->
<td ng-show="isDeleteMode" os-perms="agenda.can_manage" class="deleteColumn">
<input type="checkbox" ng-model="item.selected">
<!-- agenda data columns -->
<td ng-if="!item.quickEdit" ng-mouseover="item.hover=true" ng-mouseleave="item.hover=false"
style="padding-left: calc(8px + {{ item.parentCount }}*15px)">
<strong>
<a ui-sref="{{ getUpdateStatePrefix(item) }}.detail({id: item.content_object.id})" ng-show="isAllowedToSeeOpenLink(item)">
{{ item.getListViewTitle() }}
</a>
<span ng-hide="isAllowedToSeeOpenLink(item)">
{{ item.getListViewTitle() }}
</span>
</strong>
<span ng-if="item.is_hidden" title="{{ 'Internal item' | translate }}"><i class="fa fa-ban"></i></span>
<div ng-if="item.comment">
<small><i class="fa fa-info-circle"></i> {{ item.comment }}</small>
</div>
<div os-perms="agenda.can_see" class="hoverActions" ng-class="{'hiddenDiv': !item.hover}">
<a ui-sref="agenda.item.detail({id: item.id})" translate>List of speakers</a>
<span os-perms="agenda.can_manage"> |
<a ui-sref="{{ getUpdateStatePrefix(item) }}.detail.update({id: item.content_object.id})"
translate>Edit</a> |
<a href="" ng-click="item.quickEdit=true" translate>QuickEdit</a> |
<a href="" class="text-danger"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
<b>{{ item.getTitle() }}</b>"
ng-bootbox-confirm-action="deleteRelatedItem(item)" translate>Delete</a>
</span>
</div>
<td ng-show="!item.quickEdit" os-perms="agenda.can_see_hidden_items" class="optional">
{{ item.duration | osMinutesToTime }}
<span ng-if="item.duration" translate-comment="'h' means time in hours" translate>h</span>
<td ng-if="!item.quickEdit" class="optional">
<span os-perms="!agenda.can_manage">
<i ng-if="item.closed" class="fa fa-check-square-o"></i>
</span>
<input os-perms="agenda.can_manage" type="checkbox" ng-model="item.closed" ng-change="save(item);">
<!-- quickEdit columns -->
<td ng-show="item.quickEdit" os-perms="agenda.can_manage" colspan="3">
<h4>{{ item.getTitle() }} <span class="text-muted">&ndash; QuickEdit</span></h4>
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}">
{{ alert.msg }}
<!-- search field -->
<span class="form-group">
<span class="input-group">
<span class="input-group-addon"><i class="fa fa-search"></i></span>
<input type="text" ng-model="filter.filterString" class="form-control"
placeholder="{{ 'Search' | translate}}" ng-disabled="isSelectMode"
ng-change="filter.save()">
</span>
</span>
</span>
<!-- show all selected multiselectoptions -->
<span>
<!-- for all boolean Filters -->
<span ng-repeat="(name, booleanFilter) in filter.booleanFilters"
ng-hide="booleanFilter.value === undefined"
class="pointer spacer-left-lg"
ng-click="booleanFilter.value = undefined; filter.save();"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ booleanFilter.value ? booleanFilter.choiceYes : booleanFilter.choiceNo | translate }}
</span>
</span>
</span>
</div>
</div>
<!-- main table -->
<div class="row data-row" ng-mouseover="item.hover=true"
ng-mouseleave="item.hover=false"
ng-class="{'projected': item.isProjected().length}"
ng-repeat="item in itemsFiltered = (items
| osFilter: filter.filterString : filter.getObjectQueryString
| filter: {closed: filter.booleanFilters.closed.value}
| filter: {is_hidden: filter.booleanFilters.is_hidden.value}
| limitTo : itemsPerPage : limitBegin)">
<!-- select column -->
<div ng-show="isSelectMode" os-perms="agenda.can_manage" class="col-xs-1 centered">
<i class="fa text-danger pointer" ng-click="item.selected=!item.selected"
ng-class="item.selected ? 'fa-check-square-o' : 'fa-square-o'"></i>
</div>
<!-- projector column -->
<div class="col-xs-1 centered projector" os-perms="core.can_manage_projector">
<div class="btn-group" style="min-width:{{ (item.hasSubitems(items) || projectors.length > 1) ? '54' : '34' }}px;" uib-dropdown
uib-tooltip="{{ 'Projector' | translate }} {{ item.isProjected(item.tree)[0] || '' }}"
tooltip-enable="item.isProjected(item.tree).length">
<button class="btn btn-default btn-sm"
title="{{ 'Project item' | translate }}"
ng-click="item.project(getProjectionDefault(item), item.tree)"
ng-class="{ 'btn-primary': item.isProjected(item.tree).length && inArray(item.isProjected(item.tree), getProjectionDefault(item))}">
<i class="fa fa-video-camera"></i>
</button>
<button type="button" class="btn btn-default btn-sm slimDropDown"
ng-class="{ 'btn-primary': item.isProjected(item.tree).length && !inArray(item.isProjected(item.tree), getProjectionDefault(item))}"
ng-if="item.hasSubitems(items) || projectors.length > 1"
uib-dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button"
ng-if="item.hasSubitems(items) || projectors.length > 1">
<li role="menuitem" ng-show="item.hasSubitems(items)">
<a href="" ng-click="changeItemTree(item); $event.stopPropagation();">
<i class="fa" ng-class="item.tree ? 'fa-check-square-o' : 'fa-square-o'"></i>
<translate>Include all sub items</translate>
</a>
</li>
<li class="divider" ng-show="item.hasSubitems(items)"></li>
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="item.project(projector.id, item.tree)"
ng-class="{ 'projected': inArray(item.isProjected(item.tree), projector.id) }">
<i class="fa fa-video-camera" ng-show="inArray(item.isProjected(item.tree), projector.id)"></i>
{{ projector.name | translate }}
<span ng-if="projector.id == getProjectionDefault(item)">(<translate>Default</translate>)</span>
</a>
</li>
</ul>
</div>
</div>
<!-- main content column -->
<div class="col-xs-6 content" style="padding-left: calc({{ item.parentCount }}*15px)">
<div class="spacer-right">
<strong>
<i class="fa fa-ban fa-lg" ng-style="{'visibility': item.is_hidden ? 'visible' : 'hidden'}"
title="{{ 'Internal item' | translate }}"></i>
</strong>
</div>
<div>
<!-- ID and title -->
<div>
<strong>
<a class="title" ui-sref="{{ getUpdateStatePrefix(item) }}.detail({id: item.content_object.id})" ng-show="isAllowedToSeeOpenLink(item)">
{{ item.getListViewTitle() }}
</a>
<span class="title" ng-hide="isAllowedToSeeOpenLink(item)">
{{ item.getListViewTitle() }}
</span>
</strong>
</div>
<div class="row">
<div class="col-xs-6">
<label for="inputItemNumber" translate>Item number</label>
<input type="text" ng-model="item.item_number" class="form-control input-sm" id="inputItemNumber">
<!-- hover menu -->
<div os-perms="agenda.can_see" ng-class="{'hiddenDiv': !item.hover}">
<small>
<a ui-sref="agenda.item.detail({id: item.id})" translate>List of speakers</a>
<span os-perms="agenda.can_manage"> &middot;
<a ui-sref="{{ getUpdateStatePrefix(item) }}.detail.update({id: item.content_object.id})"
translate>Edit</a> &middot;
<a href="" class="text-danger"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
<b>{{ item.getTitle() }}</b>"
ng-bootbox-confirm-action="deleteRelatedItem(item)" translate>Delete</a>
</span>
</small>
</div>
</div>
</div>
<!-- additional content column -->
<div class="col-xs-4 content" ng-style="{'width': isSelectMode ? 'calc(50% - 120px)' : 'calc(50% - 70px)'}">
<div style="width: 60%;" class="optional">
<small>
<div ng-style="{'visibility': (item.duration || item.hover) ? 'visible' : 'hidden'}">
<div class="popover-wrapper">
<i class="fa fa-clock-o"></i>
<span editable-text="item.durationText" e-placeholder="hh:mm"
onshow="generateDurationText(item)" onaftersave="setDurationText(item)">
{{ (item.duration | osMinutesToTime) || ('Set duration...' | translate) }}
<span ng-if="item.duration" translate-comment="'h' means time in hours" translate>h</span>
</span>
</div>
</div>
<div class="col-xs-6">
<label for="inputComment" translate>Comment</label>
<input type="text" ng-model="item.comment" class="form-control input-sm" id="inputComment">
<div ng-style="{'visibility': (item.comment || item.hover) ? 'visible' : 'hidden'}">
<div class="popover-wrapper">
<i class="fa fa-info-circle"></i>
<span editable-text="item.comment" onaftersave="save(item)">{{ item.comment || ('Set comment...' | translate)}}</span>
</div>
</div>
</small>
</div>
<div style="width: 40%;" class="pull-right">
<div os-perms="agenda.can_manage">
<div class="pointer nobr" ng-click="item.type = (item.type == 1) ? 2 : 1; save(item);" ng-show="item.hover || item.is_hidden">
<i class="fa" ng-class="item.is_hidden ? 'fa-check-square-o' : 'fa-square-o'"></i>
<span class="spacer-left" translate>Internal item</span>
</div>
<div class="pointer nobr" ng-click="item.closed = !item.closed; save(item);" ng-show="item.hover || item.closed">
<i class="fa" ng-class="item.closed ? 'fa-check-square-o' : 'fa-square-o'"></i>
<span class="spacer-left" translate>Done</span>
</div>
</div>
<div class="row">
<div class="col-xs-6">
<!-- item type: AGENDA_ITEM = 1, HIDDEN_ITEM = 2 -->
<input type="checkbox" ng-model="item.type" ng-true-value="1" ng-false-value="2">
<translate>Show as agenda item</translate>
</div>
<div class="col-xs-6">
<label for="inputDuration" translate>Duration</label>
<input type="text" ng-model="item.duration" placeholder="hh:mm" hour-min-format
class="form-control input-sm" id="inputDuration">
</div>
<div os-perms="!agenda.can_manage" ng-if="item.closed">
<i class="fa fa-check-square-o"></i>
<span class="spacer-left" translate>Done</span>
</div>
<div class="spacer">
<button ng-click="cancelQuickEdit(item)" class="btn btn-default pull-left" translate>
Cancel
</button> &nbsp;
<button ng-click="save(item)" class="btn btn-primary" translate>
Update
</button>
<a ui-sref="{{ item.content_object.collection.replace('/','.') }}.detail.update({id: item.content_object.id})"
class="pull-right"><translate>Edit ...</translate></a>
</div>
</table>
</div>
</div>
</div> <!-- data row -->
</div> <!-- container -->
<ul uib-pagination
ng-show="itemsFiltered.length > itemsPerPage"
total-items="itemsFiltered.length"
@ -288,4 +349,4 @@
first-text="&laquo;"
last-text="&raquo;">
</ul>
</div>
</div> <!-- details -->

View File

@ -303,9 +303,10 @@ angular.module('OpenSlidesApp.assignments.site', [
'User',
'osTableFilter',
'osTableSort',
'gettext',
function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, phases, Projector, ProjectionDefault,
gettextCatalog, AssignmentContentProvider, AssignmentCatalogContentProvider, PdfMakeDocumentProvider,
User, osTableFilter, osTableSort) {
User, osTableFilter, osTableSort, gettext) {
Assignment.bindAll({}, $scope, 'assignments');
Tag.bindAll({}, $scope, 'tags');
$scope.$watch(function () {
@ -320,11 +321,14 @@ angular.module('OpenSlidesApp.assignments.site', [
$scope.alert = {};
// Filtering
$scope.filter = osTableFilter.createInstance();
$scope.filter.multiselectFilters = {
tag: [],
phase: [],
};
$scope.filter = osTableFilter.createInstance('AssignmentTableFilter');
if (!$scope.filter.existsCookie()) {
$scope.filter.multiselectFilters = {
tag: [],
phase: [],
};
}
$scope.filter.propertyList = ['title', 'description'];
$scope.filter.propertyFunctionList = [
function (assignment) {

View File

@ -162,7 +162,8 @@
<span class="input-group">
<span class="input-group-addon"><i class="fa fa-search"></i></span>
<input type="text" ng-model="filter.filterString" class="form-control"
placeholder="{{ 'Search' | translate}}">
placeholder="{{ 'Search' | translate}}" ng-disable="isSelectMode"
ng-change="filter.save()">
</span>
</span>
@ -224,7 +225,9 @@
<!-- title and phase -->
<div>
<strong>
<a ui-sref="assignments.assignment.detail({id: assignment.id})">{{ assignment.title }}</a>
<a ui-sref="assignments.assignment.detail({id: assignment.id})">
{{ assignment.title }}
</a>
</strong>
<span style="padding: 5px;" ng-mouseover="assignment.phaseHover=true" ng-mouseleave="assignment.phaseHover=false">
<span class="label" ng-class="{'label-primary': assignment.phase == 0,

View File

@ -978,7 +978,7 @@ img {
.os-table .title {
font-size: 115%;
margin-right: 5px;
margin-right: 3px;
padding: 0;
background-color: transparent;
}
@ -1370,6 +1370,21 @@ img {
animation: fade-out 0.25s linear;
}
/* xeditable */
.editable-click {
border: none;
cursor: pointer;
color: #555;
}
.editable-click:hover {
color: #555;
}
.popover-wrapper .editable-hide {
display: inline !important;
}
@keyframes fade-out {
0% { opacity: 1; background: none; }
100% { opacity: 0; background: none; }

View File

@ -13,12 +13,14 @@ angular.module('OpenSlidesApp.core.site', [
'formlyBootstrap',
'localytics.directives',
'ngBootbox',
'ngCookies',
'ngDialog',
'ngFileSaver',
'ngMessages',
'ngCsvImport',
'ui.tinymce',
'luegg.directives',
'xeditable',
])
// Can be used to find out if the projector or the side is used
@ -102,11 +104,25 @@ angular.module('OpenSlidesApp.core.site', [
.run([
'loadGlobalData',
'operator',
function(loadGlobalData, operator) {
function (loadGlobalData, operator) {
operator.onOperatorChange(loadGlobalData);
}
])
.run([
'editableOptions',
'gettext',
function (editableOptions, gettext) {
editableOptions.theme = 'bs3';
editableOptions.cancelButtonAriaLabel = gettext('Cancel');
editableOptions.cancelButtonTitle = gettext('Cancel');
editableOptions.clearButtonAriaLabel = gettext('Clear');
editableOptions.clearButtonTitle = gettext('Clear');
editableOptions.submitButtonAriaLabel = gettext('Submit');
editableOptions.submitButtonTitle = gettext('Submit');
}
])
.config([
'mainMenuProvider',
'gettext',
@ -355,13 +371,26 @@ angular.module('OpenSlidesApp.core.site', [
* - propertyList, propertyFunctionList, propertyDict: See function getObjectQueryString
*/
.factory('osTableFilter', [
function () {
var createInstance = function () {
'$cookies',
function ($cookies) {
var createInstance = function (cookieName) {
var self = {
multiselectFilters: {},
booleanFilters: {},
filterString: '',
};
var existsCookie = function () {
return $cookies.getObject(cookieName);
};
var cookie = existsCookie();
if (cookie) {
self = cookie;
}
self.existsCookie = existsCookie;
self.save = function () {
$cookies.putObject(cookieName, self);
};
self.areFiltersSet = function () {
var areFiltersSet = _.find(self.multiselectFilters, function (filterList) {
return filterList.length > 0;
@ -380,6 +409,7 @@ angular.module('OpenSlidesApp.core.site', [
self.booleanFilters[filter].value = undefined;
});
self.filterString = '';
self.save();
};
self.operateMultiselectFilter = function (filter, id, danger) {
if (!danger) {
@ -390,6 +420,7 @@ angular.module('OpenSlidesApp.core.site', [
// add id
self.multiselectFilters[filter].push(id);
}
self.save();
}
};
/* Three things are could be given to create the query string:

View File

@ -99,22 +99,25 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
updatePresentedMediafiles();
// Filtering
$scope.filter = osTableFilter.createInstance();
$scope.filter.booleanFilters = {
isPrivate: {
value: undefined,
displayName: gettext('Private'),
choiceYes: gettext('Is private'),
choiceNo: gettext('Is not private'),
needExtraPermission: true,
},
isPdf: {
value: undefined,
displayName: gettext('Is PDF'),
choiceYes: gettext('Is PDF file'),
choiceNo: gettext('Is no PDF file'),
},
};
$scope.filter = osTableFilter.createInstance('MediafilesTableFilter');
if (!$scope.filter.existsCookie()) {
$scope.filter.booleanFilters = {
isPrivate: {
value: undefined,
displayName: gettext('Private'),
choiceYes: gettext('Is private'),
choiceNo: gettext('Is not private'),
needExtraPermission: true,
},
isPdf: {
value: undefined,
displayName: gettext('Is PDF'),
choiceYes: gettext('Is PDF file'),
choiceNo: gettext('Is no PDF file'),
},
};
}
$scope.filter.propertyList = ['title_or_filename'];
$scope.filter.propertyFunctionList = [
function (mediafile) {return mediafile.uploader.get_short_name();},

View File

@ -170,13 +170,13 @@
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown{{ name }}">
<li>
<a href ng-click="booleanFilter.value = (booleanFilter.value ? undefined : true)">
<a href ng-click="booleanFilter.value = (booleanFilter.value ? undefined : true); filter.save();">
<i class="fa" ng-class="{'fa-check': booleanFilter.value === true}"></i>
{{ booleanFilter.choiceYes }}
</a>
</li>
<li>
<a href ng-click="booleanFilter.value = (booleanFilter.value === false) ? undefined : false">
<a href ng-click="booleanFilter.value = (booleanFilter.value === false) ? undefined : false; filter.save();">
<i class="fa" ng-class="{'fa-check': booleanFilter.value === false}"></i>
{{ booleanFilter.choiceNo }}
</a>
@ -209,7 +209,8 @@
<span class="input-group">
<span class="input-group-addon"><i class="fa fa-search"></i></span>
<input type="text" ng-model="filter.filterString" class="form-control"
placeholder="{{ 'Search' | translate}}" ng-disabled="isSelectMode">
placeholder="{{ 'Search' | translate}}" ng-disabled="isSelectMode"
ng-change="filter.save()">
</span>
</span>
</span>
@ -219,7 +220,7 @@
<span ng-repeat="(name, booleanFilter) in filter.booleanFilters"
ng-hide="booleanFilter.value === undefined"
class="pointer spacer-left-lg"
ng-click="booleanFilter.value = undefined;"
ng-click="booleanFilter.value = undefined; filter.save();"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>

View File

@ -822,13 +822,16 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.alert = {};
// Filtering
$scope.filter = osTableFilter.createInstance();
$scope.filter.multiselectFilters = {
state: [],
category: [],
motionBlock: [],
tag: []
};
$scope.filter = osTableFilter.createInstance('MotionTableFilter');
if (!$scope.filter.existsCookie()) {
$scope.filter.multiselectFilters = {
state: [],
category: [],
motionBlock: [],
tag: []
};
}
$scope.filter.propertyList = ['identifier', 'origin'];
$scope.filter.propertyFunctionList = [
function (motion) {return motion.getTitle();},
@ -907,7 +910,7 @@ angular.module('OpenSlidesApp.motions.site', [
// Use this methon instead of Motion.save(), because otherwise
// you have to provide always a title and a text
var save = function (motion) {
$scope.save = function (motion) {
motion.title = motion.getTitle(-1);
motion.text = motion.getText(-1);
motion.reason = motion.getReason(-1);
@ -922,7 +925,7 @@ angular.module('OpenSlidesApp.motions.site', [
} else {
motion.tags_id.push(tag.id);
}
save(motion);
$scope.save(motion);
};
$scope.toggleCategory = function (motion, category) {
if (motion.category_id == category.id) {
@ -930,7 +933,7 @@ angular.module('OpenSlidesApp.motions.site', [
} else {
motion.category_id = category.id;
}
save(motion);
$scope.save(motion);
};
$scope.toggleMotionBlock = function (motion, block) {
if (motion.motion_block_id == block.id) {
@ -938,7 +941,7 @@ angular.module('OpenSlidesApp.motions.site', [
} else {
motion.motion_block_id = block.id;
}
save(motion);
$scope.save(motion);
};
// open new/edit dialog

View File

@ -241,7 +241,8 @@
<span class="input-group">
<span class="input-group-addon"><i class="fa fa-search"></i></span>
<input type="text" ng-model="filter.filterString" class="form-control"
placeholder="{{ 'Search' | translate}}" ng-disabled="isSelectMode">
placeholder="{{ 'Search' | translate}}" ng-disabled="isSelectMode"
ng-change="filter.save()">
</span>
</span>
</span>
@ -506,7 +507,11 @@
<!-- Origin -->
<div ng-if="motion.origin">
<i class="fa fa-share spacer-right" uib-tooltip="{{ 'Origin' | translate }}"></i>
{{ motion.origin | limitTo:25 }}{{ motion.origin.length > 25 ? '...' : '' }}
<div class="popover-wrapper">
<span editable-text="motion.origin" onaftersave="save(motion)">
{{ motion.origin | limitTo:25 }}{{ motion.origin.length > 25 ? '...' : '' }}
</span>
</div>
</div>
</small>
</div>

View File

@ -510,39 +510,42 @@ angular.module('OpenSlidesApp.users.site', [
$scope.alert = {};
// Filtering
$scope.filter = osTableFilter.createInstance();
$scope.filter.multiselectFilters = {
group: [],
};
$scope.filter = osTableFilter.createInstance('UserTableFilter');
if (!$scope.filter.existsCookie()) {
$scope.filter.multiselectFilters = {
group: [],
};
$scope.filter.booleanFilters = {
isPresent: {
value: undefined,
displayName: gettext('Present'),
choiceYes: gettext('Is present'),
choiceNo: gettext('Is not present'),
needExtraPermission: true,
},
isActive: {
value: undefined,
displayName: gettext('Active'),
choiceYes: gettext('Is active'),
choiceNo: gettext('Is not active'),
needExtraPermission: true,
},
isCommittee: {
value: undefined,
displayName: gettext('Committee'),
choiceYes: gettext('Is committee'),
choiceNo: gettext('Is not committee'),
},
};
}
$scope.filter.propertyList = ['first_name', 'last_name', 'title', 'number', 'comment', 'structure_level'];
$scope.filter.propertyDict = {
'groups_id' : function (group_id) {
return Group.get(group_id).name;
},
};
$scope.filter.booleanFilters = {
isPresent: {
value: undefined,
displayName: gettext('Present'),
choiceYes: gettext('Is present'),
choiceNo: gettext('Is not present'),
needExtraPermission: true,
},
isActive: {
value: undefined,
displayName: gettext('Active'),
choiceYes: gettext('Is active'),
choiceNo: gettext('Is not active'),
needExtraPermission: true,
},
isCommittee: {
value: undefined,
displayName: gettext('Committee'),
choiceYes: gettext('Is committee'),
choiceNo: gettext('Is not committee'),
},
};
$scope.getItemId = {
group: function (user) {return user.groups_id;},
};

View File

@ -181,13 +181,13 @@
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown{{ name }}">
<li>
<a href ng-click="booleanFilter.value = (booleanFilter.value ? undefined : true)">
<a href ng-click="booleanFilter.value = (booleanFilter.value ? undefined : true); filter.save();">
<i class="fa" ng-class="{'fa-check': booleanFilter.value === true}"></i>
{{ booleanFilter.choiceYes }}
</a>
</li>
<li>
<a href ng-click="booleanFilter.value = (booleanFilter.value === false) ? undefined : false">
<a href ng-click="booleanFilter.value = (booleanFilter.value === false) ? undefined : false; filter.save();">
<i class="fa" ng-class="{'fa-check': booleanFilter.value === false}"></i>
{{ booleanFilter.choiceNo }}
</a>
@ -220,7 +220,8 @@
<span class="input-group">
<span class="input-group-addon"><i class="fa fa-search"></i></span>
<input type="text" ng-model="filter.filterString" class="form-control" ng-model-options="{debounce: 500}"
placeholder="{{ 'Search' | translate}}" ng-disabled="isSelectMode">
placeholder="{{ 'Search' | translate}}" ng-disabled="isSelectMode"
ng-change="filter.save()">
</span>
</span>
</span>
@ -245,7 +246,7 @@
<span ng-repeat="(name, booleanFilter) in filter.booleanFilters"
ng-hide="booleanFilter.value === undefined"
class="pointer spacer-left-lg"
ng-click="booleanFilter.value = undefined;"
ng-click="booleanFilter.value = undefined; filter.save();"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
@ -289,7 +290,14 @@
<a ui-sref="users.user.detail({id: user.id})" class="title">{{ user.get_short_name() }}</a>
</strong>
</div>
<div ng-if="user.number"><translate>No.</translate> {{ user.number }} </div>
<div os-perms="users.can_manage"> <!-- user number -->
<div ng-if="user.number" editable-text="user.number" onaftersave="save(user)">
<translate translate-comment="abbreviation for number">No.</translate> {{ user.number }}
</div>
</div>
<div os-perms="!users.can_manage" ng-if="user.number">
<translate translate-comment="abbreviation for number">No.</translate> {{ user.number }}
</div>
<div os-perms="users.can_manage" ng-class="{'hiddenDiv': !user.hover}">
<small>
<a href="" ng-click="openDialog(user)" translate>Edit</a> &middot;
@ -345,19 +353,41 @@
... [+{{ user.groups_id.length - 2}}]</span>
<!-- sorry for merging them together, but otherwise there would be a whitespace because of the new line -->
</div>
<div ng-if="user.structure_level" uib-tooltip="{{ 'Structure level' | translate }}" tooltip-placement="top-left">
<i class="fa fa-flag"></i>
{{ user.structure_level }}
<div os-perms="users.can_manage" ng-show="user.structure_level || user.hover">
<div class="popover-wrapper" uib-tooltip="{{ 'Structure level' | translate }}" tooltip-placement="top-left">
<i class="fa fa-flag"></i>
<span editable-text="user.structure_level" onaftersave="save(user)">
{{ user.structure_level || ('Set structure level...' | translate)}}
</span>
</div>
</div>
<div ng-if="user.comment" uib-tooltip="{{ 'Comment' | translate }}" tooltip-placement="top-left">
<i class="fa fa-info-circle"></i>
{{ user.comment | limitTo:25}}{{ user.comment.length > 25 ? '...' : '' }}
<div os-perms="!users.can_manage">
<div ng-if="user.structure_level" uib-tooltip="{{ 'Structure level' | translate }}" tooltip-placement="top-left">
<i class="fa fa-flag"></i>
{{ user.structure_level }}
</div>
</div>
<div os-perms="users.can_manage" ng-show="user.comment || user.hover">
<div class="popover-wrapper" uib-tooltip="{{ 'Comment' | translate }}" tooltip-placement="top-left">
<i class="fa fa-info-circle"></i>
<span editable-text="user.comment" onaftersave="save(user)">
{{ user.comment || ('Set comment...' | translate)}}
</span>
</div>
</div>
<div os-perms="!users.can_manage">
<div ng-if="user.comment" uib-tooltip="{{ 'Comment' | translate }}" tooltip-placement="top-left">
<i class="fa fa-info-circle"></i>
{{ user.comment | limitTo:25}}{{ user.comment.length > 25 ? '...' : '' }}
</div>
</div>
</small>
</div>
<div style="width: 40%;" class="pull-right" os-perms="users.can_see_extra_data">
<div os-perms="users.can_manage">
<span class="pointer nobr" ng-click="user.is_present = !user.is_present; save(user);">
<span class="pointer nobr" ng-click="user.is_present = !user.is_present; save(user);" ng-show="user.hover || user.is_present">
<i class="fa" ng-class="user.is_present ? 'fa-check-square-o' : 'fa-square-o'"></i>
<span class="spacer-left" translate>Present</span>
</span>