From cab53f043497aba0140e9f45d287b9499e8f74e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Sun, 18 Sep 2016 22:14:24 +0200 Subject: [PATCH] Moved custom slides to own app topics for better app structure. Renamed model to Topic. Added migrations file. Fixed #2402. --- CHANGELOG | 1 + openslides/agenda/static/js/agenda/site.js | 180 +------- .../static/templates/agenda/item-list.html | 4 +- .../assignments/static/js/assignments/site.js | 4 +- openslides/core/access_permissions.py | 19 - openslides/core/apps.py | 2 - .../migrations/0005_auto_20160918_2104.py | 69 ++++ openslides/core/models.py | 60 +-- openslides/core/projector.py | 23 +- openslides/core/serializers.py | 11 +- openslides/core/static/js/core/base.js | 47 +-- openslides/core/static/js/core/projector.js | 16 - openslides/core/static/js/core/site.js | 221 +--------- .../templates/core/slide_customslide.html | 4 - openslides/core/views.py | 24 +- openslides/global_settings.py | 1 + openslides/topics/__init__.py | 1 + openslides/topics/access_permissions.py | 20 + openslides/topics/apps.py | 20 + openslides/topics/migrations/0001_initial.py | 31 ++ openslides/topics/migrations/__init__.py | 0 openslides/topics/models.py | 53 +++ openslides/topics/projector.py | 24 ++ openslides/topics/serializers.py | 12 + openslides/topics/static/js/topics/base.js | 53 +++ .../topics/static/js/topics/projector.js | 28 ++ openslides/topics/static/js/topics/site.js | 391 ++++++++++++++++++ .../static/templates/topics/slide_topic.html | 4 + .../templates/topics/topic-detail.html} | 20 +- .../static/templates/topics/topic-form.html} | 8 +- .../templates/topics/topic-import.html} | 13 +- openslides/topics/views.py | 25 ++ tests/integration/agenda/test_models.py | 4 +- tests/integration/agenda/test_views.py | 10 +- tests/integration/agenda/test_viewsets.py | 17 +- tests/integration/core/test_views.py | 11 +- tests/old/agenda/test_list_of_speakers.py | 6 +- 37 files changed, 804 insertions(+), 633 deletions(-) create mode 100644 openslides/core/migrations/0005_auto_20160918_2104.py delete mode 100644 openslides/core/static/templates/core/slide_customslide.html create mode 100644 openslides/topics/__init__.py create mode 100644 openslides/topics/access_permissions.py create mode 100644 openslides/topics/apps.py create mode 100644 openslides/topics/migrations/0001_initial.py create mode 100644 openslides/topics/migrations/__init__.py create mode 100644 openslides/topics/models.py create mode 100644 openslides/topics/projector.py create mode 100644 openslides/topics/serializers.py create mode 100644 openslides/topics/static/js/topics/base.js create mode 100644 openslides/topics/static/js/topics/projector.js create mode 100644 openslides/topics/static/js/topics/site.js create mode 100644 openslides/topics/static/templates/topics/slide_topic.html rename openslides/{core/static/templates/core/customslide-detail.html => topics/static/templates/topics/topic-detail.html} (58%) rename openslides/{core/static/templates/core/customslide-form.html => topics/static/templates/topics/topic-form.html} (58%) rename openslides/{agenda/static/templates/agenda/item-import.html => topics/static/templates/topics/topic-import.html} (93%) create mode 100644 openslides/topics/views.py 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.

@@ -114,20 +113,20 @@ Keep each item in a single line.

{{ itemsFailed.length }} - agenda items will be not imported. + topics will be not imported.
{{ 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')