diff --git a/CHANGELOG b/CHANGELOG
index 4e0fd740f..54ea48a31 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -18,6 +18,7 @@ Assignments:
Core:
- Added support for big assemblies with lots of users.
- Added HTML support for messages on the projector.
+- Moved custom slides to own app "topics". Renamed it to "Topic".
Motions:
- Added origin field.
diff --git a/openslides/agenda/static/js/agenda/site.js b/openslides/agenda/static/js/agenda/site.js
index 160f168a6..31be6db39 100644
--- a/openslides/agenda/static/js/agenda/site.js
+++ b/openslides/agenda/static/js/agenda/site.js
@@ -68,10 +68,6 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
url: '/sort',
controller: 'AgendaSortCtrl',
})
- .state('agenda.item.import', {
- url: '/import',
- controller: 'AgendaImportCtrl',
- })
.state('agenda.current-list-of-speakers', {
url: '/speakers',
controller: 'ListOfSpeakersViewCtrl',
@@ -100,10 +96,10 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
'operator',
'ngDialog',
'Agenda',
- 'CustomslideForm',
+ 'TopicForm', // TODO: Remove this dependency. Use template hook for "New" and "Import" buttons.
'AgendaTree',
'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
$scope.$watch(function () {
return Agenda.lastModified();
@@ -125,11 +121,12 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
};
// check open permission
+ // TODO: Use generic solution here.
$scope.isAllowedToSeeOpenLink = function (item) {
var collection = item.content_object.collection;
switch (collection) {
- case 'core/customslide':
- return operator.hasPerms('core.can_manage_projector');
+ case 'topics/topic':
+ return operator.hasPerms('agenda.can_see');
case 'motions/motion':
return operator.hasPerms('motions.can_see');
case 'assignments/assignment':
@@ -138,9 +135,9 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
return false;
}
};
- // open new dialog
+ // open dialog for new topics // TODO Remove this. Don't forget import button in template.
$scope.newDialog = function () {
- ngDialog.open(CustomslideForm.getDialog());
+ ngDialog.open(TopicForm.getDialog());
};
// cancel QuickEdit mode
$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 () {
angular.forEach($scope.items, function (item) {
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', [
'$scope',
'$state',
'$http',
'Projector',
- 'Assignment',
- 'Customslide',
- 'Motion',
+ 'Assignment', // TODO: Remove this after refactoring of data loading on start.
+ 'Topic', // TODO: Remove this after refactoring of data loading on start.
+ 'Motion', // TODO: Remove this after refactoring of data loading on start.
'Agenda',
- function($scope, $state, $http, Projector, Assignment, Customslide, Motion, Agenda) {
+ function($scope, $state, $http, Projector, Assignment, Topic, Motion, Agenda) {
$scope.$watch(
function() {
return Projector.lastModified(1);
@@ -572,10 +424,10 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
});
});
break;
- case 'core/customslide':
- Customslide.find(element.id).then(function(customslide) {
- Customslide.loadRelations(customslide, 'agenda_item').then(function() {
- $scope.AgendaItem = customslide.agenda_item;
+ case 'topics/topic':
+ Topic.find(element.id).then(function(topic) {
+ Topic.loadRelations(topic, 'agenda_item').then(function() {
+ $scope.AgendaItem = topic.agenda_item;
});
});
break;
diff --git a/openslides/agenda/static/templates/agenda/item-list.html b/openslides/agenda/static/templates/agenda/item-list.html
index 31e1d99fe..fb6a0469c 100644
--- a/openslides/agenda/static/templates/agenda/item-list.html
+++ b/openslides/agenda/static/templates/agenda/item-list.html
@@ -9,7 +9,7 @@
Sort agenda
-
+
Import
@@ -202,7 +202,7 @@
-
+
{{ item.getTitle() }} – QuickEdit
diff --git a/openslides/assignments/static/js/assignments/site.js b/openslides/assignments/static/js/assignments/site.js
index 259e15294..23074e1b2 100644
--- a/openslides/assignments/static/js/assignments/site.js
+++ b/openslides/assignments/static/js/assignments/site.js
@@ -82,8 +82,8 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
// (from assignment controller use AssignmentForm factory instead to open dialog in front
// of current view without redirect)
.state('assignments.assignment.detail.update', {
- onEnter: ['$stateParams', '$state', 'ngDialog', 'Assignment', 'Agenda',
- function($stateParams, $state, ngDialog, Assignment, Agenda) {
+ onEnter: ['$stateParams', '$state', 'ngDialog', 'Assignment',
+ function($stateParams, $state, ngDialog, Assignment) {
ngDialog.open({
template: 'static/templates/assignments/assignment-form.html',
controller: 'AssignmentUpdateCtrl',
diff --git a/openslides/core/access_permissions.py b/openslides/core/access_permissions.py
index 578dd409d..e2b553661 100644
--- a/openslides/core/access_permissions.py
+++ b/openslides/core/access_permissions.py
@@ -20,25 +20,6 @@ class ProjectorAccessPermissions(BaseAccessPermissions):
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):
"""
Access permissions container for Tag and TagViewSet.
diff --git a/openslides/core/apps.py b/openslides/core/apps.py
index d6a0987c8..ede9590c6 100644
--- a/openslides/core/apps.py
+++ b/openslides/core/apps.py
@@ -24,7 +24,6 @@ class CoreAppConfig(AppConfig):
from .views import (
ChatMessageViewSet,
ConfigViewSet,
- CustomSlideViewSet,
ProjectorViewSet,
TagViewSet,
)
@@ -40,7 +39,6 @@ class CoreAppConfig(AppConfig):
# Register viewsets.
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('CustomSlide').get_collection_string(), CustomSlideViewSet)
router.register(self.get_model('Tag').get_collection_string(), TagViewSet)
router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config')
diff --git a/openslides/core/migrations/0005_auto_20160918_2104.py b/openslides/core/migrations/0005_auto_20160918_2104.py
new file mode 100644
index 000000000..c35ab6b41
--- /dev/null
+++ b/openslides/core/migrations/0005_auto_20160918_2104.py
@@ -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',
+ ),
+ ]
diff --git a/openslides/core/models.py b/openslides/core/models.py
index b5b1a9258..2b6dfc2d0 100644
--- a/openslides/core/models.py
+++ b/openslides/core/models.py
@@ -1,17 +1,14 @@
from django.conf import settings
-from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session as DjangoSession
from django.db import models
from jsonfield import JSONField
-from openslides.mediafiles.models import Mediafile
from openslides.utils.models import RESTModelMixin
from openslides.utils.projector import ProjectorElement
from .access_permissions import (
ChatMessageAccessPermissions,
ConfigAccessPermissions,
- CustomSlideAccessPermissions,
ProjectorAccessPermissions,
TagAccessPermissions,
)
@@ -32,7 +29,7 @@ class Projector(RESTModelMixin, models.Model):
{
"881d875cf01741718ca926279ac9c99c": {
- "name": "core/customslide",
+ "name": "topics/topic",
"id": 1
},
"191c0878cdc04abfbd64f3177a21891a": {
@@ -155,61 +152,6 @@ class Projector(RESTModelMixin, models.Model):
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):
"""
Model for tags. This tags can be used for other models like agenda items,
diff --git a/openslides/core/projector.py b/openslides/core/projector.py
index ac1b5943b..52736b235 100644
--- a/openslides/core/projector.py
+++ b/openslides/core/projector.py
@@ -3,28 +3,7 @@ from django.utils.timezone import now
from ..utils.projector import ProjectorElement
from .config import config
from .exceptions import ProjectorException
-from .models import CustomSlide, 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
+from .models import Projector
class Clock(ProjectorElement):
diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py
index 5adac0e20..34ec9c2e2 100644
--- a/openslides/core/serializers.py
+++ b/openslides/core/serializers.py
@@ -1,6 +1,6 @@
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):
@@ -33,15 +33,6 @@ class ProjectorSerializer(ModelSerializer):
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):
"""
Serializer for core.models.Tag objects.
diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js
index 9b62f7395..df98b96fb 100644
--- a/openslides/core/static/js/core/base.js
+++ b/openslides/core/static/js/core/base.js
@@ -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', [
'DS',
function(DS) {
@@ -544,10 +500,9 @@ angular.module('OpenSlidesApp.core', [
.run([
'ChatMessage',
'Config',
- 'Customslide',
'Projector',
'Tag',
- function (ChatMessage, Config, Customslide, Projector, Tag) {}
+ function (ChatMessage, Config, Projector, Tag) {}
]);
}());
diff --git a/openslides/core/static/js/core/projector.js b/openslides/core/static/js/core/projector.js
index af42559ca..86aa8227b 100644
--- a/openslides/core/static/js/core/projector.js
+++ b/openslides/core/static/js/core/projector.js
@@ -42,10 +42,6 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
.config([
'slidesProvider',
function(slidesProvider) {
- slidesProvider.registerSlide('core/customslide', {
- template: 'static/templates/core/slide_customslide.html',
- });
-
slidesProvider.registerSlide('core/clock', {
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', [
'$scope',
function($scope) {
diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js
index e4feb622f..173c457e9 100644
--- a/openslides/core/static/js/core/site.js
+++ b/openslides/core/static/js/core/site.js
@@ -713,48 +713,6 @@ angular.module('OpenSlidesApp.core.site', [
templateUrl: 'static/templates/search.html',
})
- // customslide
- .state('core.customslide', {
- url: '/customslide',
- abstract: true,
- template: "",
- })
- .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
.state('core.tag', {
url: '/tag',
@@ -1144,7 +1102,7 @@ angular.module('OpenSlidesApp.core.site', [
if ($scope.filterMotion && result.urlState == 'motions.motion.detail') {
return result;
}
- if ($scope.filterAgenda && result.urlState == 'core.customslide.detail') {
+ if ($scope.filterAgenda && result.urlState == 'topics.topic.detail') {
return result;
}
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
.controller('ProjectorControlCtrl', [
'$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
.controller('TagListCtrl', [
'$scope',
diff --git a/openslides/core/static/templates/core/slide_customslide.html b/openslides/core/static/templates/core/slide_customslide.html
deleted file mode 100644
index 1be7c0fa8..000000000
--- a/openslides/core/static/templates/core/slide_customslide.html
+++ /dev/null
@@ -1,4 +0,0 @@
-
- {{ customslide.agenda_item.getTitle() }}
-
-
diff --git a/openslides/core/views.py b/openslides/core/views.py
index 0c3ea6ef4..7d8fa7aa0 100644
--- a/openslides/core/views.py
+++ b/openslides/core/views.py
@@ -37,13 +37,12 @@ from openslides.utils.search import search
from .access_permissions import (
ChatMessageAccessPermissions,
ConfigAccessPermissions,
- CustomSlideAccessPermissions,
ProjectorAccessPermissions,
TagAccessPermissions,
)
from .config import config
from .exceptions import ConfigError, ConfigNotFound
-from .models import ChatMessage, CustomSlide, Projector, Tag
+from .models import ChatMessage, Projector, Tag
# Special Django views
@@ -449,27 +448,6 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
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):
"""
API endpoint for tags.
diff --git a/openslides/global_settings.py b/openslides/global_settings.py
index 7647c8acf..f236ed9fc 100644
--- a/openslides/global_settings.py
+++ b/openslides/global_settings.py
@@ -17,6 +17,7 @@ INSTALLED_APPS = [
'rest_framework',
'channels',
'openslides.agenda',
+ 'openslides.topics',
'openslides.motions',
'openslides.assignments',
'openslides.mediafiles',
diff --git a/openslides/topics/__init__.py b/openslides/topics/__init__.py
new file mode 100644
index 000000000..f12ac1e7f
--- /dev/null
+++ b/openslides/topics/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'openslides.topics.apps.TopicsAppConfig'
diff --git a/openslides/topics/access_permissions.py b/openslides/topics/access_permissions.py
new file mode 100644
index 000000000..c77e11214
--- /dev/null
+++ b/openslides/topics/access_permissions.py
@@ -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
diff --git a/openslides/topics/apps.py b/openslides/topics/apps.py
new file mode 100644
index 000000000..15f37e93e
--- /dev/null
+++ b/openslides/topics/apps.py
@@ -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)
diff --git a/openslides/topics/migrations/0001_initial.py b/openslides/topics/migrations/0001_initial.py
new file mode 100644
index 000000000..6e9c4ec47
--- /dev/null
+++ b/openslides/topics/migrations/0001_initial.py
@@ -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),
+ ),
+ ]
diff --git a/openslides/topics/migrations/__init__.py b/openslides/topics/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/openslides/topics/models.py b/openslides/topics/models.py
new file mode 100644
index 000000000..25a3b967e
--- /dev/null
+++ b/openslides/topics/models.py
@@ -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))
diff --git a/openslides/topics/projector.py b/openslides/topics/projector.py
new file mode 100644
index 000000000..642379452
--- /dev/null
+++ b/openslides/topics/projector.py
@@ -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
diff --git a/openslides/topics/serializers.py b/openslides/topics/serializers.py
new file mode 100644
index 000000000..6ae92c1e8
--- /dev/null
+++ b/openslides/topics/serializers.py
@@ -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')
diff --git a/openslides/topics/static/js/topics/base.js b/openslides/topics/static/js/topics/base.js
new file mode 100644
index 000000000..4cceb5b37
--- /dev/null
+++ b/openslides/topics/static/js/topics/base.js
@@ -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) {}]);
+
+}());
diff --git a/openslides/topics/static/js/topics/projector.js b/openslides/topics/static/js/topics/projector.js
new file mode 100644
index 000000000..e910351c1
--- /dev/null
+++ b/openslides/topics/static/js/topics/projector.js
@@ -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');
+ }
+]);
+
+})();
diff --git a/openslides/topics/static/js/topics/site.js b/openslides/topics/static/js/topics/site.js
new file mode 100644
index 000000000..6f00792ab
--- /dev/null
+++ b/openslides/topics/static/js/topics/site.js
@@ -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: "",
+ })
+
+ .state('topics.topic', {
+ url: '/topic',
+ abstract: true,
+ template: "",
+ })
+ .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';
+ };
+ }
+]);
+
+}());
diff --git a/openslides/topics/static/templates/topics/slide_topic.html b/openslides/topics/static/templates/topics/slide_topic.html
new file mode 100644
index 000000000..0e6e1ccf5
--- /dev/null
+++ b/openslides/topics/static/templates/topics/slide_topic.html
@@ -0,0 +1,4 @@
+
+ {{ topic.agenda_item.getTitle() }}
+
+
diff --git a/openslides/core/static/templates/core/customslide-detail.html b/openslides/topics/static/templates/topics/topic-detail.html
similarity index 58%
rename from openslides/core/static/templates/core/customslide-detail.html
rename to openslides/topics/static/templates/topics/topic-detail.html
index 5beb233c9..2d2854da8 100644
--- a/openslides/core/static/templates/core/customslide-detail.html
+++ b/openslides/topics/static/templates/topics/topic-detail.html
@@ -6,34 +6,34 @@
Back to overview
-
+
List of speakers
+ ng-class="{ 'btn-primary': topic.isProjected() }"
+ ng-click="topic.project()"
+ title="{{ 'Project topic' | translate }}">
-
- {{ customslide.agenda_item.getTitle() }}
- Agenda item
+ {{ topic.agenda_item.getTitle() }}
+ Topic
-
- Attachments
+
+ Attachments
- -
+
-
{{ attachment.title_or_filename }}
diff --git a/openslides/core/static/templates/core/customslide-form.html b/openslides/topics/static/templates/topics/topic-form.html
similarity index 58%
rename from openslides/core/static/templates/core/customslide-form.html
rename to openslides/topics/static/templates/topics/topic-form.html
index f437f8c95..a5329b05a 100644
--- a/openslides/core/static/templates/core/customslide-form.html
+++ b/openslides/topics/static/templates/topics/topic-form.html
@@ -1,13 +1,13 @@
-
Edit agenda item
-New agenda item
+Edit topic
+New topic
{{ alert.msg }}
-
- Import agenda items
+ Import topics
Import by copy/paste
- Copy and paste your agenda item titles in this textbox.
-Keep each item in a single line.
+ Copy and paste your topic titles in this textbox. Keep each item in a single line.
{{ items.length - itemsFailed.length }}
- items will be imported.
+ topics will be imported.
{{ itemsImported.length }}
- items were successfully imported.
+ topics were successfully imported.
@@ -135,7 +134,7 @@ Keep each item in a single line.
Clear preview
diff --git a/openslides/topics/views.py b/openslides/topics/views.py
new file mode 100644
index 000000000..e4515037e
--- /dev/null
+++ b/openslides/topics/views.py
@@ -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
diff --git a/tests/integration/agenda/test_models.py b/tests/integration/agenda/test_models.py
index 073abd8b8..b0df81bb4 100644
--- a/tests/integration/agenda/test_models.py
+++ b/tests/integration/agenda/test_models.py
@@ -1,5 +1,5 @@
from openslides.agenda.models import Item
-from openslides.core.models import CustomSlide
+from openslides.topics.models import Topic
from openslides.utils.test import TestCase
@@ -9,7 +9,7 @@ class TestItemManager(TestCase):
Test that get_root_and_children needs only one db query.
"""
for i in range(10):
- CustomSlide.objects.create(title='item{}'.format(i))
+ Topic.objects.create(title='item{}'.format(i))
with self.assertNumQueries(1):
Item.objects.get_root_and_children()
diff --git a/tests/integration/agenda/test_views.py b/tests/integration/agenda/test_views.py
index c2ca7e537..1ddb7ce42 100644
--- a/tests/integration/agenda/test_views.py
+++ b/tests/integration/agenda/test_views.py
@@ -3,15 +3,15 @@ import json
from rest_framework.test import APIClient
from openslides.agenda.models import Item
-from openslides.core.models import CustomSlide
+from openslides.topics.models import Topic
from openslides.utils.test import TestCase
class AgendaTreeTest(TestCase):
def setUp(self):
- CustomSlide.objects.create(title='item1')
- item2 = CustomSlide.objects.create(title='item2').agenda_item
- item3 = CustomSlide.objects.create(title='item2a').agenda_item
+ Topic.objects.create(title='item1')
+ item2 = Topic.objects.create(title='item2').agenda_item
+ item3 = Topic.objects.create(title='item2a').agenda_item
item3.parent = item2
item3.save()
self.client = APIClient()
@@ -90,7 +90,7 @@ class TestAgendaPDF(TestCase):
"""
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')
response = self.client.get('/agenda/print/')
diff --git a/tests/integration/agenda/test_viewsets.py b/tests/integration/agenda/test_viewsets.py
index 8b2f752a0..1f36c2b27 100644
--- a/tests/integration/agenda/test_viewsets.py
+++ b/tests/integration/agenda/test_viewsets.py
@@ -5,7 +5,8 @@ from rest_framework.test import APIClient
from openslides.agenda.models import Item, Speaker
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
@@ -16,7 +17,7 @@ class RetrieveItem(TestCase):
def setUp(self):
self.client = APIClient()
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):
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.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(
username='test_user_jooSaex1bo5ooPhuphae',
password='test_password_e6paev4zeeh9n')
@@ -164,7 +165,7 @@ class Speak(TestCase):
def setUp(self):
self.client = APIClient()
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(
username='test_user_Aigh4vohb3seecha4aa4',
password='test_password_eneupeeVo5deilixoo8j')
@@ -273,19 +274,19 @@ class Numbering(TestCase):
def setUp(self):
self.client = APIClient()
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.weight = 1
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.weight = 2
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.parent = self.item_2
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.weight = 3
self.item_3.save()
diff --git a/tests/integration/core/test_views.py b/tests/integration/core/test_views.py
index 8c33e2b71..626893e9a 100644
--- a/tests/integration/core/test_views.py
+++ b/tests/integration/core/test_views.py
@@ -6,7 +6,8 @@ from rest_framework.test import APIClient
from openslides import __version__ as version
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.test import TestCase
@@ -17,10 +18,10 @@ class ProjectorAPI(TestCase):
"""
def test_slide_on_default_projector(self):
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.config = {
- 'aae4a07b26534cfb9af4232f361dce73': {'name': 'core/customslide', 'id': customslide.id}}
+ 'aae4a07b26534cfb9af4232f361dce73': {'name': 'topics/topic', 'id': topic.id}}
default_projector.save()
response = self.client.get(reverse('projector-detail', args=['1']))
@@ -30,9 +31,9 @@ class ProjectorAPI(TestCase):
'id': 1,
'elements': {
'aae4a07b26534cfb9af4232f361dce73':
- {'id': customslide.id,
+ {'id': topic.id,
'uuid': 'aae4a07b26534cfb9af4232f361dce73',
- 'name': 'core/customslide'}},
+ 'name': 'topics/topic'}},
'scale': 0,
'scroll': 0,
'width': 1024,
diff --git a/tests/old/agenda/test_list_of_speakers.py b/tests/old/agenda/test_list_of_speakers.py
index 4569a80e6..60d56a05b 100644
--- a/tests/old/agenda/test_list_of_speakers.py
+++ b/tests/old/agenda/test_list_of_speakers.py
@@ -1,5 +1,5 @@
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.utils.exceptions import OpenSlidesError
from openslides.utils.test import TestCase
@@ -7,8 +7,8 @@ from openslides.utils.test import TestCase
class ListOfSpeakerModelTests(TestCase):
def setUp(self):
- self.item1 = CustomSlide.objects.create(title='item1').agenda_item
- self.item2 = CustomSlide.objects.create(title='item2').agenda_item
+ self.item1 = Topic.objects.create(title='item1').agenda_item
+ self.item2 = Topic.objects.create(title='item2').agenda_item
self.speaker1 = User.objects.create(username='user1')
self.speaker2 = User.objects.create(username='user2')
|