Moved custom slides to own app topics for better app structure.

Renamed model to Topic. Added migrations file. Fixed #2402.
This commit is contained in:
Norman Jäckel 2016-09-18 22:14:24 +02:00
parent 53c4932171
commit cab53f0434
37 changed files with 804 additions and 633 deletions

View File

@ -18,6 +18,7 @@ Assignments:
Core: Core:
- Added support for big assemblies with lots of users. - Added support for big assemblies with lots of users.
- Added HTML support for messages on the projector. - Added HTML support for messages on the projector.
- Moved custom slides to own app "topics". Renamed it to "Topic".
Motions: Motions:
- Added origin field. - Added origin field.

View File

@ -68,10 +68,6 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
url: '/sort', url: '/sort',
controller: 'AgendaSortCtrl', controller: 'AgendaSortCtrl',
}) })
.state('agenda.item.import', {
url: '/import',
controller: 'AgendaImportCtrl',
})
.state('agenda.current-list-of-speakers', { .state('agenda.current-list-of-speakers', {
url: '/speakers', url: '/speakers',
controller: 'ListOfSpeakersViewCtrl', controller: 'ListOfSpeakersViewCtrl',
@ -100,10 +96,10 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
'operator', 'operator',
'ngDialog', 'ngDialog',
'Agenda', 'Agenda',
'CustomslideForm', 'TopicForm', // TODO: Remove this dependency. Use template hook for "New" and "Import" buttons.
'AgendaTree', 'AgendaTree',
'Projector', 'Projector',
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, CustomslideForm, AgendaTree, Projector) { function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm, AgendaTree, Projector) {
// Bind agenda tree to the scope // Bind agenda tree to the scope
$scope.$watch(function () { $scope.$watch(function () {
return Agenda.lastModified(); return Agenda.lastModified();
@ -125,11 +121,12 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
}; };
// check open permission // check open permission
// TODO: Use generic solution here.
$scope.isAllowedToSeeOpenLink = function (item) { $scope.isAllowedToSeeOpenLink = function (item) {
var collection = item.content_object.collection; var collection = item.content_object.collection;
switch (collection) { switch (collection) {
case 'core/customslide': case 'topics/topic':
return operator.hasPerms('core.can_manage_projector'); return operator.hasPerms('agenda.can_see');
case 'motions/motion': case 'motions/motion':
return operator.hasPerms('motions.can_see'); return operator.hasPerms('motions.can_see');
case 'assignments/assignment': case 'assignments/assignment':
@ -138,9 +135,9 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
return false; return false;
} }
}; };
// open new dialog // open dialog for new topics // TODO Remove this. Don't forget import button in template.
$scope.newDialog = function () { $scope.newDialog = function () {
ngDialog.open(CustomslideForm.getDialog()); ngDialog.open(TopicForm.getDialog());
}; };
// cancel QuickEdit mode // cancel QuickEdit mode
$scope.cancelQuickEdit = function (item) { $scope.cancelQuickEdit = function (item) {
@ -185,7 +182,7 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
}); });
} }
}; };
// delete selected items only if items are customslides // delete selected items
$scope.deleteMultiple = function () { $scope.deleteMultiple = function () {
angular.forEach($scope.items, function (item) { angular.forEach($scope.items, function (item) {
if (item.selected) { if (item.selected) {
@ -401,161 +398,16 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
} }
]) ])
.controller('AgendaImportCtrl', [
'$scope',
'gettext',
'Agenda',
'Customslide',
function($scope, gettext, Agenda, Customslide) {
// import from textarea
$scope.importByLine = function () {
if ($scope.itemlist) {
$scope.titleItems = $scope.itemlist[0].split("\n");
$scope.importcounter = 0;
$scope.titleItems.forEach(function(title, index) {
var item = {title: title};
// TODO: create all items in bulk mode
Customslide.create(item).then(
function(success) {
// find related agenda item
Agenda.find(success.agenda_item_id).then(function(item) {
// import all items as type AGENDA_ITEM = 1
item.type = 1;
item.weight = 1000 + index;
Agenda.save(item);
});
$scope.importcounter++;
}
);
});
}
};
// *** CSV import ***
// set initial data for csv import
$scope.items = [];
$scope.separator = ',';
$scope.encoding = 'UTF-8';
$scope.encodingOptions = ['UTF-8', 'ISO-8859-1'];
$scope.accept = '.csv, .txt';
$scope.csv = {
content: null,
header: true,
headerVisible: false,
separator: $scope.separator,
separatorVisible: false,
encoding: $scope.encoding,
encodingVisible: false,
accept: $scope.accept,
result: null
};
// set csv file encoding
$scope.setEncoding = function () {
$scope.csv.encoding = $scope.encoding;
};
// set csv file encoding
$scope.setSeparator = function () {
$scope.csv.separator = $scope.separator;
};
// detect if csv file is loaded
$scope.$watch('csv.result', function () {
$scope.items = [];
var quotionRe = /^"(.*)"$/;
angular.forEach($scope.csv.result, function (item, index) {
// title
if (item.title) {
item.title = item.title.replace(quotionRe, '$1');
}
if (!item.title) {
item.importerror = true;
item.title_error = gettext('Error: Title is required.');
}
// text
if (item.text) {
item.text = item.text.replace(quotionRe, '$1');
}
// duration
if (item.duration) {
item.duration = item.duration.replace(quotionRe, '$1');
}
// comment
if (item.comment) {
item.comment = item.comment.replace(quotionRe, '$1');
}
// is_hidden
if (item.is_hidden) {
item.is_hidden = item.is_hidden.replace(quotionRe, '$1');
if (item.is_hidden == '1') {
item.type = 2;
} else {
item.type = 1;
}
} else {
item.type = 1;
}
// set weight for right csv row order
// (Use 1000+ to protect existing items and prevent collision
// with new items which use weight 10000 as default.)
item.weight = 1000 + index;
$scope.items.push(item);
});
});
// import from csv file
$scope.import = function () {
$scope.csvImporting = true;
angular.forEach($scope.items, function (item) {
if (!item.importerror) {
Customslide.create(item).then(
function(success) {
item.imported = true;
// find related agenda item
Agenda.find(success.agenda_item_id).then(function(agendaItem) {
agendaItem.duration = item.duration;
agendaItem.comment = item.comment;
agendaItem.type = item.type;
agendaItem.weight = item.weight;
Agenda.save(agendaItem);
});
}
);
}
});
$scope.csvimported = true;
};
$scope.clear = function () {
$scope.csv.result = null;
};
// download CSV example file
$scope.downloadCSVExample = function () {
var element = document.getElementById('downloadLink');
var csvRows = [
// column header line
['title', 'text', 'duration', 'comment', 'is_hidden'],
// example entries
['Demo 1', 'Demo text 1', '1:00', 'test comment', ''],
['Break', '', '0:10', '', '1'],
['Demo 2', 'Demo text 2', '1:30', '', '']
];
var csvString = csvRows.join("%0A");
element.href = 'data:text/csv;charset=utf-8,' + csvString;
element.download = 'agenda-example.csv';
element.target = '_blank';
};
}
])
.controller('ListOfSpeakersViewCtrl', [ .controller('ListOfSpeakersViewCtrl', [
'$scope', '$scope',
'$state', '$state',
'$http', '$http',
'Projector', 'Projector',
'Assignment', 'Assignment', // TODO: Remove this after refactoring of data loading on start.
'Customslide', 'Topic', // TODO: Remove this after refactoring of data loading on start.
'Motion', 'Motion', // TODO: Remove this after refactoring of data loading on start.
'Agenda', 'Agenda',
function($scope, $state, $http, Projector, Assignment, Customslide, Motion, Agenda) { function($scope, $state, $http, Projector, Assignment, Topic, Motion, Agenda) {
$scope.$watch( $scope.$watch(
function() { function() {
return Projector.lastModified(1); return Projector.lastModified(1);
@ -572,10 +424,10 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
}); });
}); });
break; break;
case 'core/customslide': case 'topics/topic':
Customslide.find(element.id).then(function(customslide) { Topic.find(element.id).then(function(topic) {
Customslide.loadRelations(customslide, 'agenda_item').then(function() { Topic.loadRelations(topic, 'agenda_item').then(function() {
$scope.AgendaItem = customslide.agenda_item; $scope.AgendaItem = topic.agenda_item;
}); });
}); });
break; break;

View File

@ -9,7 +9,7 @@
<i class="fa fa-sitemap fa-lg"></i> <i class="fa fa-sitemap fa-lg"></i>
<translate>Sort agenda</translate> <translate>Sort agenda</translate>
</a> </a>
<a ui-sref="agenda.item.import" os-perms="agenda.can_manage" class="btn btn-default btn-sm"> <a ui-sref="topics.topic.import" os-perms="agenda.can_manage" class="btn btn-default btn-sm">
<i class="fa fa-download fa-lg"></i> <i class="fa fa-download fa-lg"></i>
<translate>Import</translate> <translate>Import</translate>
</a> </a>
@ -202,7 +202,7 @@
<span os-perms="!agenda.can_manage"> <span os-perms="!agenda.can_manage">
<i ng-if="item.closed" class="fa fa-check-square-o"></i> <i ng-if="item.closed" class="fa fa-check-square-o"></i>
</span> </span>
<input os-perms="agenda.can_manage" type="checkbox" ng-model="item.closed" ng-change="save(item.id);"> <input os-perms="agenda.can_manage" type="checkbox" ng-model="item.closed" ng-change="save(item);">
<!-- quickEdit columns --> <!-- quickEdit columns -->
<td ng-show="item.quickEdit" os-perms="agenda.can_manage" colspan="3"> <td ng-show="item.quickEdit" os-perms="agenda.can_manage" colspan="3">
<h4>{{ item.getTitle() }} <span class="text-muted">&ndash; QuickEdit</span></h4> <h4>{{ item.getTitle() }} <span class="text-muted">&ndash; QuickEdit</span></h4>

View File

@ -82,8 +82,8 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
// (from assignment controller use AssignmentForm factory instead to open dialog in front // (from assignment controller use AssignmentForm factory instead to open dialog in front
// of current view without redirect) // of current view without redirect)
.state('assignments.assignment.detail.update', { .state('assignments.assignment.detail.update', {
onEnter: ['$stateParams', '$state', 'ngDialog', 'Assignment', 'Agenda', onEnter: ['$stateParams', '$state', 'ngDialog', 'Assignment',
function($stateParams, $state, ngDialog, Assignment, Agenda) { function($stateParams, $state, ngDialog, Assignment) {
ngDialog.open({ ngDialog.open({
template: 'static/templates/assignments/assignment-form.html', template: 'static/templates/assignments/assignment-form.html',
controller: 'AssignmentUpdateCtrl', controller: 'AssignmentUpdateCtrl',

View File

@ -20,25 +20,6 @@ class ProjectorAccessPermissions(BaseAccessPermissions):
return ProjectorSerializer return ProjectorSerializer
class CustomSlideAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for CustomSlide and CustomSlideViewSet.
"""
def check_permissions(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('core.can_manage_projector')
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import CustomSlideSerializer
return CustomSlideSerializer
class TagAccessPermissions(BaseAccessPermissions): class TagAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Tag and TagViewSet. Access permissions container for Tag and TagViewSet.

View File

@ -24,7 +24,6 @@ class CoreAppConfig(AppConfig):
from .views import ( from .views import (
ChatMessageViewSet, ChatMessageViewSet,
ConfigViewSet, ConfigViewSet,
CustomSlideViewSet,
ProjectorViewSet, ProjectorViewSet,
TagViewSet, TagViewSet,
) )
@ -40,7 +39,6 @@ class CoreAppConfig(AppConfig):
# Register viewsets. # Register viewsets.
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet) router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet)
router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet) router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet)
router.register(self.get_model('CustomSlide').get_collection_string(), CustomSlideViewSet)
router.register(self.get_model('Tag').get_collection_string(), TagViewSet) router.register(self.get_model('Tag').get_collection_string(), TagViewSet)
router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config') router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config')

View File

@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-18 19:04
from __future__ import unicode_literals
from django.db import migrations, models
from openslides.utils.autoupdate import (
inform_changed_data_receiver,
inform_deleted_data_receiver,
)
def move_custom_slides_to_topics(apps, schema_editor):
# Disconnect autoupdate. We do not want to trigger it here.
models.signals.post_save.disconnect(dispatch_uid='inform_changed_data_receiver')
models.signals.post_save.disconnect(dispatch_uid='inform_deleted_data_receiver')
# We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version.
ContentType = apps.get_model('contenttypes', 'ContentType')
CustomSlide = apps.get_model('core', 'CustomSlide')
Item = apps.get_model('agenda', 'Item')
Topic = apps.get_model('topics', 'Topic')
# Copy data.
content_type_custom_slide = ContentType.objects.get_for_model(CustomSlide)
content_type_topic = ContentType.objects.get_for_model(Topic)
for custom_slide in CustomSlide.objects.all():
# This line does not create a new Item because this migration model has
# no method 'get_agenda_title()'. See agenda/signals.py.
topic = Topic.objects.create(title=custom_slide.title, text=custom_slide.text)
topic.attachments.add(*custom_slide.attachments.all())
item = Item.objects.get(object_id=custom_slide.pk, content_type=content_type_custom_slide)
item.object_id = topic.pk
item.content_type = content_type_topic
item.save()
# Delete old data.
CustomSlide.objects.all().delete()
content_type_custom_slide.delete()
# Reconnect autoupdate.
models.signals.post_save.connect(
inform_changed_data_receiver,
dispatch_uid='inform_changed_data_receiver')
models.signals.post_delete.connect(
inform_deleted_data_receiver,
dispatch_uid='inform_deleted_data_receiver')
class Migration(migrations.Migration):
dependencies = [
('core', '0004_projector_resolution'),
('topics', '0001_initial'),
]
operations = [
migrations.RunPython(
move_custom_slides_to_topics
),
migrations.RemoveField(
model_name='customslide',
name='attachments',
),
migrations.DeleteModel(
name='CustomSlide',
),
]

View File

@ -1,17 +1,14 @@
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session as DjangoSession from django.contrib.sessions.models import Session as DjangoSession
from django.db import models from django.db import models
from jsonfield import JSONField from jsonfield import JSONField
from openslides.mediafiles.models import Mediafile
from openslides.utils.models import RESTModelMixin from openslides.utils.models import RESTModelMixin
from openslides.utils.projector import ProjectorElement from openslides.utils.projector import ProjectorElement
from .access_permissions import ( from .access_permissions import (
ChatMessageAccessPermissions, ChatMessageAccessPermissions,
ConfigAccessPermissions, ConfigAccessPermissions,
CustomSlideAccessPermissions,
ProjectorAccessPermissions, ProjectorAccessPermissions,
TagAccessPermissions, TagAccessPermissions,
) )
@ -32,7 +29,7 @@ class Projector(RESTModelMixin, models.Model):
{ {
"881d875cf01741718ca926279ac9c99c": { "881d875cf01741718ca926279ac9c99c": {
"name": "core/customslide", "name": "topics/topic",
"id": 1 "id": 1
}, },
"191c0878cdc04abfbd64f3177a21891a": { "191c0878cdc04abfbd64f3177a21891a": {
@ -155,61 +152,6 @@ class Projector(RESTModelMixin, models.Model):
return True return True
class CustomSlide(RESTModelMixin, models.Model):
"""
Model for slides with custom content.
"""
access_permissions = CustomSlideAccessPermissions()
title = models.CharField(
max_length=256)
text = models.TextField(
blank=True)
weight = models.IntegerField(
default=0)
attachments = models.ManyToManyField(
Mediafile,
blank=True)
class Meta:
default_permissions = ()
ordering = ('weight', 'title', )
def __str__(self):
return self.title
@property
def agenda_item(self):
"""
Returns the related agenda item.
"""
# TODO: Move the agenda app in the core app to fix circular dependencies
from openslides.agenda.models import Item
content_type = ContentType.objects.get_for_model(self)
return Item.objects.get(object_id=self.pk, content_type=content_type)
@property
def agenda_item_id(self):
"""
Returns the id of the agenda item object related to this object.
"""
return self.agenda_item.pk
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.
"""
return " ".join((
self.title,
self.text))
class Tag(RESTModelMixin, models.Model): class Tag(RESTModelMixin, models.Model):
""" """
Model for tags. This tags can be used for other models like agenda items, Model for tags. This tags can be used for other models like agenda items,

View File

@ -3,28 +3,7 @@ from django.utils.timezone import now
from ..utils.projector import ProjectorElement from ..utils.projector import ProjectorElement
from .config import config from .config import config
from .exceptions import ProjectorException from .exceptions import ProjectorException
from .models import CustomSlide, Projector from .models import Projector
class CustomSlideSlide(ProjectorElement):
"""
Slide definitions for custom slide model.
"""
name = 'core/customslide'
def check_data(self):
if not CustomSlide.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('Custom slide does not exist.')
def get_requirements(self, config_entry):
try:
custom_slide = CustomSlide.objects.get(pk=config_entry.get('id'))
except CustomSlide.DoesNotExist:
# Custom slide does not exist. Just do nothing.
pass
else:
yield custom_slide
yield custom_slide.agenda_item
class Clock(ProjectorElement): class Clock(ProjectorElement):

View File

@ -1,6 +1,6 @@
from openslides.utils.rest_api import Field, ModelSerializer, ValidationError from openslides.utils.rest_api import Field, ModelSerializer, ValidationError
from .models import ChatMessage, CustomSlide, Projector, Tag from .models import ChatMessage, Projector, Tag
class JSONSerializerField(Field): class JSONSerializerField(Field):
@ -33,15 +33,6 @@ class ProjectorSerializer(ModelSerializer):
fields = ('id', 'config', 'elements', 'scale', 'scroll', 'width', 'height',) fields = ('id', 'config', 'elements', 'scale', 'scroll', 'width', 'height',)
class CustomSlideSerializer(ModelSerializer):
"""
Serializer for core.models.CustomSlide objects.
"""
class Meta:
model = CustomSlide
fields = ('id', 'title', 'text', 'weight', 'attachments', 'agenda_item_id')
class TagSerializer(ModelSerializer): class TagSerializer(ModelSerializer):
""" """
Serializer for core.models.Tag objects. Serializer for core.models.Tag objects.

View File

@ -338,50 +338,6 @@ angular.module('OpenSlidesApp.core', [
} }
]) ])
.factory('Customslide', [
'DS',
'jsDataModel',
'gettext',
function(DS, jsDataModel, gettext) {
var name = 'core/customslide';
return DS.defineResource({
name: name,
useClass: jsDataModel,
verboseName: gettext('Agenda item'),
methods: {
getResourceName: function () {
return name;
},
getAgendaTitle: function () {
return this.title;
},
// link name which is shown in search result
getSearchResultName: function () {
return this.getAgendaTitle();
},
// subtitle of search result
getSearchResultSubtitle: function () {
return "Agenda item";
},
},
relations: {
belongsTo: {
'agenda/item': {
localKey: 'agenda_item_id',
localField: 'agenda_item',
}
},
hasMany: {
'mediafiles/mediafile': {
localField: 'attachments',
localKeys: 'attachments_id',
}
}
}
});
}
])
.factory('Tag', [ .factory('Tag', [
'DS', 'DS',
function(DS) { function(DS) {
@ -544,10 +500,9 @@ angular.module('OpenSlidesApp.core', [
.run([ .run([
'ChatMessage', 'ChatMessage',
'Config', 'Config',
'Customslide',
'Projector', 'Projector',
'Tag', 'Tag',
function (ChatMessage, Config, Customslide, Projector, Tag) {} function (ChatMessage, Config, Projector, Tag) {}
]); ]);
}()); }());

View File

@ -42,10 +42,6 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
.config([ .config([
'slidesProvider', 'slidesProvider',
function(slidesProvider) { function(slidesProvider) {
slidesProvider.registerSlide('core/customslide', {
template: 'static/templates/core/slide_customslide.html',
});
slidesProvider.registerSlide('core/clock', { slidesProvider.registerSlide('core/clock', {
template: 'static/templates/core/slide_clock.html', template: 'static/templates/core/slide_clock.html',
}); });
@ -151,18 +147,6 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
} }
]) ])
.controller('SlideCustomSlideCtrl', [
'$scope',
'Customslide',
function($scope, Customslide) {
// Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
var id = $scope.element.id;
Customslide.bindOne(id, $scope, 'customslide');
}
])
.controller('SlideClockCtrl', [ .controller('SlideClockCtrl', [
'$scope', '$scope',
function($scope) { function($scope) {

View File

@ -713,48 +713,6 @@ angular.module('OpenSlidesApp.core.site', [
templateUrl: 'static/templates/search.html', templateUrl: 'static/templates/search.html',
}) })
// customslide
.state('core.customslide', {
url: '/customslide',
abstract: true,
template: "<ui-view/>",
})
.state('core.customslide.detail', {
resolve: {
customslide: function(Customslide, $stateParams) {
return Customslide.find($stateParams.id);
},
items: function(Agenda) {
return Agenda.findAll();
}
}
})
// redirects to customslide detail and opens customslide edit form dialog, uses edit url,
// used by ui-sref links from agenda only
// (from customslide controller use CustomSlideForm factory instead to open dialog in front
// of current view without redirect)
.state('core.customslide.detail.update', {
onEnter: ['$stateParams', '$state', 'ngDialog', 'Customslide', 'Agenda',
function($stateParams, $state, ngDialog, Customslide, Agenda) {
ngDialog.open({
template: 'static/templates/core/customslide-form.html',
controller: 'CustomslideUpdateCtrl',
className: 'ngdialog-theme-default wide-form',
resolve: {
customslide: function() {
return Customslide.find($stateParams.id);
},
items: function() {
return Agenda.findAll();
}
},
preCloseCallback: function() {
$state.go('core.customslide.detail', {customslide: $stateParams.id});
return true;
}
});
}],
})
// tag // tag
.state('core.tag', { .state('core.tag', {
url: '/tag', url: '/tag',
@ -1144,7 +1102,7 @@ angular.module('OpenSlidesApp.core.site', [
if ($scope.filterMotion && result.urlState == 'motions.motion.detail') { if ($scope.filterMotion && result.urlState == 'motions.motion.detail') {
return result; return result;
} }
if ($scope.filterAgenda && result.urlState == 'core.customslide.detail') { if ($scope.filterAgenda && result.urlState == 'topics.topic.detail') {
return result; return result;
} }
if ($scope.filterAssignment && result.urlState == 'assignments.assignment.detail') { if ($scope.filterAssignment && result.urlState == 'assignments.assignment.detail') {
@ -1159,88 +1117,6 @@ angular.module('OpenSlidesApp.core.site', [
} }
]) ])
// Provide generic customslide form fields for create and update view
.factory('CustomslideForm', [
'gettextCatalog',
'Editor',
'Mediafile',
'Agenda',
'AgendaTree',
function (gettextCatalog, Editor, Mediafile, Agenda, AgendaTree) {
return {
// ngDialog for customslide form
getDialog: function (customslide) {
var resolve = {};
if (customslide) {
resolve = {
customslide: function(Customslide) {return Customslide.find(customslide.id);}
};
}
resolve.mediafiles = function(Mediafile) {return Mediafile.findAll();};
return {
template: 'static/templates/core/customslide-form.html',
controller: (customslide) ? 'CustomslideUpdateCtrl' : 'CustomslideCreateCtrl',
className: 'ngdialog-theme-default wide-form',
closeByEscape: false,
closeByDocument: false,
resolve: (resolve) ? resolve : null
};
},
getFormFields: function () {
var images = Mediafile.getAllImages();
return [
{
key: 'title',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Title'),
required: true
}
},
{
key: 'text',
type: 'editor',
templateOptions: {
label: gettextCatalog.getString('Text')
},
data: {
tinymceOption: Editor.getOptions(images)
}
},
{
key: 'attachments_id',
type: 'select-multiple',
templateOptions: {
label: gettextCatalog.getString('Attachment'),
options: Mediafile.getAll(),
ngOptions: 'option.id as option.title_or_filename for option in to.options',
placeholder: gettextCatalog.getString('Select or search an attachment ...')
}
},
{
key: 'showAsAgendaItem',
type: 'checkbox',
templateOptions: {
label: gettextCatalog.getString('Show as agenda item'),
description: gettextCatalog.getString('If deactivated it appears as internal item on agenda.')
}
},
{
key: 'agenda_parent_item_id',
type: 'select-single',
templateOptions: {
label: gettextCatalog.getString('Parent item'),
options: AgendaTree.getFlatTree(Agenda.getAll()),
ngOptions: 'item.id as item.getListViewTitle() for item in to.options | notself : model.agenda_item_id',
placeholder: gettextCatalog.getString('Select a parent item ...')
}
}];
}
};
}
])
// Projector Control Controller // Projector Control Controller
.controller('ProjectorControlCtrl', [ .controller('ProjectorControlCtrl', [
'$scope', '$scope',
@ -1454,101 +1330,6 @@ angular.module('OpenSlidesApp.core.site', [
} }
]) ])
// Customslide Controllers
.controller('CustomslideDetailCtrl', [
'$scope',
'ngDialog',
'CustomslideForm',
'Customslide',
'customslide',
function($scope, ngDialog, CustomslideForm, Customslide, customslide) {
Customslide.bindOne(customslide.id, $scope, 'customslide');
Customslide.loadRelations(customslide, 'agenda_item');
// open edit dialog
$scope.openDialog = function (customslide) {
ngDialog.open(CustomslideForm.getDialog(customslide));
};
}
])
.controller('CustomslideCreateCtrl', [
'$scope',
'$state',
'Customslide',
'CustomslideForm',
'Agenda',
'AgendaUpdate',
function($scope, $state, Customslide, CustomslideForm, Agenda, AgendaUpdate) {
$scope.customslide = {};
$scope.model = {};
$scope.model.showAsAgendaItem = true;
// get all form fields
$scope.formFields = CustomslideForm.getFormFields();
// save form
$scope.save = function (customslide) {
Customslide.create(customslide).then(
function(success) {
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
// see openslides.agenda.models.Item.ITEM_TYPE.
var changes = [{key: 'type', value: (customslide.showAsAgendaItem ? 1 : 2)},
{key: 'parent_id', value: customslide.agenda_parent_item_id}];
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
});
$scope.closeThisDialog();
};
}
])
.controller('CustomslideUpdateCtrl', [
'$scope',
'$state',
'Customslide',
'CustomslideForm',
'Agenda',
'AgendaUpdate',
'customslide',
function($scope, $state, Customslide, CustomslideForm, Agenda, AgendaUpdate, customslide) {
Customslide.loadRelations(customslide, 'agenda_item');
$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;
} else if($scope.formFields[i].key == "agenda_parent_item_id") {
$scope.formFields[i].defaultValue = customslide.agenda_item.parent_id;
}
}
// save form
$scope.save = function (customslide) {
Customslide.create(customslide).then(
function(success) {
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
// see openslides.agenda.models.Item.ITEM_TYPE.
var changes = [{key: 'type', value: (customslide.showAsAgendaItem ? 1 : 2)},
{key: 'parent_id', value: customslide.agenda_parent_item_id}];
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
$scope.closeThisDialog();
}, function (error) {
// save error: revert all changes by restore
// (refresh) original customslide object from server
Customslide.refresh(customslide);
var message = '';
for (var e in error.data) {
message += e + ': ' + error.data[e] + ' ';
}
$scope.alert = {type: 'danger', msg: message, show: true};
}
);
};
}
])
// Tag Controller // Tag Controller
.controller('TagListCtrl', [ .controller('TagListCtrl', [
'$scope', '$scope',

View File

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

View File

@ -37,13 +37,12 @@ from openslides.utils.search import search
from .access_permissions import ( from .access_permissions import (
ChatMessageAccessPermissions, ChatMessageAccessPermissions,
ConfigAccessPermissions, ConfigAccessPermissions,
CustomSlideAccessPermissions,
ProjectorAccessPermissions, ProjectorAccessPermissions,
TagAccessPermissions, TagAccessPermissions,
) )
from .config import config from .config import config
from .exceptions import ConfigError, ConfigNotFound from .exceptions import ConfigError, ConfigNotFound
from .models import ChatMessage, CustomSlide, Projector, Tag from .models import ChatMessage, Projector, Tag
# Special Django views # Special Django views
@ -449,27 +448,6 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
return Response({'detail': message}) return Response({'detail': message})
class CustomSlideViewSet(ModelViewSet):
"""
API endpoint for custom slides.
There are the following views: metadata, list, retrieve, create,
partial_update, update and destroy.
"""
access_permissions = CustomSlideAccessPermissions()
queryset = CustomSlide.objects.all()
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().check_permissions(self.request.user)
else:
result = self.request.user.has_perm('core.can_manage_projector')
return result
class TagViewSet(ModelViewSet): class TagViewSet(ModelViewSet):
""" """
API endpoint for tags. API endpoint for tags.

View File

@ -17,6 +17,7 @@ INSTALLED_APPS = [
'rest_framework', 'rest_framework',
'channels', 'channels',
'openslides.agenda', 'openslides.agenda',
'openslides.topics',
'openslides.motions', 'openslides.motions',
'openslides.assignments', 'openslides.assignments',
'openslides.mediafiles', 'openslides.mediafiles',

View File

@ -0,0 +1 @@
default_app_config = 'openslides.topics.apps.TopicsAppConfig'

View File

@ -0,0 +1,20 @@
from ..utils.access_permissions import BaseAccessPermissions
class TopicAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Topic and TopicViewSet.
"""
def check_permissions(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('agenda.can_see')
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import TopicSerializer
return TopicSerializer

20
openslides/topics/apps.py Normal file
View File

@ -0,0 +1,20 @@
from django.apps import AppConfig
class TopicsAppConfig(AppConfig):
name = 'openslides.topics'
verbose_name = 'OpenSlides Topics'
angular_site_module = True
angular_projector_module = True
def ready(self):
# Load projector elements.
# Do this by just importing all from these files.
from . import projector # noqa
# Import all required stuff.
from ..utils.rest_api import router
from .views import TopicViewSet
# Register viewsets.
router.register(self.get_model('Topic').get_collection_string(), TopicViewSet)

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-18 20:20
from __future__ import unicode_literals
from django.db import migrations, models
import openslides.utils.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('mediafiles', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Topic',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=256)),
('text', models.TextField(blank=True)),
('attachments', models.ManyToManyField(blank=True, to='mediafiles.Mediafile')),
],
options={
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model),
),
]

View File

View File

@ -0,0 +1,53 @@
from django.contrib.contenttypes.models import ContentType
from django.db import models
from ..agenda.models import Item
from ..mediafiles.models import Mediafile
from ..utils.models import RESTModelMixin
from .access_permissions import TopicAccessPermissions
class Topic(RESTModelMixin, models.Model):
"""
Model for slides with custom content. Used to be called custom slide.
"""
access_permissions = TopicAccessPermissions()
title = models.CharField(max_length=256)
text = models.TextField(blank=True)
attachments = models.ManyToManyField(Mediafile, blank=True)
class Meta:
default_permissions = ()
def __str__(self):
return self.title
@property
def agenda_item(self):
"""
Returns the related agenda item.
"""
content_type = ContentType.objects.get_for_model(self)
return Item.objects.get(object_id=self.pk, content_type=content_type)
@property
def agenda_item_id(self):
"""
Returns the id of the agenda item object related to this object.
"""
return self.agenda_item.pk
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.
"""
return " ".join((
self.title,
self.text))

View File

@ -0,0 +1,24 @@
from ..core.exceptions import ProjectorException
from ..utils.projector import ProjectorElement
from .models import Topic
class TopicSlide(ProjectorElement):
"""
Slide definitions for topic model.
"""
name = 'topics/topic'
def check_data(self):
if not Topic.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('Topic does not exist.')
def get_requirements(self, config_entry):
try:
topic = Topic.objects.get(pk=config_entry.get('id'))
except Topic.DoesNotExist:
# Topic does not exist. Just do nothing.
pass
else:
yield topic
yield topic.agenda_item

View File

@ -0,0 +1,12 @@
from openslides.utils.rest_api import ModelSerializer
from .models import Topic
class TopicSerializer(ModelSerializer):
"""
Serializer for core.models.Topic objects.
"""
class Meta:
model = Topic
fields = ('id', 'title', 'text', 'attachments', 'agenda_item_id')

View File

@ -0,0 +1,53 @@
(function () {
'use strict';
angular.module('OpenSlidesApp.topics', [])
.factory('Topic', [
'DS',
'jsDataModel',
'gettext',
function(DS, jsDataModel, gettext) {
var name = 'topics/topic';
return DS.defineResource({
name: name,
useClass: jsDataModel,
verboseName: gettext('Topic'),
methods: {
getResourceName: function () {
return name;
},
getAgendaTitle: function () {
return this.title;
},
// link name which is shown in search result
getSearchResultName: function () {
return this.getAgendaTitle();
},
// subtitle of search result
getSearchResultSubtitle: function () {
return 'Topic';
},
},
relations: {
belongsTo: {
'agenda/item': {
localKey: 'agenda_item_id',
localField: 'agenda_item',
}
},
hasMany: {
'mediafiles/mediafile': {
localField: 'attachments',
localKeys: 'attachments_id',
}
}
}
});
}
])
.run(['Topic', function(Topic) {}]);
}());

View File

@ -0,0 +1,28 @@
(function () {
'use strict';
angular.module('OpenSlidesApp.topics.projector', ['OpenSlidesApp.topics'])
.config([
'slidesProvider',
function (slidesProvider) {
slidesProvider.registerSlide('topics/topic', {
template: 'static/templates/topics/slide_topic.html'
});
}
])
.controller('SlideTopicCtrl', [
'$scope',
'Topic',
function($scope, Topic) {
// Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
var id = $scope.element.id;
Topic.bindOne(id, $scope, 'topic');
}
]);
})();

View File

@ -0,0 +1,391 @@
(function () {
'use strict';
angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics'])
.config([
'$stateProvider',
function($stateProvider) {
$stateProvider
.state('topics', {
url: '/topics',
abstract: true,
template: "<ui-view/>",
})
.state('topics.topic', {
url: '/topic',
abstract: true,
template: "<ui-view/>",
})
.state('topics.topic.detail', {
resolve: {
topic: function(Topic, $stateParams) {
return Topic.find($stateParams.id);
},
items: function(Agenda) {
return Agenda.findAll();
}
}
})
// redirects to topic detail and opens topic edit form dialog, uses edit url,
// used by ui-sref links from agenda only
// (from topic controller use TopicForm factory instead to open dialog in front
// of current view without redirect)
.state('topics.topic.detail.update', {
onEnter: ['$stateParams', '$state', 'ngDialog', 'Topic',
function($stateParams, $state, ngDialog, Topic) {
ngDialog.open({
template: 'static/templates/topics/topic-form.html',
controller: 'TopicUpdateCtrl',
className: 'ngdialog-theme-default wide-form',
closeByEscape: false,
closeByDocument: false,
resolve: {
topic: function() {
return Topic.find($stateParams.id);
},
items: function(Agenda) {
return Agenda.findAll().catch(
function() {
return null;
}
);
}
},
preCloseCallback: function() {
$state.go('topics.topic.detail', {topic: $stateParams.id});
return true;
}
});
}],
})
.state('topics.topic.import', {
url: '/import',
controller: 'TopicImportCtrl',
});
}
])
.factory('TopicForm', [
'gettextCatalog',
'Editor',
'Mediafile',
'Agenda',
'AgendaTree',
function (gettextCatalog, Editor, Mediafile, Agenda, AgendaTree) {
return {
// ngDialog for topic form
getDialog: function (topic) {
var resolve = {};
if (topic) {
resolve = {
topic: function (Topic) {return Topic.find(topic.id);}
};
}
resolve.mediafiles = function (Mediafile) {
return Mediafile.findAll();
};
return {
template: 'static/templates/topics/topic-form.html',
controller: (topic) ? 'TopicUpdateCtrl' : 'TopicCreateCtrl',
className: 'ngdialog-theme-default wide-form',
closeByEscape: false,
closeByDocument: false,
resolve: (resolve) ? resolve : null
};
},
getFormFields: function () {
var images = Mediafile.getAllImages();
return [
{
key: 'title',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Title'),
required: true
}
},
{
key: 'text',
type: 'editor',
templateOptions: {
label: gettextCatalog.getString('Text')
},
data: {
tinymceOption: Editor.getOptions(images)
}
},
{
key: 'attachments_id',
type: 'select-multiple',
templateOptions: {
label: gettextCatalog.getString('Attachment'),
options: Mediafile.getAll(),
ngOptions: 'option.id as option.title_or_filename for option in to.options',
placeholder: gettextCatalog.getString('Select or search an attachment ...')
}
},
{
key: 'showAsAgendaItem',
type: 'checkbox',
templateOptions: {
label: gettextCatalog.getString('Show as agenda item'),
description: gettextCatalog.getString('If deactivated it appears as internal item on agenda.')
}
},
{
key: 'agenda_parent_item_id',
type: 'select-single',
templateOptions: {
label: gettextCatalog.getString('Parent item'),
options: AgendaTree.getFlatTree(Agenda.getAll()),
ngOptions: 'item.id as item.getListViewTitle() for item in to.options | notself : model.agenda_item_id',
placeholder: gettextCatalog.getString('Select a parent item ...')
}
}];
}
};
}
])
.controller('TopicDetailCtrl', [
'$scope',
'ngDialog',
'TopicForm',
'Topic',
'topic',
function($scope, ngDialog, TopicForm, Topic, topic) {
Topic.bindOne(topic.id, $scope, 'topic');
Topic.loadRelations(topic, 'agenda_item');
$scope.openDialog = function (topic) {
ngDialog.open(TopicForm.getDialog(topic));
};
}
])
.controller('TopicCreateCtrl', [
'$scope',
'$state',
'Topic',
'TopicForm',
'Agenda',
'AgendaUpdate',
function($scope, $state, Topic, TopicForm, Agenda, AgendaUpdate) {
$scope.topic = {};
$scope.model = {};
$scope.model.showAsAgendaItem = true;
// get all form fields
$scope.formFields = TopicForm.getFormFields();
// save form
$scope.save = function (topic) {
Topic.create(topic).then(
function (success) {
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
// see openslides.agenda.models.Item.ITEM_TYPE.
var changes = [{key: 'type', value: (topic.showAsAgendaItem ? 1 : 2)},
{key: 'parent_id', value: topic.agenda_parent_item_id}];
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
});
$scope.closeThisDialog();
};
}
])
.controller('TopicUpdateCtrl', [
'$scope',
'$state',
'Topic',
'TopicForm',
'Agenda',
'AgendaUpdate',
'topic',
function($scope, $state, Topic, TopicForm, Agenda, AgendaUpdate, topic) {
Topic.loadRelations(topic, 'agenda_item');
$scope.alert = {};
// set initial values for form model by create deep copy of topic object
// so list/detail view is not updated while editing
$scope.model = angular.copy(topic);
// get all form fields
$scope.formFields = TopicForm.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 = !topic.agenda_item.is_hidden;
} else if ($scope.formFields[i].key == "agenda_parent_item_id") {
$scope.formFields[i].defaultValue = topic.agenda_item.parent_id;
}
}
// save form
$scope.save = function (topic) {
Topic.create(topic).then(
function(success) {
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
// see openslides.agenda.models.Item.ITEM_TYPE.
var changes = [{key: 'type', value: (topic.showAsAgendaItem ? 1 : 2)},
{key: 'parent_id', value: topic.agenda_parent_item_id}];
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
$scope.closeThisDialog();
}, function (error) {
// save error: revert all changes by restore
// (refresh) original topic object from server
Topic.refresh(topic);
var message = '';
for (var e in error.data) {
message += e + ': ' + error.data[e] + ' ';
}
$scope.alert = {type: 'danger', msg: message, show: true};
}
);
};
}
])
.controller('TopicImportCtrl', [
'$scope',
'gettext',
'Agenda',
'Topic',
function($scope, gettext, Agenda, Topic) {
// Big TODO: Change wording from "item" to "topic".
// import from textarea
$scope.importByLine = function () {
if ($scope.itemlist) {
$scope.titleItems = $scope.itemlist[0].split("\n");
$scope.importcounter = 0;
$scope.titleItems.forEach(function(title, index) {
var item = {title: title};
// TODO: create all items in bulk mode
Topic.create(item).then(
function(success) {
// find related agenda item
Agenda.find(success.agenda_item_id).then(function(item) {
// import all items as type AGENDA_ITEM = 1
item.type = 1;
item.weight = 1000 + index;
Agenda.save(item);
});
$scope.importcounter++;
}
);
});
}
};
// *** CSV import ***
// set initial data for csv import
$scope.items = [];
$scope.separator = ',';
$scope.encoding = 'UTF-8';
$scope.encodingOptions = ['UTF-8', 'ISO-8859-1'];
$scope.accept = '.csv, .txt';
$scope.csv = {
content: null,
header: true,
headerVisible: false,
separator: $scope.separator,
separatorVisible: false,
encoding: $scope.encoding,
encodingVisible: false,
accept: $scope.accept,
result: null
};
// set csv file encoding
$scope.setEncoding = function () {
$scope.csv.encoding = $scope.encoding;
};
// set csv file encoding
$scope.setSeparator = function () {
$scope.csv.separator = $scope.separator;
};
// detect if csv file is loaded
$scope.$watch('csv.result', function () {
$scope.items = [];
var quotionRe = /^"(.*)"$/;
angular.forEach($scope.csv.result, function (item, index) {
// title
if (item.title) {
item.title = item.title.replace(quotionRe, '$1');
}
if (!item.title) {
item.importerror = true;
item.title_error = gettext('Error: Title is required.');
}
// text
if (item.text) {
item.text = item.text.replace(quotionRe, '$1');
}
// duration
if (item.duration) {
item.duration = item.duration.replace(quotionRe, '$1');
}
// comment
if (item.comment) {
item.comment = item.comment.replace(quotionRe, '$1');
}
// is_hidden
if (item.is_hidden) {
item.is_hidden = item.is_hidden.replace(quotionRe, '$1');
if (item.is_hidden == '1') {
item.type = 2;
} else {
item.type = 1;
}
} else {
item.type = 1;
}
// set weight for right csv row order
// (Use 1000+ to protect existing items and prevent collision
// with new items which use weight 10000 as default.)
item.weight = 1000 + index;
$scope.items.push(item);
});
});
// import from csv file
$scope.import = function () {
$scope.csvImporting = true;
angular.forEach($scope.items, function (item) {
if (!item.importerror) {
Topic.create(item).then(
function(success) {
item.imported = true;
// find related agenda item
Agenda.find(success.agenda_item_id).then(function(agendaItem) {
agendaItem.duration = item.duration;
agendaItem.comment = item.comment;
agendaItem.type = item.type;
agendaItem.weight = item.weight;
Agenda.save(agendaItem);
});
}
);
}
});
$scope.csvimported = true;
};
$scope.clear = function () {
$scope.csv.result = null;
};
// download CSV example file
$scope.downloadCSVExample = function () {
var element = document.getElementById('downloadLink');
var csvRows = [
// column header line
['title', 'text', 'duration', 'comment', 'is_hidden'],
// example entries
['Demo 1', 'Demo text 1', '1:00', 'test comment', ''],
['Break', '', '0:10', '', '1'],
['Demo 2', 'Demo text 2', '1:30', '', '']
];
var csvString = csvRows.join("%0A");
element.href = 'data:text/csv;charset=utf-8,' + csvString;
element.download = 'agenda-example.csv';
element.target = '_blank';
};
}
]);
}());

View File

@ -0,0 +1,4 @@
<div ng-controller="SlideTopicCtrl" class="content scrollcontent">
<h1>{{ topic.agenda_item.getTitle() }}</h1>
<div ng-bind-html="topic.text | trusted"></div>
</div>

View File

@ -6,34 +6,34 @@
<translate>Back to overview</translate> <translate>Back to overview</translate>
</a> </a>
<!-- List of speakers --> <!-- List of speakers -->
<a ui-sref="agenda.item.detail({id: customslide.agenda_item_id})" class="btn btn-sm btn-default"> <a ui-sref="agenda.item.detail({id: topic.agenda_item_id})" class="btn btn-sm btn-default">
<i class="fa fa-microphone fa-lg"></i> <i class="fa fa-microphone fa-lg"></i>
<translate>List of speakers</translate> <translate>List of speakers</translate>
</a> </a>
<!-- project --> <!-- project -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm" <a os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': customslide.isProjected() }" ng-class="{ 'btn-primary': topic.isProjected() }"
ng-click="customslide.project()" ng-click="topic.project()"
title="{{ 'Project agenda item' | translate }}"> title="{{ 'Project topic' | translate }}">
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
</a> </a>
<!-- edit --> <!-- edit -->
<a os-perms="agenda.can_manage" ng-click="openDialog(customslide)" <a os-perms="agenda.can_manage" ng-click="openDialog(topic)"
class="btn btn-default btn-sm" class="btn btn-default btn-sm"
title="{{ 'Edit' | translate}}"> title="{{ 'Edit' | translate}}">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
</a> </a>
</div> </div>
<h1>{{ customslide.agenda_item.getTitle() }}</h1> <h1>{{ topic.agenda_item.getTitle() }}</h1>
<h2 translate>Agenda item</h2> <h2 translate>Topic</h2>
</div> </div>
</div> </div>
<div class="details"> <div class="details">
<div ng-bind-html="customslide.text | trusted"></div> <div ng-bind-html="topic.text | trusted"></div>
<h3 ng-if="customslide.attachments.length > 0" translate>Attachments</h3> <h3 ng-if="topic.attachments.length > 0" translate>Attachments</h3>
<ul> <ul>
<li ng-repeat="attachment in customslide.attachments"> <li ng-repeat="attachment in topic.attachments">
<a href="{{ attachment.mediafileUrl }}" target="_blank"> <a href="{{ attachment.mediafileUrl }}" target="_blank">
{{ attachment.title_or_filename }} {{ attachment.title_or_filename }}
</a> </a>

View File

@ -1,13 +1,13 @@
<h1 ng-if="model.id" translate>Edit agenda item</h1> <h1 ng-if="model.id" translate>Edit topic</h1>
<h1 ng-if="!model.id" translate>New agenda item</h1> <h1 ng-if="!model.id" translate>New topic</h1>
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}"> <div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}">
{{ alert.msg }} {{ alert.msg }}
</div> </div>
<form name="customslideForm" ng-submit="save(model)"> <form name="topicForm" ng-submit="save(model)">
<formly-form model="model" fields="formFields"> <formly-form model="model" fields="formFields">
<button type="submit" ng-disabled="customslideForm.$invalid" class="btn btn-primary" translate> <button type="submit" ng-disabled="topicForm.$invalid" class="btn btn-primary" translate>
Save Save
</button> </button>
<button type="button" ng-click="closeThisDialog()" class="btn btn-default" translate> <button type="button" ng-click="closeThisDialog()" class="btn btn-default" translate>

View File

@ -6,14 +6,13 @@
<translate>Back to overview</translate> <translate>Back to overview</translate>
</a> </a>
</div> </div>
<h1 translate>Import agenda items</h1> <h1 translate>Import topics</h1>
</div> </div>
</div> </div>
<div class="details"> <div class="details">
<h2 translate>Import by copy/paste</h2> <h2 translate>Import by copy/paste</h2>
<p translate>Copy and paste your agenda item titles in this textbox. <p translate>Copy and paste your topic titles in this textbox. Keep each item in a single line.</p>
Keep each item in a single line.</p>
<div class="row"> <div class="row">
<div class="form-group col-sm-6"> <div class="form-group col-sm-6">
@ -114,20 +113,20 @@ Keep each item in a single line.</p>
<div ng-repeat="item in itemsFailed = (items | filter:{importerror:true})"></div> <div ng-repeat="item in itemsFailed = (items | filter:{importerror:true})"></div>
<i class="fa fa-exclamation-triangle"></i> <i class="fa fa-exclamation-triangle"></i>
{{ itemsFailed.length }} {{ itemsFailed.length }}
<translate>agenda items will be not imported.</translate> <translate>topics will be not imported.</translate>
</div> </div>
<div> <div>
<div ng-repeat="item in itemsPassed = (items | filter:{importerror:false})"></div> <div ng-repeat="item in itemsPassed = (items | filter:{importerror:false})"></div>
<i class="fa fa-check-circle-o fa-lg"></i> <i class="fa fa-check-circle-o fa-lg"></i>
{{ items.length - itemsFailed.length }} {{ items.length - itemsFailed.length }}
<translate>items will be imported.</translate> <translate>topics will be imported.</translate>
</div> </div>
<div ng-repeat="item in itemsImported = (items | filter:{imported:true})"></div> <div ng-repeat="item in itemsImported = (items | filter:{imported:true})"></div>
<div ng-if="itemsImported.length > 0" class="text-success"> <div ng-if="itemsImported.length > 0" class="text-success">
<hr class="smallhr"> <hr class="smallhr">
<i class="fa fa-check-circle fa-lg"></i> <i class="fa fa-check-circle fa-lg"></i>
{{ itemsImported.length }} {{ itemsImported.length }}
<translate>items were successfully imported.</translate> <translate>topics were successfully imported.</translate>
</div> </div>
<div class="spacer"> <div class="spacer">
@ -135,7 +134,7 @@ Keep each item in a single line.</p>
Clear preview Clear preview
</button> </button>
<button ng-if="!csvImporting" ng-click="import()" class="btn btn-primary" translate> <button ng-if="!csvImporting" ng-click="import()" class="btn btn-primary" translate>
Import {{ items.length - itemsFailed.length }} items Import {{ items.length - itemsFailed.length }} topics
</button> </button>
</div> </div>
<div class="spacer"> <div class="spacer">

View File

@ -0,0 +1,25 @@
from openslides.utils.rest_api import ModelViewSet
from .access_permissions import TopicAccessPermissions
from .models import Topic
class TopicViewSet(ModelViewSet):
"""
API endpoint for topics.
There are the following views: metadata, list, retrieve, create,
partial_update, update and destroy.
"""
access_permissions = TopicAccessPermissions()
queryset = Topic.objects.all()
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().check_permissions(self.request.user)
else:
result = self.request.user.has_perm('agenda.can_manage')
return result

View File

@ -1,5 +1,5 @@
from openslides.agenda.models import Item from openslides.agenda.models import Item
from openslides.core.models import CustomSlide from openslides.topics.models import Topic
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
@ -9,7 +9,7 @@ class TestItemManager(TestCase):
Test that get_root_and_children needs only one db query. Test that get_root_and_children needs only one db query.
""" """
for i in range(10): for i in range(10):
CustomSlide.objects.create(title='item{}'.format(i)) Topic.objects.create(title='item{}'.format(i))
with self.assertNumQueries(1): with self.assertNumQueries(1):
Item.objects.get_root_and_children() Item.objects.get_root_and_children()

View File

@ -3,15 +3,15 @@ import json
from rest_framework.test import APIClient from rest_framework.test import APIClient
from openslides.agenda.models import Item from openslides.agenda.models import Item
from openslides.core.models import CustomSlide from openslides.topics.models import Topic
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
class AgendaTreeTest(TestCase): class AgendaTreeTest(TestCase):
def setUp(self): def setUp(self):
CustomSlide.objects.create(title='item1') Topic.objects.create(title='item1')
item2 = CustomSlide.objects.create(title='item2').agenda_item item2 = Topic.objects.create(title='item2').agenda_item
item3 = CustomSlide.objects.create(title='item2a').agenda_item item3 = Topic.objects.create(title='item2a').agenda_item
item3.parent = item2 item3.parent = item2
item3.save() item3.save()
self.client = APIClient() self.client = APIClient()
@ -90,7 +90,7 @@ class TestAgendaPDF(TestCase):
""" """
Tests that a requst on the pdf-page returns with statuscode 200. Tests that a requst on the pdf-page returns with statuscode 200.
""" """
CustomSlide.objects.create(title='item1') Topic.objects.create(title='item1')
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
response = self.client.get('/agenda/print/') response = self.client.get('/agenda/print/')

View File

@ -5,7 +5,8 @@ from rest_framework.test import APIClient
from openslides.agenda.models import Item, Speaker from openslides.agenda.models import Item, Speaker
from openslides.core.config import config from openslides.core.config import config
from openslides.core.models import CustomSlide, Projector from openslides.core.models import Projector
from openslides.topics.models import Topic
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
@ -16,7 +17,7 @@ class RetrieveItem(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
config['general_system_enable_anonymous'] = True config['general_system_enable_anonymous'] = True
self.item = CustomSlide.objects.create(title='test_title_Idais2pheepeiz5uph1c').agenda_item self.item = Topic.objects.create(title='test_title_Idais2pheepeiz5uph1c').agenda_item
def test_normal_by_anonymous_without_perm_to_see_hidden_items(self): def test_normal_by_anonymous_without_perm_to_see_hidden_items(self):
group = get_user_model().groups.field.related_model.objects.get(pk=1) # Group with pk 1 is for anonymous users. group = get_user_model().groups.field.related_model.objects.get(pk=1) # Group with pk 1 is for anonymous users.
@ -47,7 +48,7 @@ class ManageSpeaker(TestCase):
self.client = APIClient() self.client = APIClient()
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
self.item = CustomSlide.objects.create(title='test_title_aZaedij4gohn5eeQu8fe').agenda_item self.item = Topic.objects.create(title='test_title_aZaedij4gohn5eeQu8fe').agenda_item
self.user = get_user_model().objects.create_user( self.user = get_user_model().objects.create_user(
username='test_user_jooSaex1bo5ooPhuphae', username='test_user_jooSaex1bo5ooPhuphae',
password='test_password_e6paev4zeeh9n') password='test_password_e6paev4zeeh9n')
@ -164,7 +165,7 @@ class Speak(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
self.item = CustomSlide.objects.create(title='test_title_KooDueco3zaiGhiraiho').agenda_item self.item = Topic.objects.create(title='test_title_KooDueco3zaiGhiraiho').agenda_item
self.user = get_user_model().objects.create_user( self.user = get_user_model().objects.create_user(
username='test_user_Aigh4vohb3seecha4aa4', username='test_user_Aigh4vohb3seecha4aa4',
password='test_password_eneupeeVo5deilixoo8j') password='test_password_eneupeeVo5deilixoo8j')
@ -273,19 +274,19 @@ class Numbering(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
self.item_1 = CustomSlide.objects.create(title='test_title_thuha8eef7ohXar3eech').agenda_item self.item_1 = Topic.objects.create(title='test_title_thuha8eef7ohXar3eech').agenda_item
self.item_1.type = Item.AGENDA_ITEM self.item_1.type = Item.AGENDA_ITEM
self.item_1.weight = 1 self.item_1.weight = 1
self.item_1.save() self.item_1.save()
self.item_2 = CustomSlide.objects.create(title='test_title_eisah7thuxa1eingaeLo').agenda_item self.item_2 = Topic.objects.create(title='test_title_eisah7thuxa1eingaeLo').agenda_item
self.item_2.type = Item.AGENDA_ITEM self.item_2.type = Item.AGENDA_ITEM
self.item_2.weight = 2 self.item_2.weight = 2
self.item_2.save() self.item_2.save()
self.item_2_1 = CustomSlide.objects.create(title='test_title_Qui0audoaz5gie1phish').agenda_item self.item_2_1 = Topic.objects.create(title='test_title_Qui0audoaz5gie1phish').agenda_item
self.item_2_1.type = Item.AGENDA_ITEM self.item_2_1.type = Item.AGENDA_ITEM
self.item_2_1.parent = self.item_2 self.item_2_1.parent = self.item_2
self.item_2_1.save() self.item_2_1.save()
self.item_3 = CustomSlide.objects.create(title='test_title_ah7tphisheineisgaeLo').agenda_item self.item_3 = Topic.objects.create(title='test_title_ah7tphisheineisgaeLo').agenda_item
self.item_3.type = Item.AGENDA_ITEM self.item_3.type = Item.AGENDA_ITEM
self.item_3.weight = 3 self.item_3.weight = 3
self.item_3.save() self.item_3.save()

View File

@ -6,7 +6,8 @@ from rest_framework.test import APIClient
from openslides import __version__ as version from openslides import __version__ as version
from openslides.core.config import ConfigVariable, config from openslides.core.config import ConfigVariable, config
from openslides.core.models import CustomSlide, Projector from openslides.core.models import Projector
from openslides.topics.models import Topic
from openslides.utils.rest_api import ValidationError from openslides.utils.rest_api import ValidationError
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
@ -17,10 +18,10 @@ class ProjectorAPI(TestCase):
""" """
def test_slide_on_default_projector(self): def test_slide_on_default_projector(self):
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
customslide = CustomSlide.objects.create(title='title_que1olaish5Wei7que6i', text='text_aishah8Eh7eQuie5ooji') topic = Topic.objects.create(title='title_que1olaish5Wei7que6i', text='text_aishah8Eh7eQuie5ooji')
default_projector = Projector.objects.get(pk=1) default_projector = Projector.objects.get(pk=1)
default_projector.config = { default_projector.config = {
'aae4a07b26534cfb9af4232f361dce73': {'name': 'core/customslide', 'id': customslide.id}} 'aae4a07b26534cfb9af4232f361dce73': {'name': 'topics/topic', 'id': topic.id}}
default_projector.save() default_projector.save()
response = self.client.get(reverse('projector-detail', args=['1'])) response = self.client.get(reverse('projector-detail', args=['1']))
@ -30,9 +31,9 @@ class ProjectorAPI(TestCase):
'id': 1, 'id': 1,
'elements': { 'elements': {
'aae4a07b26534cfb9af4232f361dce73': 'aae4a07b26534cfb9af4232f361dce73':
{'id': customslide.id, {'id': topic.id,
'uuid': 'aae4a07b26534cfb9af4232f361dce73', 'uuid': 'aae4a07b26534cfb9af4232f361dce73',
'name': 'core/customslide'}}, 'name': 'topics/topic'}},
'scale': 0, 'scale': 0,
'scroll': 0, 'scroll': 0,
'width': 1024, 'width': 1024,

View File

@ -1,5 +1,5 @@
from openslides.agenda.models import Item, Speaker from openslides.agenda.models import Item, Speaker
from openslides.core.models import CustomSlide from openslides.topics.models import Topic
from openslides.users.models import User from openslides.users.models import User
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
@ -7,8 +7,8 @@ from openslides.utils.test import TestCase
class ListOfSpeakerModelTests(TestCase): class ListOfSpeakerModelTests(TestCase):
def setUp(self): def setUp(self):
self.item1 = CustomSlide.objects.create(title='item1').agenda_item self.item1 = Topic.objects.create(title='item1').agenda_item
self.item2 = CustomSlide.objects.create(title='item2').agenda_item self.item2 = Topic.objects.create(title='item2').agenda_item
self.speaker1 = User.objects.create(username='user1') self.speaker1 = User.objects.create(username='user1')
self.speaker2 = User.objects.create(username='user2') self.speaker2 = User.objects.create(username='user2')