Several template fixes and clean up

- Use ng-cloak for hide template parts while loading.
- Set html lang attribute dynamically (Fixes #1546)
- Clean up: Rename 'dashboard' to 'home'.
- Show duration of speech in minutes. (Fixes #1882)
- Save agenda specific stuff for customslides. (Fixes #1887)
- Remove title from QuickEdit from.
- Checkbox for item.closed is now visible for manager only.
- Agenda list view: Show list of speakers link also for normal users.
- Improve slide templates: Show agenda item number and subtitle.
- Fixed agenda title for motions and assignments.
  (Don't load motions and assignmetn in agenda app.)
- Added missing seach template.
This commit is contained in:
Emanuel Schuetze 2016-01-25 21:22:22 +01:00
parent 741cae028c
commit 23503eb4ba
35 changed files with 345 additions and 146 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
# General
*.pyc
*.swp
*.swo
*~
# Virtual Environment

View File

@ -299,6 +299,17 @@ class Item(RESTModelMixin, models.Model):
raise NotImplementedError('You have to provide a get_agenda_title '
'method on your related model.')
@property
def list_view_title(self):
"""
Return get_agenda_list_view_title() from the content_object.
"""
try:
return self.content_object.get_agenda_list_view_title()
except AttributeError:
raise NotImplementedError('You have to provide a get_agenda_list_view_title '
'method on your related model.')
def is_hidden(self):
"""
Returns True if the type of this object itself is a hidden item or any

View File

@ -53,6 +53,7 @@ class ItemSerializer(ModelSerializer):
'id',
'item_number',
'title',
'list_view_title',
'comment',
'closed',
'type',

View File

@ -48,12 +48,24 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
try {
title = this.getContentObject().getAgendaTitle();
} catch (e) {
// Only use this.title when the content object is not
// in the DS store.
// when the content object is not in the DS store.
title = this.title;
}
if (this.getContentResource().agendaSupplement) {
title = gettextCatalog.getString(this.getContentResource().agendaSupplement) + ' ' + title;
if (this.item_number) {
title = this.item_number + ' · ' + title;
}
return title;
},
getAgendaTitle: function () {
return this.title;
},
getListViewTitle: function () {
var title;
try {
title = this.getContentObject().getAgendaListViewTitle();
} catch (e) {
// when the content object is not in the DS store
title = this.list_view_title;
}
if (this.item_number) {
title = this.item_number + ' · ' + title;

View File

@ -280,6 +280,14 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
$scope.alert = { type: 'danger', msg: data.detail, show: true };
});
};
// gets speech duration of selected speaker in seconds
$scope.getDuration = function (speaker) {
var beginTimestamp = new Date(speaker.begin_time).getTime()
var endTimestamp = new Date(speaker.end_time).getTime()
// calculate duration in seconds
return Math.floor((endTimestamp - beginTimestamp) / 1000);
}
// save reordered list of speakers
$scope.treeOptions = {
dropped: function (event) {

View File

@ -7,7 +7,7 @@
</a>
<a href="" ng-click="open(item)" class="btn btn-sm btn-default">
<i class="fa fa-angle-double-left fa-lg"></i>
{{ item.getContentResource().verboseName }}
{{ item.getContentResource().verboseName | translate }}
</a>
<!-- project list of speakers -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
@ -22,7 +22,7 @@
ng-click="item.project()"
title="{{ 'Project item' | translate }}">
<i class="fa fa-video-camera"></i>
{{ item.getContentResource().verboseName }}
{{ item.getContentResource().verboseName | translate }}
</a>
</div>
<h1>{{ item.getTitle() }}</h1>
@ -68,7 +68,7 @@
<div class="spacer">
<h3 translate>Last speakers</h3>
<button ng-click="showOldSpeakers = !showOldSpeakers"
class="btn btn-xs btn-default">
class="btn btn-sm btn-default">
<translate ng-if="!showOldSpeakers">Show</translate>
<translate ng-if="showOldSpeakers">Hide</translate>
</button>
@ -77,8 +77,9 @@
<li ng-repeat="speaker in item.speakers | filter: {end_time: '!!'}">
{{ speaker.user.get_full_name() }}
<small class="grey">
[{{ speaker.begin_time | date:'yyyy-MM-dd HH:mm:ss' }}
{{ speaker.end_time | date:'yyyy-MM-dd HH:mm:ss' }}]
{{ getDuration(speaker) | osSecondsToTime }} <translate>minutes</translate>
(<translate>Start time</translate>:
{{ speaker.begin_time | date:'yyyy-MM-dd HH:mm:ss' }})
</small>
<button os-perms="agenda.can_manage" ng-click="removeSpeaker(speaker.id)"
class="btn btn-default btn-xs" title="{{ 'Remove' | translate }}">
@ -94,11 +95,11 @@
filter: {end_time: null, begin_time: '!!'}">
{{ speaker.user.get_full_name() }}
<button os-perms="agenda.can_manage" ng-click="endSpeech()"
class="btn btn-default btn-xs" title="{{ 'End speech' | translate }}">
<i class="fa fa-microphone-slash"></i>
class="btn btn-default btn-sm" title="{{ 'End speech' | translate }}">
<i class="fa fa-microphone-slash"></i> <translate>Stop</translate>
</button>
<button os-perms="agenda.can_manage" ng-click="removeSpeaker(speaker.id)"
class="btn btn-default btn-xs" title="{{ 'Remove' | translate }}">
class="btn btn-default btn-sm" title="{{ 'Remove' | translate }}">
<i class="fa fa-times"></i>
</button>
</strong>
@ -113,11 +114,11 @@
{{ $index + 1 }}.
{{ speaker.user.get_full_name() }}
<button os-perms="agenda.can_manage" ng-click="beginSpeech(speaker.id)"
class="btn btn-default btn-xs" title="{{ 'Begin speech' | translate }}">
<i class="fa fa-microphone"></i>
class="btn btn-default btn-sm" title="{{ 'Begin speech' | translate }}">
<i class="fa fa-microphone"></i> <translate>Start</translate>
</button>
<button os-perms="agenda.can_manage" ng-click="removeSpeaker(speaker.id)"
class="btn btn-default btn-xs" title="{{ 'Remove' | translate }}">
class="btn btn-default btn-sm" title="{{ 'Remove' | translate }}">
<i class="fa fa-times"></i>
</button>
</ol>

View File

@ -28,8 +28,8 @@
<div class="details">
<div class="row">
<div class="col-sm-6">
<form class="form-inline">
<div class="col-sm-7">
<div class="form-inline">
<!-- delete mode -->
<button os-perms="agenda.can_manage" class="btn"
ng-class="$parent.isDeleteMode ? 'btn-primary' : 'btn-default'"
@ -37,10 +37,9 @@
<i class="fa fa-check-square-o"></i>
<translate>Select ...</translate>
</button>
<div class="form-group">
<!-- project agenda button -->
<a os-perms="core.can_manage_projector"
class="btn btn-default form-control"
class="btn btn-default"
title="{{ 'Project agenda' | translate }}"
ng-click="projectAgenda()"
ng-class="{ 'btn-primary': isAgendaProjected() }">
@ -49,15 +48,14 @@
</a>
<!-- auto numbering button -->
<a os-perms="core.can_manage_projector"
class="btn btn-default form-control"
class="btn btn-default"
ng-click="autoNumbering()">
<i class="fa fa-sort-numeric-asc"></i>
<translate>Number agenda</translate>
<translate>Numbering</translate>
</a>
</div>
</form>
</div>
<div class="col-sm-6">
<div class="col-sm-5">
<div class="form-inline text-right">
<div class="form-group">
<div class="input-group">
@ -138,15 +136,16 @@
style="padding-left: calc(8px + {{ item.parentCount }}*15px)">
<strong>
<a href="" ng-click="open(item)">
{{ item.getTitle() }}
{{ item.getListViewTitle() }}
</a>
</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_manage" class="hoverActions" ng-class="{'hiddenDiv': !item.hover}">
<a ui-sref="agenda.item.detail({id: item.id})" translate>List of speakers</a> |
<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 href="" ng-click="editDialog(item)" translate>Edit</a> |
<a href="" ng-click="item.quickEdit=true" translate>QuickEdit</a>
<span ng-if="item.content_object.collection == 'core/customslide'"> |
@ -155,12 +154,16 @@
<b>{{ item.getTitle() }}</b>"
ng-bootbox-confirm-action="deleteRelatedItem(item)" translate>Delete</a>
</span>
</span>
</div>
<td ng-show="!item.quickEdit" os-perms="agenda.can_see_hidden_items" class="optional">
{{ item.duration }}
<span ng-if="item.duration" translate-comment="'h' means time in hours" translate>h</span>
<td ng-if="!item.quickEdit">
<input type="checkbox" ng-model="item.closed" ng-change="save(item.id);">
<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.id);">
<!-- quickEdit columns -->
<td ng-show="item.quickEdit" os-perms="agenda.can_manage" colspan="3">
<form ng-submit="save(item)">
@ -170,8 +173,8 @@
</uib-alert>
<div class="row">
<div class="col-xs-6">
<label for="inputTitle" translate>Title</label>
<input type="text" ng-model="item.title" class="form-control input-sm" id="inputTitle">
<label for="inputItemNumber" translate>Item number</label>
<input type="text" ng-model="item.item_number" class="form-control input-sm" id="inputItemNumber">
</div>
<div class="col-xs-6">
<label for="inputComment" translate>Comment</label>
@ -180,25 +183,15 @@
</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">
<!-- 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" class="form-control input-sm" id="inputDuration">
</div>
</div>
<div class="row">
<div class="col-xs-6">
<label>
<!-- item type: AGENDA_ITEM = 1, HIDDEN_ITEM = 2 -->
<input type="checkbox" ng-model="item.type" ng-true-value="2" ng-false-value="1">
<translate> Hidden item</translate>
</label>
</div>
<div class="col-xs-6">
</div>
</div>
<div class="spacer">
<button ng-click="item.quickEdit=false" class="btn btn-default pull-left" translate>
Cancel

View File

@ -5,7 +5,7 @@
<!-- item type: AGENDA_ITEM = 1, HIDDEN_ITEM = 2 -->
<p ng-repeat="item in items | filter: {type: 1}" ng-class="{ 'spacer-top': !item.parent_id }">
<span ng-repeat="n in [].constructor(item.parentCount) track by $index">&nbsp;&nbsp;</span>
{{ item.getTitle() }}
{{ item.getListViewTitle() }}
<i ng-if="item.closed" class="fa fa-check"></i>
</p>
</div>

View File

@ -3,6 +3,7 @@ from collections import OrderedDict
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.agenda.models import Item, Speaker
@ -268,6 +269,15 @@ class Assignment(RESTModelMixin, models.Model):
def get_agenda_title(self):
return str(self)
def get_agenda_list_view_title(self):
"""
Return a title string for the agenda list view.
Contains agenda item number, title and assignment verbose name.
Note: It has to be the same return value like in JavaScript.
"""
return '%s (%s)' % (self.title, _(self._meta.verbose_name))
@property
def agenda_item(self):
"""

View File

@ -73,18 +73,21 @@ angular.module('OpenSlidesApp.assignments', [])
'AssignmentPoll',
'jsDataModel',
'gettext',
function ($http, DS, AssignmentRelatedUser, AssignmentPoll, jsDataModel, gettext) {
'gettextCatalog',
function ($http, DS, AssignmentRelatedUser, AssignmentPoll, jsDataModel, gettext, gettextCatalog) {
var name = 'assignments/assignment';
var phases;
return DS.defineResource({
name: name,
useClass: jsDataModel,
verboseName: gettext('Election'),
agendaSupplement: gettext('Election'),
phases: phases,
getPhases: function () {
if (!this.phases) {
this.phases = $http({ 'method': 'OPTIONS', 'url': '/rest/assignments/assignment/' });
this.phases = $http({ 'method': 'OPTIONS', 'url': '/rest/assignments/assignment/' })
.then(function(phases) {
return phases.data.actions.POST.phase.choices;
});
}
return this.phases;
},

View File

@ -22,8 +22,16 @@ angular.module('OpenSlidesApp.assignments.projector', ['OpenSlidesApp.assignment
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
var id = $scope.element.id;
Assignment.find(id);
// load assignemt object and related agenda item
Assignment.find(id).then(function(assignment) {
Assignment.loadRelations(assignment, 'agenda_item');
});
Assignment.bindOne(id, $scope, 'assignment');
Assignment.getPhases().then(function(phases) {
$scope.phases = phases;
});
// load all users
User.findAll();
User.bindAll({}, $scope, 'users');
}

View File

@ -36,19 +36,21 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
assignments: function(Assignment) {
return Assignment.findAll();
},
items: function(Agenda) {
return Agenda.findAll();
},
phases: function(Assignment) {
return Assignment.getPhases();
},
users: function(User) {
return User.findAll();
},
}
}
})
.state('assignments.assignment.detail', {
controller: 'AssignmentDetailCtrl',
resolve: {
assignment: function(Assignment, $stateParams) {
return Assignment.find($stateParams.id);
return Assignment.find($stateParams.id).then(function(assignment) {
return Assignment.loadRelations(assignment, 'agenda_item');
});
},
users: function(User) {
return User.findAll();
@ -172,8 +174,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
'phases',
function($scope, ngDialog, AssignmentForm, Assignment, phases) {
Assignment.bindAll({}, $scope, 'assignments');
// get all item types via OPTIONS request
$scope.phases = phases.data.actions.POST.phase.choices;
$scope.phases = phases;
$scope.alert = {};
// setup table sorting
@ -266,8 +267,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
Assignment.bindOne(assignment.id, $scope, 'assignment');
Assignment.loadRelations(assignment, 'agenda_item');
$scope.candidateSelectBox = {};
// get all item types via OPTIONS request
$scope.phases = phases.data.actions.POST.phase.choices;
$scope.phases = phases;
$scope.alert = {};
// open edit dialog

View File

@ -28,13 +28,9 @@
<i class="fa fa-pencil"></i>
</a>
</div>
<h1>{{ assignment.title }}</h1>
<h1>{{ assignment.agenda_item.getTitle() }}</h1>
<h2>
<translate>Election</translate>
<span ng-if="assignment.agenda_item.item_number">
&ndash;
<translate>Agenda</translate>: {{ assignment.agenda_item.item_number }}
</span>
</h2>
</div>
</div>

View File

@ -80,19 +80,33 @@
<tr>
<!-- projector column -->
<th ng-show="!isDeleteMode" os-perms="core.can_manage_projector" class="minimum">
<!-- delete selection column -->
<th ng-show="isDeleteMode" os-perms="assignments.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">
<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>
<!-- title column -->
<th ng-click="toggleSort('title')" class="sortable">
<translate>Title</translate>
<i class="pull-right fa" ng-show="sortColumn === 'title' && header.sortable != false"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i>
<!-- candicates / posts column -->
<th ng-click="toggleSort('open_posts')" class="sortable optional">
<translate>Candidates</translate> &middot; <translate>Posts</translate>
<i class="pull-right fa" ng-show="sortColumn === 'open_posts' && header.sortable != false"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i>
<!-- phase column -->
<th ng-click="toggleSort('phase')" class="sortable optional">
<translate>Phase</translate>
<i class="pull-right fa" ng-show="sortColumn === 'phase' && header.sortable != false"
@ -103,7 +117,8 @@
filter: {phase: phaseFilter} | orderBy: sortColumn:reverse)"
class="animate-item"
ng-class="{ 'activeline': assignment.isProjected(), 'selected': assignment.selected }">
<!-- projector column -->
<!-- projector -->
<td ng-show="!isDeleteMode" os-perms="core.can_manage_projector">
<a class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': assignment.isProjected() }"
@ -111,10 +126,16 @@
title="{{ 'Project election' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<!-- delete selection column -->
<!-- delete selection -->
<td ng-show="isDeleteMode" os-perms="assignments.can_manage" class="deleteColumn">
<input type="checkbox" ng-model="assignment.selected">
<!-- assignment data colums -->
<!-- agenda item number -->
<td ng-if="!assignment.quickEdit">
{{ assignment.agenda_item.item_number }}
<!-- title -->
<td ng-if="!assignment.quickEdit" ng-mouseover="assignment.hover=true" ng-mouseleave="assignment.hover=false">
<strong><a ui-sref="assignments.assignment.detail({id: assignment.id})">{{ assignment.title }}</a></strong>
<div os-perms="assignments.can_manage" class="hoverActions" ng-class="{'hiddenDiv': !assignment.hover}">
@ -125,16 +146,21 @@
<b>{{ assignment.title }}</b>"
ng-bootbox-confirm-action="delete(assignment)" translate>Delete</a>
</div>
<!-- candidates / posts -->
<td ng-if="!assignment.quickEdit" class="optional">
<span class="badge">{{ assignment.assignment_related_users.length }}</span>
/
<span class="badge">{{ assignment.open_posts }}</span>
<!-- phase -->
<td ng-if="!assignment.quickEdit" class="optional">
<span class="label" ng-class="{'label-primary': assignment.phase == 0,
'label-warning': assignment.phase == 1,
'label-success': assignment.phase == 2 }">
{{ phases[assignment.phase].display_name }}
</span>
<!-- quickEdit columns -->
<td ng-if="assignment.quickEdit" colspan="3">
<h4>{{ assignment.title }} <span class="text-muted">&ndash; Quick Edit</span></h4>

View File

@ -1,9 +1,28 @@
<div ng-controller="SlideAssignmentCtrl" class="content scrollcontent">
<h1>{{ assignment.title }}</h1>
<div id="sidebox">
<!-- Phase -->
<h3 translate>State</h3>
{{ phases[assignment.phase].display_name }}
<!-- Posts -->
<h3 translate>Posts</h3>
{{ assignment.open_posts }}
</div>
<!-- Title -->
<div id="title">
<h1>{{ assignment.agenda_item.getTitle() }}</h1>
<h2>
<translate>Election</translate>
</h2>
</div>
<!-- Description -->
<div class="white-space-pre-line">{{ assignment.description }}</div>
<!-- candidates -->
<h2 translate>Candidates</h2>
<!-- Candidates -->
<h3 translate>Candidates</h3>
<ol>
<li ng-repeat="related_user in assignment.assignment_related_users">
{{ related_user.user.get_full_name() }}

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0002_customslide_attachments'),
]
operations = [
migrations.AlterModelOptions(
name='projector',
options={'default_permissions': (), 'permissions': (
('can_see_projector', 'Can see the projector'),
('can_manage_projector', 'Can manage the projector'),
('can_see_frontpage', 'Can see the front page'))},
),
]

View File

@ -66,7 +66,7 @@ class Projector(RESTModelMixin, models.Model):
permissions = (
('can_see_projector', ugettext_noop('Can see the projector')),
('can_manage_projector', ugettext_noop('Can manage the projector')),
('can_see_dashboard', ugettext_noop('Can see the dashboard')))
('can_see_frontpage', ugettext_noop('Can see the front page')))
@property
def elements(self):
@ -159,6 +159,9 @@ class CustomSlide(RESTModelMixin, models.Model):
def get_agenda_title(self):
return self.title
def get_agenda_list_view_title(self):
return self.title
def get_search_index_string(self):
"""
Returns a string that can be indexed for the search.

View File

@ -102,7 +102,7 @@ h2 {
}
h3 {
color: #222;
margin-bottom: 2px
margin-bottom: 10px
}
#title {
width: calc(100% - 230px);

View File

@ -89,7 +89,9 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
var id = $scope.element.id;
Customslide.find(id);
Customslide.find(id).then(function(customslide) {
Customslide.loadRelations(customslide, 'agenda_item');
});
Customslide.bindOne(id, $scope, 'customslide');
}
])

View File

@ -68,7 +68,7 @@ angular.module('OpenSlidesApp.core.site', [
'gettext',
function (mainMenuProvider, gettext) {
mainMenuProvider.register({
'ui_sref': 'dashboard',
'ui_sref': 'home',
'img_class': 'home',
'title': gettext('Home'),
'weight': 100,
@ -193,9 +193,9 @@ angular.module('OpenSlidesApp.core.site', [
function($stateProvider, $locationProvider) {
// Core urls
$stateProvider
.state('dashboard', {
.state('home', {
url: '/',
templateUrl: 'static/templates/dashboard.html'
templateUrl: 'static/templates/home.html'
})
.state('projector', {
url: '/projector',
@ -556,11 +556,11 @@ angular.module('OpenSlidesApp.core.site', [
}
},
{
key: 'showOnAgenda',
key: 'showAsAgendaItem',
type: 'checkbox',
templateOptions: {
label: gettextCatalog.getString('Show on agenda'),
description: gettextCatalog.getString('If deactivated it appears as internal item.')
label: gettextCatalog.getString('Show as agenda item'),
description: gettextCatalog.getString('If deactivated it appears as internal item on agenda.')
}
},
];
@ -791,6 +791,8 @@ angular.module('OpenSlidesApp.core.site', [
'Agenda',
function($scope, $state, Customslide, CustomslideForm, Agenda) {
$scope.customslide = {};
$scope.model = {};
$scope.model.showAsAgendaItem = true;
// get all form fields
$scope.formFields = CustomslideForm.getFormFields();
@ -798,14 +800,16 @@ angular.module('OpenSlidesApp.core.site', [
$scope.save = function (customslide) {
Customslide.create(customslide).then(
function(success) {
// show as agenda item
if (customslide.showOnAgenda) {
// find related agenda item
Agenda.find(success.agenda_item_id).then(function(item) {
// set item type to AGENDA_ITEM = 1 (default is HIDDEN_ITEM = 2)
item.type = 1;
// check form element and set item type (AGENDA_ITEM = 1, HIDDEN_ITEM = 2)
var type = customslide.showAsAgendaItem ? 1 : 2;
// save only if agenda item type is modified
if (item.type != type) {
item.type = type;
Agenda.save(item);
});
}
});
$scope.closeThisDialog();
}
);
@ -818,14 +822,21 @@ angular.module('OpenSlidesApp.core.site', [
'$state',
'Customslide',
'CustomslideForm',
'Agenda',
'customslide',
function($scope, $state, Customslide, CustomslideForm, customslide) {
function($scope, $state, Customslide, CustomslideForm, Agenda, customslide) {
$scope.alert = {};
// set initial values for form model by create deep copy of customslide object
// so list/detail view is not updated while editing
$scope.model = angular.copy(customslide);
// get all form fields
$scope.formFields = CustomslideForm.getFormFields();
for (var i = 0; i < $scope.formFields.length; i++) {
if ($scope.formFields[i].key == "showAsAgendaItem") {
// get state from agenda item (hidden/internal or agenda item)
$scope.formFields[i].defaultValue = !customslide.agenda_item.is_hidden;
}
}
// save form
$scope.save = function (customslide) {
@ -834,6 +845,12 @@ angular.module('OpenSlidesApp.core.site', [
// save change customslide object on server
Customslide.save(customslide).then(
function(success) {
// save agenda specific stuff
var type = customslide.showAsAgendaItem ? 1 : 2;
if (customslide.agenda_item.type != type) {
customslide.agenda_item.type = type;
Agenda.save(customslide.agenda_item);
}
$scope.closeThisDialog();
},
function (error) {

View File

@ -8,7 +8,7 @@
<i class="fa toggle-icon" ng-class="isLiveViewClosed ? 'fa-angle-up' : 'fa-angle-down'"></i>
<h4 translate>Live view</h4>
</a>
<div uib-collapse="isLiveViewClosed">
<div uib-collapse="isLiveViewClosed" ng-cloak>
<a ui-sref="projector" target="_blank">
<div id="iframewrapper">
<iframe id="iframe" src="/projector" frameborder="0"></iframe>
@ -61,7 +61,7 @@
<i class="fa toggle-icon" ng-class="isCountdowns ? 'fa-angle-up' : 'fa-angle-down'"></i>
<h4 translate>Countdowns</h4>
</a>
<div uib-collapse="!isCountdowns">
<div uib-collapse="!isCountdowns" ng-cloak>
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="{{countdown.uuid}}"
class="countdown panel panel-default">
<div class="panel-heading">
@ -153,7 +153,7 @@
<i class="fa toggle-icon" ng-class="isMessages ? 'fa-angle-up' : 'fa-angle-down'"></i>
<h4 translate>Messages</h4>
</a>
<div uib-collapse="!isMessages">
<div uib-collapse="!isMessages" ng-cloak>
<div ng-repeat="message in messages | orderBy: 'index'" id="{{message.uuid}}" class="message panel panel-default">
<div class="panel-body"
ng-class="{ 'projected': message.visible }">

View File

@ -1,4 +1,4 @@
<div ng-controller="SlideCustomSlideCtrl" class="content scrollcontent">
<h1>{{ customslide.title }}</h1>
<h1>{{ customslide.agenda_item.getTitle() }}</h1>
<div ng-bind-html="customslide.text"></div>
</div>

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="no-js"> <!-- TODO: make lang dynamic -->
<html ng-controller="LanguageCtrl" lang="{{ selectedLanguage[0].code }}" class="no-js">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<base href="/">
@ -11,17 +11,17 @@
<link rel="icon" href="/static/img/favicon.png">
<script src="static/js/openslides-libs.js"></script>
<script src="static/ckeditor/ckeditor.js"></script>
<div id="wrapper">
<div id="wrapper" ng-cloak>
<!-- Header -->
<div id="header">
<div class="containerOS">
<!-- Logo -->
<div class="title">
<a ui-sref="dashboard">
<a ui-sref="home">
<img src="/static/img/openslides-logo-dark.png" alt="Logo" height="35">
</a>
<!-- TODO: <span class="navbar-text optional">{{ config('general_event_name') }}</span>-->
</div>
<!-- user specific header (chat, user settings / login, language)-->
@ -104,7 +104,7 @@
</div>
<!-- language switcher -->
<span ng-controller="LanguageCtrl" uib-dropdown>
<span uib-dropdown>
| <a href class="headerlink" uib-dropdown-toggle>
<i class="fa fa-flag"></i>
{{ selectedLanguage[0].name | translate }}
@ -157,7 +157,7 @@
<div class="containerOS">
<div class="col1" ng-class="isProjectorSidebar ? 'min' : 'max'">
<!-- dynamic views -->
<div ui-view></div>
<div ui-view ng-cloak></div>
<!-- footer -->
<div id="footer">
&copy; Copyright by <a href="http://www.openslides.org" target="_blank">OpenSlides</a> |

View File

@ -0,0 +1,23 @@
<div class="header">
<div class="title">
<h1 translate>Search results</h1>
</div>
</div>
<div class="details">
<form class="input-group" ng-submit="search(query)">
<input type="text" ng-model="query" class="form-control">
<span class="input-group-btn">
<button type="submit" class="btn btn-default" translate>Search</button>
</span>
</form>
<div class="searchresults spacer-top-lg">
<ol ng-show="results">
<li ng-repeat="result in results">
<a ui-sref="{{ result.urlState }}({{ result.urlParam }})">{{ result.getSearchResultName() }}</a><br>
<span class="grey">{{ result.getSearchResultSubtitle() | translate }}</span>
</ol>
<p ng-show="!results" translate>No results.</p>
</div>
</div>

View File

@ -125,13 +125,9 @@ class Motion(RESTModelMixin, models.Model):
def __str__(self):
"""
Return a human readable name of this motion.
Return the title of this motion.
"""
if self.identifier:
string = '%s: %s' % (self.identifier, self.title)
else:
string = self.title
return string
return self.title
# TODO: Use transaction
def save(self, use_version=None, *args, **kwargs):
@ -464,11 +460,25 @@ class Motion(RESTModelMixin, models.Model):
def get_agenda_title(self):
"""
Return a title for the agenda.
Return a simple title string for the agenda.
Contains only agenda item number and title.
"""
# There has to be a function with the same return value in javascript.
return str(self)
def get_agenda_list_view_title(self):
"""
Return a title string for the agenda list view.
Contains agenda item number, title and motion identifier.
Note: It has to be the same return value like in JavaScript.
"""
if self.identifier:
string = '%s (%s %s)' % (self.title, _(self._meta.verbose_name), self.identifier)
else:
string = '%s (%s)' % (self.title, _(self._meta.verbose_name))
return string
@property
def agenda_item(self):
"""

View File

@ -161,15 +161,15 @@ angular.module('OpenSlidesApp.motions', ['OpenSlidesApp.users'])
'MotionPoll',
'jsDataModel',
'gettext',
'gettextCatalog',
'operator',
'Config',
function(DS, MotionPoll, jsDataModel, gettext, operator, Config) {
function(DS, MotionPoll, jsDataModel, gettext, gettextCatalog, operator, Config) {
var name = 'motions/motion';
return DS.defineResource({
name: name,
useClass: jsDataModel,
verboseName: gettext('Motion'),
agendaSupplement: gettext('Motion'),
methods: {
getResourceName: function () {
return name;
@ -195,16 +195,9 @@ angular.module('OpenSlidesApp.motions', ['OpenSlidesApp.users'])
getReason: function (versionId) {
return this.getVersion(versionId).reason;
},
getAgendaTitle: function () {
var value = '';
if (this.identifier) {
value = ' ' + this.identifier;
}
return "Motion " + value + ': ' + this.getTitle();
},
// link name which is shown in search result
getSearchResultName: function () {
return this.getAgendaTitle();
return this.getTitle();
},
// subtitle of search result
getSearchResultSubtitle: function () {

View File

@ -22,9 +22,16 @@ angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions'])
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
var id = $scope.element.id;
Motion.find(id);
User.findAll();
// load motion object and related agenda item
Motion.find(id).then(function(motion) {
Motion.loadRelations(motion, 'agenda_item');
});
Motion.bindOne(id, $scope, 'motion');
// load all users
User.findAll();
User.bindAll({}, $scope, 'users');
}
]);

View File

@ -34,7 +34,12 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
.state('motions.motion.list', {
resolve: {
motions: function(Motion) {
return Motion.findAll();
return Motion.findAll().then(function(motions) {
angular.forEach(motions, function(motion) {
Motion.loadRelations(motion, 'agenda_item');
});
});
},
categories: function(Category) {
return Category.findAll();
@ -53,7 +58,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
.state('motions.motion.detail', {
resolve: {
motion: function(Motion, $stateParams) {
return Motion.find($stateParams.id);
return Motion.find($stateParams.id).then(function(motion) {
return Motion.loadRelations(motion, 'agenda_item');
});
},
categories: function(Category) {
return Category.findAll();
@ -85,7 +92,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
resolve: {
motion: function() {
return Motion.find($stateParams.id).then(function(motion) {
return Motion.loadRelations(motion, 'agenda_item');
Motion.loadRelations(motion, 'agenda_item');
});
},
},

View File

@ -28,7 +28,7 @@
<i class="fa fa-pencil"></i>
</a>
</div>
<h1>{{ motion.getTitle(version) }}</h1>
<h1>{{ motion.agenda_item.getTitle() }}</h1>
<h2>
<translate>Motion</translate> {{ motion.identifier }}
<span ng-if="motion.versions.length > 1" >| Version {{ motion.getVersion(version).version_number }}</span>
@ -36,10 +36,6 @@
<i class="fa fa-exclamation-triangle"></i>
<translate>This version is not permitted.</translate>
</span>
<span ng-if="motion.agenda_item.item_number">
&ndash;
<translate>Agenda</translate>: {{ motion.agenda_item.item_number }}
</span>
</h2>
</div>
</div>

View File

@ -89,26 +89,42 @@
<!-- 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">
<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>
<!-- 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">
<translate>State</translate>
<i class="pull-right fa" ng-show="sortColumn === 'state.name' && header.sortable != false"
@ -119,7 +135,8 @@
filter: {state_id: stateFilter} | orderBy: sortColumn:reverse)"
class="animate-item"
ng-class="{ 'activeline': motion.isProjected(), 'selected': motion.selected }">
<!-- projector column -->
<!-- projector -->
<td ng-show="!isDeleteMode" os-perms="core.can_manage_projector">
<a class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': motion.isProjected() }"
@ -127,11 +144,18 @@
title="{{ 'Project motion' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<!-- delete selection column -->
<!-- delete selection -->
<td ng-show="isDeleteMode" os-perms="motions.can_manage" class="deleteColumn">
<input type="checkbox" ng-model="motion.selected">
<!-- motion data colums -->
<!-- agenda item number -->
<td ng-if="!motion.quickEdit">{{ 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">
@ -151,16 +175,23 @@
ng-bootbox-confirm-action="delete(motion)" translate>Delete</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">
<span class="label" ng-class="'label-'+motion.state.css_class">
{{ motion.state.name | translate }}
</span>
<!-- quickEdit columns -->
<td ng-if="motion.quickEdit && motion.isAllowed('quickedit')" colspan="5">
<h4>{{ motion.getTitle() }} <span class="text-muted">&ndash; <translate>QuickEdit</translate></span></h4>

View File

@ -14,7 +14,7 @@
<!-- Title -->
<div id="title">
<h1>{{ motion.getTitle() }}</h1>
<h1>{{ motion.agenda_item.getTitle() }}</h1>
<h2>
<translate>Motion</translate> {{ motion.identifier }}
<span ng-if="motion.versions.length > 1" >| Version {{ motion.getVersion().version_number }}</span>
@ -25,6 +25,6 @@
<div ng-bind-html="motion.getText()"></div>
<!-- Reason -->
<h2 ng-if="motion.getReason()" translate>Reason</h2>
<h3 ng-if="motion.getReason()" translate>Reason</h3>
<div ng-bind-html="motion.getReason()"></div>
</div>

View File

@ -113,7 +113,7 @@ def create_builtin_groups_and_admin(**kwargs):
'core.can_manage_config',
'core.can_manage_projector',
'core.can_manage_tags',
'core.can_see_dashboard',
'core.can_see_frontpage',
'core.can_see_projector',
'core.can_use_chat',
'mediafiles.can_manage',
@ -143,7 +143,7 @@ def create_builtin_groups_and_admin(**kwargs):
permission_dict['agenda.can_see'],
permission_dict['agenda.can_see_hidden_items'],
permission_dict['assignments.can_see'],
permission_dict['core.can_see_dashboard'],
permission_dict['core.can_see_frontpage'],
permission_dict['core.can_see_projector'],
permission_dict['mediafiles.can_see'],
permission_dict['motions.can_see'],

View File

@ -130,7 +130,7 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
closeByEscape: $stateParams.guest_enabled,
closeByDocument: $stateParams.guest_enabled,
preCloseCallback: function() {
$state.go('dashboard');
$state.go('home');
return true;
}
});

View File

@ -128,7 +128,7 @@ class ModelTest(TestCase):
motion.active_version = None
motion.save(update_fields=['active_version'])
# motion.__unicode__() raised an AttributeError
self.assertEqual(str(motion), 'test_identifier_VohT1hu9uhiSh6ooVBFS: test_title_Koowoh1ISheemeey1air')
self.assertEqual(str(motion), 'test_title_Koowoh1ISheemeey1air')
def test_is_amendment(self):
config['motions_amendments_enabled'] = True