From a05a29c99acbcd5d572635415e22938cc7b18a2e Mon Sep 17 00:00:00 2001 From: Finn Stutzenstein Date: Fri, 12 Aug 2016 15:01:41 +0200 Subject: [PATCH] A new motions table with csv export --- openslides/agenda/static/js/agenda/base.js | 15 + openslides/core/static/css/app.css | 126 +++- openslides/core/static/js/core/base.js | 27 +- openslides/motions/static/js/motions/site.js | 145 ++++- .../static/templates/motions/motion-list.html | 612 +++++++++++------- 5 files changed, 674 insertions(+), 251 deletions(-) diff --git a/openslides/agenda/static/js/agenda/base.js b/openslides/agenda/static/js/agenda/base.js index 25ce9a5c9..cf5c62d09 100644 --- a/openslides/agenda/static/js/agenda/base.js +++ b/openslides/agenda/static/js/agenda/base.js @@ -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( diff --git a/openslides/core/static/css/app.css b/openslides/core/static/css/app.css index e49f37135..88a455a43 100644 --- a/openslides/core/static/css/app.css +++ b/openslides/core/static/css/app.css @@ -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 */ diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js index 6b46cfaa7..2f52f4dfd 100644 --- a/openslides/core/static/js/core/base.js +++ b/openslides/core/static/js/core/base.js @@ -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', diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index 1e92964f4..d619d1669 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -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); }); diff --git a/openslides/motions/static/templates/motions/motion-list.html b/openslides/motions/static/templates/motions/motion-list.html index d9665c088..20735c70c 100644 --- a/openslides/motions/static/templates/motions/motion-list.html +++ b/openslides/motions/static/templates/motions/motion-list.html @@ -17,10 +17,6 @@ Import - - - PDF -

Motions

@@ -28,7 +24,7 @@
-
+
-
-
-
-
-
-
- -
-
- +
-
-
-
- - -
-
+
@@ -85,194 +82,377 @@ {{ motions.length }} {{ "motions" | translate }}, {{(motions|filter:{selected:true}).length}} {{ "selected" | translate }}
- - - - - - - - -
- - - - - - Item - - - - - Identifier - - +
+
+
+ +
+
- -
- Title - - - - - - Submitters - - - - - - Category - - - - - - State - - -
- - - - - - - - - - {{ motion.agenda_item.item_number }} - - - {{ motion.identifier }} - - - - {{ motion.getTitle() }} - + + + + Filter + + + + + State + + + + + + + + Category + + + + + + + + Tag + + + + + + + + Sort + + + + + + + + + + + + + + + + + + {{ state.name | translate }} + + + + + + {{ category.name }} + + + + + {{ tag.name }} -
- - Origin: - {{ motion.origin | limitTo:30 }}{{ motion.origin.length > 30 ? '...' : '' }} - -
-
- - Edit - - - | QuickEdit | - - - Delete - -
+
+
+ + - -
-
- {{ submitter.get_full_name() }}
+ + +
+ + +
+ +
+ +
+ + + +
+ +
+
+
+ {{ motion.identifier }}: +
+ +
+ + + + + +   + + +
+
+
+ +
+ + {{ motion.getTitle() }} + + + + + {{ motion.state.name | translate }} + + + + + + +
+ +
+ + by + + {{ submitter.get_full_name() }},, ... + + +
- -
- {{ motion.category.name }} + + + + +
+
+ +
+ + + + + + + + + + {{ motion.category.name }} + + + + + +
+
+ + + + + + + + + + {{ tag.name }}, + + + + + + +
+
+ + {{ motion.origin | limitTo:25 }}{{ motion.origin.length > 25 ? '...' : '' }} +
+
+
+
+
+ + {{ motion.supporters.length }} + +
+
+
+
{{ motion.agenda_item.getItemNumberWithAncestors() }}
+
+
+ - -
- - {{ motion.state.name | translate }} - - - - -

{{ motion.getTitle() }} QuickEdit

- - {{ alert.msg }} - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
-
- - -
-
-
-
-   - - Edit motion ... -
-
-
+
+