A new motions table with csv export
This commit is contained in:
parent
c87111e81d
commit
a05a29c99a
@ -96,6 +96,21 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
|
||||
}
|
||||
return title;
|
||||
},
|
||||
getItemNumberWithAncestors: function (agendaId) {
|
||||
if (!agendaId) {
|
||||
agendaId = this.id;
|
||||
}
|
||||
var agendaItem = DS.get(name, agendaId);
|
||||
if (!agendaItem) {
|
||||
return '';
|
||||
} else if (agendaItem.item_number) {
|
||||
return agendaItem.item_number;
|
||||
} else if (agendaItem.parent_id) {
|
||||
return this.getItemNumberWithAncestors(agendaItem.parent_id);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
// override project function of jsDataModel factory
|
||||
project: function() {
|
||||
return $http.post(
|
||||
|
@ -664,6 +664,100 @@ img {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/** Motion **/
|
||||
#motion-table .row {
|
||||
border: 1px solid #ddd;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
#motion-table .data-row:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
#motion-table .data-row > div {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#motion-table .header-row {
|
||||
border-top: 1px solid #ddd;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.header-row > div {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#motion-table .main-header {
|
||||
width: calc(100% - 50px);
|
||||
float: right;
|
||||
}
|
||||
|
||||
#motion-table .main-header .form-inline {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
#motion-table .content > div {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#motion-table .identifier-col {
|
||||
width: 50px;
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
#motion-table .identifier-col > div {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#motion-table .title-col {
|
||||
width: calc(100% - 50px);
|
||||
}
|
||||
|
||||
#motion-table .title-col, #motion-table small {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
#motion-table .content > div > div {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#motion-table .content > div > div:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
#motion-table .row .centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#motion-table .row .col-xs-1 {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
#motion-table .row .col-xs-4 {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#motion-table .dropdown {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#motion-table .dropdown > span, #motion-table .sort-spacer {
|
||||
padding: 5px 10px 5px 10px;
|
||||
}
|
||||
|
||||
#motion-table .dropdown-entry {
|
||||
padding: 5px 10px 5px 10px;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#motion-table .title {
|
||||
margin-right: 10px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/** Footer **/
|
||||
#footer {
|
||||
float: left;
|
||||
@ -675,11 +769,23 @@ img {
|
||||
|
||||
/** General helper classes **/
|
||||
|
||||
.disabled {
|
||||
color: #555;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #317796;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
margin-left: 0px !important;
|
||||
}
|
||||
|
||||
.dropdown-entries {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@ -711,6 +817,14 @@ img {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.spacer-left {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.spacer-left-lg {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.lead-div {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@ -758,6 +872,10 @@ img {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
|
||||
.listOfSpeakers h3 {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
@ -1047,7 +1165,7 @@ tr.hiddenrow td {
|
||||
}
|
||||
|
||||
tr.activeline td, li.activeline, .projected {
|
||||
background-color: #bed4de;
|
||||
background-color: #bed4de !important;
|
||||
}
|
||||
|
||||
tr.selected td {
|
||||
@ -1113,11 +1231,11 @@ tr.selected td {
|
||||
|
||||
#chatbox { width: 100%; top: 40px; }
|
||||
|
||||
/* hide marked element / column */
|
||||
.optional, .hide-sm { display: none; }
|
||||
|
||||
/* show replacement elements, if any */
|
||||
.optional-show { display: block !important; }
|
||||
|
||||
/* hide marked element / column */
|
||||
.optional, .hide-sm { display: none !important; }
|
||||
}
|
||||
|
||||
/* display for resolutions smaller that 560px */
|
||||
|
@ -455,8 +455,7 @@ angular.module('OpenSlidesApp.core', [
|
||||
])
|
||||
|
||||
.filter('osFilter', [
|
||||
'$filter',
|
||||
function ($filter) {
|
||||
function () {
|
||||
return function (array, string, getFilterString) {
|
||||
if (!string) {
|
||||
return array;
|
||||
@ -468,6 +467,30 @@ angular.module('OpenSlidesApp.core', [
|
||||
}
|
||||
])
|
||||
|
||||
// This filter filters all items in array. If the filterArray is empty, the array is passed.
|
||||
// The filterArray contains numbers of the multiselect: [1, 3, 4].
|
||||
// Then, all items in array are passed, if the item_id (get with id_function) matches one of the
|
||||
// ids in filterArray. id_function could also return a list of ids. Example:
|
||||
// Item 1 has two tags with ids [1, 4]. filterArray = [3, 4] --> match
|
||||
.filter('SelectMultipleFilter', [
|
||||
function () {
|
||||
return function (array, filterArray, idFunction) {
|
||||
if (filterArray.length === 0) {
|
||||
return array;
|
||||
}
|
||||
return Array.prototype.filter.call(array, function (item) {
|
||||
var id = idFunction(item);
|
||||
if (!id) {
|
||||
return false;
|
||||
} else if (typeof id === 'number') {
|
||||
id = [id];
|
||||
}
|
||||
return _.intersection(id, filterArray).length > 0;
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
// mark HTML as "trusted"
|
||||
.filter('trusted', [
|
||||
'$sce',
|
||||
|
@ -661,6 +661,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
.controller('MotionListCtrl', [
|
||||
'$scope',
|
||||
'$state',
|
||||
'$http',
|
||||
'ngDialog',
|
||||
'MotionForm',
|
||||
'Motion',
|
||||
@ -668,7 +669,8 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
'Tag',
|
||||
'Workflow',
|
||||
'User',
|
||||
function($scope, $state, ngDialog, MotionForm, Motion, Category, Tag, Workflow, User) {
|
||||
'Agenda',
|
||||
function($scope, $state, $http, ngDialog, MotionForm, Motion, Category, Tag, Workflow, User, Agenda) {
|
||||
Motion.bindAll({}, $scope, 'motions');
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
Tag.bindAll({}, $scope, 'tags');
|
||||
@ -680,6 +682,32 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
$scope.sortColumn = 'identifier';
|
||||
$scope.filterPresent = '';
|
||||
$scope.reverse = false;
|
||||
|
||||
$scope.multiselectFilter = {
|
||||
state: [],
|
||||
category: [],
|
||||
tag: []
|
||||
};
|
||||
$scope.getItemId = {
|
||||
state: function (motion) {return motion.state_id;},
|
||||
category: function (motion) {return motion.category_id;},
|
||||
tag: function (motion) {return motion.tags_id;}
|
||||
};
|
||||
// function to operate the multiselectFilter
|
||||
$scope.operateMultiselectFilter = function (filter, id) {
|
||||
if (!$scope.isDeleteMode) {
|
||||
if (_.indexOf($scope.multiselectFilter[filter], id) > -1) {
|
||||
// remove id
|
||||
$scope.multiselectFilter[filter] = _.filter($scope.multiselectFilter[filter], function (_id) {
|
||||
return _id != id;
|
||||
});
|
||||
} else {
|
||||
// add id
|
||||
$scope.multiselectFilter[filter].push(id);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
// function to sort by clicked column
|
||||
$scope.toggleSort = function (column) {
|
||||
if ( $scope.sortColumn === column ) {
|
||||
@ -720,6 +748,23 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
category,
|
||||
].join(" ");
|
||||
};
|
||||
// for reset-button
|
||||
$scope.reset_filters = function () {
|
||||
$scope.multiselectFilter = {
|
||||
state: [],
|
||||
category: [],
|
||||
tag: []
|
||||
};
|
||||
if ($scope.filter) {
|
||||
$scope.filter.search = '';
|
||||
}
|
||||
};
|
||||
$scope.are_filters_set = function () {
|
||||
return $scope.multiselectFilter.state.length > 0 ||
|
||||
$scope.multiselectFilter.category.length > 0 ||
|
||||
$scope.multiselectFilter.tag.length > 0 ||
|
||||
($scope.filter ? $scope.filter.search : false);
|
||||
};
|
||||
|
||||
// collect all states of all workflows
|
||||
// TODO: regard workflows only which are used by motions
|
||||
@ -729,7 +774,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
if (workflows.length > 1) {
|
||||
var wf = {};
|
||||
wf.name = workflow.name;
|
||||
wf.workflowSeparator = "-";
|
||||
wf.workflowHeader = true;
|
||||
$scope.states.push(wf);
|
||||
}
|
||||
angular.forEach(workflow.states, function (state) {
|
||||
@ -737,41 +782,83 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
});
|
||||
});
|
||||
|
||||
// update state
|
||||
$scope.updateState = function (motion, state_id) {
|
||||
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {'state': state_id});
|
||||
};
|
||||
// reset state
|
||||
$scope.reset_state = function (motion) {
|
||||
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {});
|
||||
};
|
||||
|
||||
$scope.has_tag = function (motion, tag) {
|
||||
return _.indexOf(motion.tags_id, tag.id) > -1;
|
||||
};
|
||||
|
||||
// Use this methon instead of Motion.save(), because otherwise
|
||||
// you have to provide always a title and a text
|
||||
var save = function (motion) {
|
||||
motion.title = motion.getTitle(-1);
|
||||
motion.text = motion.getText(-1);
|
||||
motion.reason = motion.getReason(-1);
|
||||
Motion.save(motion);
|
||||
};
|
||||
$scope.toggle_tag = function (motion, tag) {
|
||||
if ($scope.has_tag(motion, tag)) {
|
||||
// remove
|
||||
motion.tags_id = _.filter(motion.tags_id, function (tag_id){
|
||||
return tag_id != tag.id;
|
||||
});
|
||||
} else {
|
||||
motion.tags_id.push(tag.id);
|
||||
}
|
||||
save(motion);
|
||||
};
|
||||
$scope.toggle_category = function (motion, category) {
|
||||
if (motion.category_id == category.id) {
|
||||
motion.category_id = null;
|
||||
} else {
|
||||
motion.category_id = category.id;
|
||||
}
|
||||
save(motion);
|
||||
};
|
||||
|
||||
// open new/edit dialog
|
||||
$scope.openDialog = function (motion) {
|
||||
ngDialog.open(MotionForm.getDialog(motion));
|
||||
};
|
||||
// cancel QuickEdit mode
|
||||
$scope.cancelQuickEdit = function (motion) {
|
||||
// revert all changes by restore (refresh) original motion object from server
|
||||
Motion.refresh(motion);
|
||||
motion.quickEdit = false;
|
||||
};
|
||||
// save changed motion
|
||||
$scope.save = function (motion) {
|
||||
// get (unchanged) values from latest version for update method
|
||||
motion.title = motion.getTitle(-1);
|
||||
motion.text = motion.getText(-1);
|
||||
motion.reason = motion.getReason(-1);
|
||||
Motion.save(motion).then(
|
||||
function(success) {
|
||||
motion.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 };
|
||||
|
||||
// Export the given motions as a csv file
|
||||
$scope.csv_export = function () {
|
||||
var element = document.getElementById('downloadLink');
|
||||
var csvRows = [
|
||||
['identifier', 'title', 'text', 'reason', 'submitter', 'category', 'origin'],
|
||||
];
|
||||
angular.forEach($scope.motionsFiltered, function (motion) {
|
||||
var row = [];
|
||||
row.push('"' + motion.identifier + '"');
|
||||
row.push('"' + motion.getTitle() + '"');
|
||||
row.push('"' + motion.getText() + '"');
|
||||
row.push('"' + motion.getReason() + '"');
|
||||
row.push('"' + motion.submitters[0].get_full_name() + '"');
|
||||
var category = motion.category ? motion.category.name : '';
|
||||
row.push('"' + category + '"');
|
||||
row.push('"' + motion.origin + '"');
|
||||
csvRows.push(row);
|
||||
});
|
||||
|
||||
var csvString = csvRows.join("%0A");
|
||||
element.href = 'data:text/csv;charset=utf-8,' + csvString;
|
||||
element.download = 'motions-export.csv';
|
||||
element.target = '_blank';
|
||||
};
|
||||
|
||||
// *** delete mode functions ***
|
||||
$scope.isDeleteMode = false;
|
||||
// check all checkboxes
|
||||
// check all checkboxes from filtered motions
|
||||
$scope.checkAll = function () {
|
||||
angular.forEach($scope.motions, function (motion) {
|
||||
$scope.selectedAll = !$scope.selectedAll;
|
||||
angular.forEach($scope.motionsFiltered, function (motion) {
|
||||
motion.selected = $scope.selectedAll;
|
||||
});
|
||||
};
|
||||
@ -786,7 +873,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
};
|
||||
// delete selected motions
|
||||
$scope.deleteMultiple = function () {
|
||||
angular.forEach($scope.motions, function (motion) {
|
||||
angular.forEach($scope.motionsFiltered, function (motion) {
|
||||
if (motion.selected)
|
||||
Motion.destroy(motion.id);
|
||||
});
|
||||
|
@ -17,10 +17,6 @@
|
||||
<i class="fa fa-download fa-lg"></i>
|
||||
<translate>Import</translate>
|
||||
</a>
|
||||
<a ui-sref="motions_pdf" target="_blank" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-file-pdf-o fa-lg"></i>
|
||||
<translate>PDF</translate>
|
||||
</a>
|
||||
</div>
|
||||
<h1 translate>Motions</h1>
|
||||
</div>
|
||||
@ -28,7 +24,7 @@
|
||||
|
||||
<div class="details">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="col-sm-12">
|
||||
<!-- delete mode -->
|
||||
<button os-perms="motions.can_manage" class="btn"
|
||||
ng-class="$parent.isDeleteMode ? 'btn-primary' : 'btn-default'"
|
||||
@ -36,38 +32,39 @@
|
||||
<i class="fa fa-check-square-o"></i>
|
||||
<translate>Select ...</translate>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<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>
|
||||
<!-- Export dropdown -->
|
||||
<div class="dropdown 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="motionsFiltered.length == motions.length" translate>
|
||||
Export all
|
||||
</span>
|
||||
<span ng-if="motionsFiltered.length != motions.length" translate>
|
||||
Export filtered
|
||||
</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownExport">
|
||||
<li>
|
||||
<a ui-sref="motions_pdf" target="_blank">
|
||||
<i class="fa fa-file-pdf-o fa-lg"></i>
|
||||
<translate>PDF</translate>
|
||||
</a>
|
||||
</li>
|
||||
<!--CSV export -->
|
||||
<li>
|
||||
<a href="" id="downloadLink"
|
||||
os-perms="motions.can_manage"
|
||||
ng-click="csv_export()">
|
||||
<i class="fa fa-file-text-o fa-lg"></i>
|
||||
<translate>CSV</translate>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div uib-collapse="!isFilterOpen" class="row spacer">
|
||||
<div class="col-sm-6 text-right"></div>
|
||||
<div class="col-sm-6 text-right">
|
||||
<!-- state filter -->
|
||||
<select ng-model="stateFilter" class="form-control" id="stateFilter">
|
||||
<option value="" translate>--- Select state ---</option>
|
||||
<option ng-repeat="state in states" value="{{ state.id }}">
|
||||
{{ state.workflowSeparator }}
|
||||
{{ state.name | translate }}
|
||||
{{ state.workflowSeparator }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div uib-collapse="!isDeleteMode" class="row spacer">
|
||||
<div class="col-sm-12 text-left">
|
||||
<!-- delete button -->
|
||||
@ -85,194 +82,377 @@
|
||||
{{ motions.length }} {{ "motions" | translate }}<span ng-if="(motions|filter:{selected:true}).length > 0">,
|
||||
{{(motions|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- projector column -->
|
||||
<th ng-show="!$parent.isDeleteMode" os-perms="core.can_manage_projector" class="minimum">
|
||||
<!-- delete selection column -->
|
||||
<th ng-show="$parent.isDeleteMode" os-perms="motions.can_manage" class="minimum deleteColumn">
|
||||
<input type="checkbox" ng-model="$parent.selectedAll" ng-change="checkAll()">
|
||||
|
||||
<!-- agenda item column -->
|
||||
<th ng-click="toggleSort('agenda_item.item_number')" class="sortable optional">
|
||||
<translate translate-comment="short form of agenda item">Item</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'agenda_item.item_number' && header.sortable != false"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
<!-- identifier column -->
|
||||
<th ng-click="toggleSort('identifier')" class="sortable minimum">
|
||||
<translate>Identifier</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'identifier' && header.sortable != false"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
<div id="motion-table" class="container-fluid">
|
||||
<div class="row header-row">
|
||||
<div class="col-xs-1 centered" ng-show="isDeleteMode">
|
||||
<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">
|
||||
|
||||
<!-- title column -->
|
||||
<th ng-click="toggleSort('getTitle()')" class="sortable">
|
||||
<translate>Title</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'getTitle()' && header.sortable != false"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
|
||||
<!-- submitters column -->
|
||||
<th ng-click="toggleSort('submitters')" class="sortable optional">
|
||||
<translate>Submitters</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'submitters' && header.sortable != false"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
|
||||
<!-- category column -->
|
||||
<th ng-click="toggleSort('category')" class="sortable optional">
|
||||
<translate>Category</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'category' && header.sortable != false"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
|
||||
<!-- state column -->
|
||||
<th ng-click="toggleSort('state.name')" class="sortable optional">
|
||||
<span class="form-inline text-right pull-right">
|
||||
<span class="sort-spacer pointer" ng-click="reset_filters()"
|
||||
ng-if="are_filters_set()" ng-disabled="isDeleteMode"
|
||||
ng-class="{'disabled': isDeleteMode}">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
<translate>Filter</translate>
|
||||
</span>
|
||||
<!-- Statefilter -->
|
||||
<span class="dropdown" uib-dropdown>
|
||||
<span class="pointer" id="dropdownState" uib-dropdown-toggle
|
||||
ng-class="{'bold': multiselectFilter.state.length > 0, 'disabled': isDeleteMode}"
|
||||
ng-disabled="isDeleteMode">
|
||||
<translate>State</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'state.name' && header.sortable != false"
|
||||
<span class="caret"></span>
|
||||
</span>
|
||||
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownState">
|
||||
<li ng-repeat="state in states" ng-class="state.workflowHeader ? 'dropdown-header' : ''">
|
||||
<div class="dropdown-entry pointer" ng-if="state.workflowHeader">
|
||||
{{ state.name | translate }}
|
||||
</div>
|
||||
<div class="dropdown-entry pointer" ng-if="!state.workflowHeader"
|
||||
ng-click="operateMultiselectFilter('state', state.id)">
|
||||
<i class="fa fa-check" ng-if="multiselectFilter.state.indexOf(state.id) > -1"></i>
|
||||
{{ state.name | translate }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<!-- Categoryfilter -->
|
||||
<span class="dropdown" uib-dropdown ng-if="categories.length > 0">
|
||||
<span class="pointer" id="dropdownCategory" uib-dropdown-toggle
|
||||
ng-class="{'bold': multiselectFilter.category.length > 0, 'disabled': isDeleteMode}"
|
||||
ng-disabled="isDeleteMode">
|
||||
<translate>Category</translate>
|
||||
<span class="caret"></span>
|
||||
</span>
|
||||
<ul class="dropdown-menu dropdown-menu-right"
|
||||
aria-labelledby="dropdownCategory">
|
||||
<li ng-repeat="category in categories">
|
||||
<div class="dropdown-entry pointer"
|
||||
ng-click="operateMultiselectFilter('category', category.id)">
|
||||
<i class="fa fa-check" ng-if="multiselectFilter.category.indexOf(category.id) > -1"></i>
|
||||
{{ category.name }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<!-- Tagfilter -->
|
||||
<span class="dropdown" uib-dropdown ng-if="tags.length > 0">
|
||||
<span class="pointer" id="dropdownTag" uib-dropdown-toggle
|
||||
ng-class="{'bold': multiselectFilter.tag.length > 0, 'disabled': isDeleteMode}"
|
||||
ng-disabled="isDeleteMode">
|
||||
<translate>Tag</translate>
|
||||
<span class="caret"></span>
|
||||
</span>
|
||||
<ul class="dropdown-menu dropdown-menu-right"
|
||||
aria-labelledby="dropdownTag">
|
||||
<li ng-repeat="tag in tags">
|
||||
<div class="dropdown-entry pointer"
|
||||
ng-click="operateMultiselectFilter('tag', tag.id)">
|
||||
<i class="fa fa-check" ng-if="multiselectFilter.tag.indexOf(tag.id) > -1"></i>
|
||||
{{ tag.name }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<!-- dropdown sort -->
|
||||
<span class="dropdown" uib-dropdown>
|
||||
<span class="pointer" id="dropdownSort" uib-dropdown-toggle
|
||||
ng-class="{'disabled': isDeleteMode}"
|
||||
ng-disabled="isDeleteMode">
|
||||
<translate>Sort</translate>
|
||||
<span class="caret"></span>
|
||||
</span>
|
||||
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownSort">
|
||||
<li>
|
||||
<!-- item -->
|
||||
<div class="pointer dropdown-entry" ng-click="toggleSort('agenda_item.getItemNumberWithAncestors()')">
|
||||
<translate translate-comment="short form of agenda item">Item</translate>
|
||||
<span class="spacer-right pull-right"></span>
|
||||
<i class="pull-right fa"
|
||||
ng-style="{'visibility': sortColumn === 'agenda_item.getItemNumberWithAncestors()' && header.sortable != false ? 'visible' : 'hidden'}"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
<tbody>
|
||||
<tr ng-repeat="motion in motionsFiltered = (motions | osFilter: filter.search : getFilterString |
|
||||
filter: {state_id: stateFilter} | orderBy: sortColumn:reverse)"
|
||||
class="animate-item"
|
||||
ng-class="{ 'activeline': motion.isProjected(), 'selected': motion.selected }">
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<!-- indentifier -->
|
||||
<div class="pointer dropdown-entry" ng-click="toggleSort('identifier')">
|
||||
<translate>Identifier</translate>
|
||||
<span class="spacer-right pull-right"></span>
|
||||
<i class="pull-right fa"
|
||||
ng-style="{'visibility': sortColumn === 'identifier' && header.sortable != false ? 'visible' : 'hidden'}"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<!-- title -->
|
||||
<div class="pointer dropdown-entry" ng-click="toggleSort('getTitle()')">
|
||||
<translate>Title</translate>
|
||||
<span class="spacer-right pull-right"></span>
|
||||
<i class="pull-right fa"
|
||||
ng-style="{'visibility': sortColumn === 'getTitle()' && header.sortable != false ? 'visible' : 'hidden'}"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<!-- submitters -->
|
||||
<div class="pointer dropdown-entry" ng-click="toggleSort('submitters')">
|
||||
<translate>Submitters</translate>
|
||||
<span class="spacer-right pull-right"></span>
|
||||
<i class="pull-right fa"
|
||||
ng-style="{'visibility': sortColumn === 'submitters' && header.sortable != false ? 'visible' : 'hidden'}"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<!-- category -->
|
||||
<div class="pointer dropdown-entry" ng-click="toggleSort('category')">
|
||||
<translate>Category</translate>
|
||||
<span class="spacer-right pull-right"></span>
|
||||
<i class="pull-right fa"
|
||||
ng-style="{'visibility': sortColumn === 'category' && header.sortable != false ? 'visible' : 'hidden'}"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<!-- state -->
|
||||
<div class="pointer dropdown-entry" ng-click="toggleSort('state.name')">
|
||||
<translate>State</translate>
|
||||
<span class="spacer-right pull-right"></span>
|
||||
<i class="pull-right fa"
|
||||
ng-style="{'visibility': sortColumn === 'state.name' && header.sortable != false ? 'visible' : 'hidden'}"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<!-- 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.search" class="form-control"
|
||||
placeholder="{{ 'Search' | translate}}" ng-disabled="isDeleteMode">
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<!-- show all selected multiselectoptions -->
|
||||
<span>
|
||||
<span ng-repeat="state in states" class="pointer spacer-left-lg"
|
||||
ng-if="!state.workflowHeader && multiselectFilter.state.indexOf(state.id) > -1"
|
||||
ng-click="operateMultiselectFilter('state', state.id)"
|
||||
ng-class="{'disabled': isDeleteMode}">
|
||||
<span class="nobr">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
{{ state.name | translate }}
|
||||
</span>
|
||||
</span>
|
||||
<span ng-repeat="category in categories" class="pointer spacer-left-lg"
|
||||
ng-if="multiselectFilter.category.indexOf(category.id) > -1"
|
||||
ng-click="operateMultiselectFilter('category', category.id)"
|
||||
ng-class="{'disabled': isDeleteMode}">
|
||||
<span class="nobr">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
{{ category.name }}
|
||||
</span>
|
||||
</span>
|
||||
<span ng-repeat="tag in tags" class="pointer spacer-left-lg"
|
||||
ng-if="multiselectFilter.tag.indexOf(tag.id) > -1"
|
||||
ng-click="operateMultiselectFilter('tag', tag.id)"
|
||||
ng-class="{'disabled': isDeleteMode}">
|
||||
<span class="nobr">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- projector -->
|
||||
<td ng-show="!isDeleteMode" os-perms="core.can_manage_projector">
|
||||
<!-- main table -->
|
||||
<!-- data row -->
|
||||
<div class="row data-row" ng-mouseover="motion.hover=true"
|
||||
ng-mouseleave="motion.hover=false"
|
||||
ng-class="{'projected': motion.isProjected()}"
|
||||
ng-repeat="motion in motionsFiltered = (motions
|
||||
| osFilter: filter.search : getFilterString
|
||||
| SelectMultipleFilter: multiselectFilter.state : getItemId.state
|
||||
| SelectMultipleFilter: multiselectFilter.category : getItemId.category
|
||||
| SelectMultipleFilter: multiselectFilter.tag : getItemId.tag
|
||||
| orderBy: sortColumn : reverse)">
|
||||
|
||||
<!-- select column -->
|
||||
<div ng-show="isDeleteMode" os-perms="motions.can_manage"
|
||||
class="col-xs-1 centered" ng-class="{'deleteColumn' : motion.selected}">
|
||||
<i class="fa text-danger pointer" ng-click="motion.selected=!motion.selected"
|
||||
ng-class="motion.selected ? 'fa-check-square-o' : 'fa-square-o'"></i>
|
||||
</div>
|
||||
<!-- projector column -->
|
||||
<div class="col-xs-1 centered" os-perms="core.can_manage_projector">
|
||||
<a class="btn btn-default btn-sm"
|
||||
ng-class="{ 'btn-primary': motion.isProjected() }"
|
||||
ng-click="motion.project()"
|
||||
title="{{ 'Project motion' | translate }}">
|
||||
<i class="fa fa-video-camera"></i>
|
||||
</a>
|
||||
|
||||
<!-- delete selection -->
|
||||
<td ng-show="isDeleteMode" os-perms="motions.can_manage" class="deleteColumn">
|
||||
<input type="checkbox" ng-model="motion.selected">
|
||||
|
||||
<!-- agenda item number -->
|
||||
<td ng-if="!motion.quickEdit" class="optional">{{ motion.agenda_item.item_number }}
|
||||
|
||||
<!-- identifier -->
|
||||
<td ng-if="!motion.quickEdit">{{ motion.identifier }}
|
||||
|
||||
<!-- title -->
|
||||
<td ng-if="!motion.quickEdit" ng-mouseover="motion.hover=true" ng-mouseleave="motion.hover=false">
|
||||
<strong><a ui-sref="motions.motion.detail({id: motion.id})">{{ motion.getTitle() }}</a></strong>
|
||||
<span ng-repeat="tag in motion.tags" class="label label-default">
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
<div ng-if="motion.origin">
|
||||
<small>
|
||||
<i class="fa fa-info-circle"></i> <translate>Origin</translate>:
|
||||
{{ motion.origin | limitTo:30 }}{{ motion.origin.length > 30 ? '...' : '' }}
|
||||
</small>
|
||||
</div>
|
||||
<div ng-if="motion.isAllowed('update')" class="hoverActions" ng-class="{'hiddenDiv': !motion.hover}">
|
||||
<!-- main content column -->
|
||||
<div class="col-xs-6 content">
|
||||
<div class="identifier-col">
|
||||
<div class="nobr" ng-show="motion.identifier">
|
||||
{{ motion.identifier }}:
|
||||
</div>
|
||||
<!-- hover menu -->
|
||||
<div ng-if="motion.isAllowed('update')" ng-class="{'hiddenDiv': !motion.hover}">
|
||||
<span ng-if="motion.isAllowed('update')">
|
||||
<a href="" ng-click="openDialog(motion)" translate>Edit</a>
|
||||
</span>
|
||||
<span ng-if="motion.isAllowed('quickedit')">
|
||||
| <a href="" ng-click="motion.quickEdit=true" translate>QuickEdit</a> |
|
||||
<a href="" ng-click="openDialog(motion)">
|
||||
<i class="fa fa-pencil"></i></a>
|
||||
</span>
|
||||
<span ng-if="motion.isAllowed('delete')">
|
||||
<a href="" class="text-danger"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
|
||||
<a href="" ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
|
||||
<b>{{ motion.getTitle() }}</b>"
|
||||
ng-bootbox-confirm-action="delete(motion)" translate>Delete</a>
|
||||
ng-bootbox-confirm-action="delete(motion)">
|
||||
<i class="fa fa-trash text-danger"></i></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- submitters -->
|
||||
<td ng-if="!motion.quickEdit" class="optional">
|
||||
<div ng-repeat="submitter in motion.submitters">
|
||||
{{ submitter.get_full_name() }}<br>
|
||||
</div>
|
||||
|
||||
<!-- category -->
|
||||
<td ng-if="!motion.quickEdit" class="optional">
|
||||
{{ motion.category.name }}
|
||||
|
||||
<!-- state -->
|
||||
<td ng-if="!motion.quickEdit" class="optional">
|
||||
<div class="title-col">
|
||||
<!-- ID and title -->
|
||||
<div>
|
||||
<strong>
|
||||
<a class="title" ui-sref="motions.motion.detail({id: motion.id})">{{ motion.getTitle() }}</a>
|
||||
</strong>
|
||||
<i class="fa fa-paperclip" ng-if="motion.attachments_id.length > 0"></i>
|
||||
<span style="padding: 5px;" ng-mouseover="motion.stateHover=true" ng-mouseleave="motion.stateHover=false">
|
||||
<span class="label" ng-class="'label-'+motion.state.css_class">
|
||||
{{ motion.state.name | translate }}
|
||||
</span>
|
||||
<span ng-class="{'hiddenDiv': !motion.stateHover}" uib-dropdown>
|
||||
<i class="fa fa-cog pointer" uib-dropdown-toggle id="state-dropdown{{ motion.id }}"></i>
|
||||
<ul uib-dropdown-menu aria-labelledby="state-dropdown{{ motion.id }}">
|
||||
<li ng-repeat="state in motion.state.getNextStates()">
|
||||
<a href ng-click="updateState(motion, state.id)">{{ state.action_word | translate }}</a>
|
||||
</li>
|
||||
<li class="divider" ng-if="motion.state.getNextStates().length"></li>
|
||||
<li>
|
||||
<a href ng-if="motion.isAllowed('reset_state')" ng-click="reset_state(motion)">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<translate>Reset state</translate>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- Submitters -->
|
||||
<div>
|
||||
<span>
|
||||
<span class="optional" translate>by</span>
|
||||
<span class="optional" ng-repeat="submitter in motion.submitters | limitTo:3">
|
||||
{{ submitter.get_full_name() }}<span ng-if="!$last">,</span></span><span ng-if="motion.submitters.length > 3">, ...</span>
|
||||
<!-- sorry for merging them together, but otherwise there would be a whitespace because of the new line -->
|
||||
|
||||
<!-- quickEdit columns -->
|
||||
<td ng-if="motion.quickEdit && motion.isAllowed('quickedit')" class="quickmode" colspan="6">
|
||||
<h4>{{ motion.getTitle() }} <span class="text-muted">– <translate>QuickEdit</translate></span></h4>
|
||||
<uib-alert ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">
|
||||
{{ alert.msg }}
|
||||
</uib-alert>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<label for="inputIdentifier" translate>Identifier</label>
|
||||
<input type="text" ng-model="motion.identifier" class="form-control input-sm"
|
||||
id="inputIdentifier">
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<label for="selectCategory" translate>Category</label>
|
||||
<select ng-options="category.id as category.name for category in categories"
|
||||
ng-model="motion.category_id" class="form-control" id="selectCategory">
|
||||
</select>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<label for="selectSubmitter" translate>Submitters</label>
|
||||
<select multiple chosen
|
||||
ng-model="motion.submitters_id"
|
||||
ng-options="user.id as user.full_name for user in users"
|
||||
search-contains="true"
|
||||
id="selectSubmitter"
|
||||
class="form-control"
|
||||
data-placeholder-text-multiple="'Select or search a submitter ...' | translate"
|
||||
no-results-text="'No results match' | translate">
|
||||
</select>
|
||||
<!-- additional content column -->
|
||||
<style>
|
||||
#motion-table .row .col-xs-4 {
|
||||
width: calc(50% - {{ isDeleteMode ? '100' : '50' }}px);
|
||||
}
|
||||
</style>
|
||||
<div class="col-xs-4 content">
|
||||
<div style="width: 60%;" class="optional">
|
||||
<small>
|
||||
<div ng-mouseover="motion.categoryHover=true"
|
||||
ng-mouseleave="motion.categoryHover=false"
|
||||
ng-show="categories.length > 0">
|
||||
<!-- Category dropdown -->
|
||||
<span uib-dropdown>
|
||||
<span id="dropdown-category{{ motion.id }}" class="pointer"
|
||||
uib-dropdown-toggle uib-tooltip="{{ 'Set a category' | translate }}"
|
||||
tooltip-class="nobr">
|
||||
<span ng-if="motion.category == null" ng-show="motion.hover">
|
||||
<i class="fa fa-sitemap"></i>
|
||||
<i class="fa fa-plus"></i>
|
||||
</span>
|
||||
<span ng-if="motion.category != null">
|
||||
<i class="fa fa-sitemap spacer-right"></i>
|
||||
{{ motion.category.name }}
|
||||
<i class="fa fa-cog fa-lg spacer-left" ng-show="motion.categoryHover"></i>
|
||||
</span>
|
||||
</span>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdown-category{{ motion.id }}">
|
||||
<li ng-repeat="category in categories">
|
||||
<div class="dropdown-entry pointer"
|
||||
ng-click="toggle_category(motion, category)">
|
||||
<i class="fa fa-check" ng-if="category.id == motion.category.id"></i>
|
||||
{{ category.name }}
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<label for="selectTags" translate>Tags</label>
|
||||
<select multiple chosen
|
||||
ng-model="motion.tags_id"
|
||||
ng-options="tag.id as tag.name for tag in tags"
|
||||
search-contains="true"
|
||||
id="selectTag"
|
||||
class="form-control"
|
||||
data-placeholder-text-multiple="'Select or search a tag ...' | translate"
|
||||
no-results-text="'No results match' | translate">
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
<div ng-mouseover="motion.tagHover=true"
|
||||
ng-mouseleave="motion.tagHover=false"
|
||||
ng-show="tags.length > 0">
|
||||
<span uib-dropdown>
|
||||
<span id="dropdown-tags{{ motion.id }}" class="pointer"
|
||||
uib-dropdown-toggle uib-tooltip="{{ 'Add a tag' | translate }}"
|
||||
tooltip-class="nobr">
|
||||
<span ng-if="motion.tags.length == 0" ng-show="motion.hover">
|
||||
<i class="fa fa-tags"></i>
|
||||
<i class="fa fa-plus"></i>
|
||||
</span>
|
||||
<span ng-if="motion.tags.length > 0">
|
||||
<i class="fa fa-tags spacer-right"></i>
|
||||
<span ng-repeat="tag in motion.tags">
|
||||
{{ tag.name }}<span ng-if="!$last">,</span>
|
||||
</span>
|
||||
<i class="fa fa-cog fa-lg spacer-left" ng-show="motion.tagHover"></i>
|
||||
</span>
|
||||
</span>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdown-tags{{ motion.id }}">
|
||||
<li ng-repeat="tag in tags">
|
||||
<div class="dropdown-entry pointer" ng-click="toggle_tag(motion, tag)">
|
||||
<i class="fa fa-check" ng-if="has_tag(motion, tag)"></i>
|
||||
{{ tag.name }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
</small>
|
||||
</div>
|
||||
<div style="width: 10%;" class="pull-right optional">
|
||||
<div class="pull-right" ng-if="config('motions_min_supporters') != 0"
|
||||
uib-tooltip="{{ motion.supporters.length }} {{ 'Supporters' | translate }}
|
||||
{{ (config('motions_min_supporters') - motion.supporters.length) > 0 ? '(' + (config('motions_min_supporters') - motion.supporters.length) + ' ' + ('needed' | translate) + ')': '' }}"
|
||||
tooltip-class="nobr">
|
||||
<span class="badge"
|
||||
ng-class="{'badge-info': motion.supporters.length < config('motions_min_supporters')}">
|
||||
{{ motion.supporters.length }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div ng-if="config('motions_min_supporters') > 0">
|
||||
<label for="selectSupporter" translate>Supporters</label>
|
||||
<select multiple chosen
|
||||
ng-model="motion.supporters_id"
|
||||
ng-options="user.id as user.full_name for user in users"
|
||||
search-contains="true"
|
||||
id="selectSupporter"
|
||||
class="form-control"
|
||||
data-placeholder-text-multiple="'Select or search a supporter ...' | translate"
|
||||
no-results-text="'No results match' | translate">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
<div style="width: 30%;" class="pull-right">
|
||||
<div class="centered">{{ motion.agenda_item.getItemNumberWithAncestors() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer">
|
||||
<button ng-click="cancelQuickEdit(motion)" class="btn btn-default pull-left" translate>
|
||||
Cancel
|
||||
</button>
|
||||
<button ng-if="motion.isAllowed('update')" ng-click="save(motion)" class="btn btn-primary" translate>
|
||||
Update
|
||||
</button>
|
||||
<a ng-if="motion.isAllowed('update')" ui-sref="motions.motion.detail.update({id: motion.id })"
|
||||
class="pull-right" translate>Edit motion ...</a>
|
||||
</div>
|
||||
</table>
|
||||
</div>
|
||||
</div> <!-- data row -->
|
||||
|
||||
</div> <!-- container -->
|
||||
</div> <!-- details -->
|
||||
|
Loading…
Reference in New Issue
Block a user