Moved custom slides to own app topics for better app structure.
Renamed model to Topic. Added migrations file. Fixed #2402.
This commit is contained in:
parent
53c4932171
commit
cab53f0434
@ -18,6 +18,7 @@ Assignments:
|
|||||||
Core:
|
Core:
|
||||||
- Added support for big assemblies with lots of users.
|
- Added support for big assemblies with lots of users.
|
||||||
- Added HTML support for messages on the projector.
|
- Added HTML support for messages on the projector.
|
||||||
|
- Moved custom slides to own app "topics". Renamed it to "Topic".
|
||||||
|
|
||||||
Motions:
|
Motions:
|
||||||
- Added origin field.
|
- Added origin field.
|
||||||
|
@ -68,10 +68,6 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
|
|||||||
url: '/sort',
|
url: '/sort',
|
||||||
controller: 'AgendaSortCtrl',
|
controller: 'AgendaSortCtrl',
|
||||||
})
|
})
|
||||||
.state('agenda.item.import', {
|
|
||||||
url: '/import',
|
|
||||||
controller: 'AgendaImportCtrl',
|
|
||||||
})
|
|
||||||
.state('agenda.current-list-of-speakers', {
|
.state('agenda.current-list-of-speakers', {
|
||||||
url: '/speakers',
|
url: '/speakers',
|
||||||
controller: 'ListOfSpeakersViewCtrl',
|
controller: 'ListOfSpeakersViewCtrl',
|
||||||
@ -100,10 +96,10 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
|
|||||||
'operator',
|
'operator',
|
||||||
'ngDialog',
|
'ngDialog',
|
||||||
'Agenda',
|
'Agenda',
|
||||||
'CustomslideForm',
|
'TopicForm', // TODO: Remove this dependency. Use template hook for "New" and "Import" buttons.
|
||||||
'AgendaTree',
|
'AgendaTree',
|
||||||
'Projector',
|
'Projector',
|
||||||
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, CustomslideForm, AgendaTree, Projector) {
|
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm, AgendaTree, Projector) {
|
||||||
// Bind agenda tree to the scope
|
// Bind agenda tree to the scope
|
||||||
$scope.$watch(function () {
|
$scope.$watch(function () {
|
||||||
return Agenda.lastModified();
|
return Agenda.lastModified();
|
||||||
@ -125,11 +121,12 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
|
|||||||
};
|
};
|
||||||
|
|
||||||
// check open permission
|
// check open permission
|
||||||
|
// TODO: Use generic solution here.
|
||||||
$scope.isAllowedToSeeOpenLink = function (item) {
|
$scope.isAllowedToSeeOpenLink = function (item) {
|
||||||
var collection = item.content_object.collection;
|
var collection = item.content_object.collection;
|
||||||
switch (collection) {
|
switch (collection) {
|
||||||
case 'core/customslide':
|
case 'topics/topic':
|
||||||
return operator.hasPerms('core.can_manage_projector');
|
return operator.hasPerms('agenda.can_see');
|
||||||
case 'motions/motion':
|
case 'motions/motion':
|
||||||
return operator.hasPerms('motions.can_see');
|
return operator.hasPerms('motions.can_see');
|
||||||
case 'assignments/assignment':
|
case 'assignments/assignment':
|
||||||
@ -138,9 +135,9 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// open new dialog
|
// open dialog for new topics // TODO Remove this. Don't forget import button in template.
|
||||||
$scope.newDialog = function () {
|
$scope.newDialog = function () {
|
||||||
ngDialog.open(CustomslideForm.getDialog());
|
ngDialog.open(TopicForm.getDialog());
|
||||||
};
|
};
|
||||||
// cancel QuickEdit mode
|
// cancel QuickEdit mode
|
||||||
$scope.cancelQuickEdit = function (item) {
|
$scope.cancelQuickEdit = function (item) {
|
||||||
@ -185,7 +182,7 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// delete selected items only if items are customslides
|
// delete selected items
|
||||||
$scope.deleteMultiple = function () {
|
$scope.deleteMultiple = function () {
|
||||||
angular.forEach($scope.items, function (item) {
|
angular.forEach($scope.items, function (item) {
|
||||||
if (item.selected) {
|
if (item.selected) {
|
||||||
@ -401,161 +398,16 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
.controller('AgendaImportCtrl', [
|
|
||||||
'$scope',
|
|
||||||
'gettext',
|
|
||||||
'Agenda',
|
|
||||||
'Customslide',
|
|
||||||
function($scope, gettext, Agenda, Customslide) {
|
|
||||||
// import from textarea
|
|
||||||
$scope.importByLine = function () {
|
|
||||||
if ($scope.itemlist) {
|
|
||||||
$scope.titleItems = $scope.itemlist[0].split("\n");
|
|
||||||
$scope.importcounter = 0;
|
|
||||||
$scope.titleItems.forEach(function(title, index) {
|
|
||||||
var item = {title: title};
|
|
||||||
// TODO: create all items in bulk mode
|
|
||||||
Customslide.create(item).then(
|
|
||||||
function(success) {
|
|
||||||
// find related agenda item
|
|
||||||
Agenda.find(success.agenda_item_id).then(function(item) {
|
|
||||||
// import all items as type AGENDA_ITEM = 1
|
|
||||||
item.type = 1;
|
|
||||||
item.weight = 1000 + index;
|
|
||||||
Agenda.save(item);
|
|
||||||
});
|
|
||||||
$scope.importcounter++;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// *** CSV import ***
|
|
||||||
// set initial data for csv import
|
|
||||||
$scope.items = [];
|
|
||||||
$scope.separator = ',';
|
|
||||||
$scope.encoding = 'UTF-8';
|
|
||||||
$scope.encodingOptions = ['UTF-8', 'ISO-8859-1'];
|
|
||||||
$scope.accept = '.csv, .txt';
|
|
||||||
$scope.csv = {
|
|
||||||
content: null,
|
|
||||||
header: true,
|
|
||||||
headerVisible: false,
|
|
||||||
separator: $scope.separator,
|
|
||||||
separatorVisible: false,
|
|
||||||
encoding: $scope.encoding,
|
|
||||||
encodingVisible: false,
|
|
||||||
accept: $scope.accept,
|
|
||||||
result: null
|
|
||||||
};
|
|
||||||
// set csv file encoding
|
|
||||||
$scope.setEncoding = function () {
|
|
||||||
$scope.csv.encoding = $scope.encoding;
|
|
||||||
};
|
|
||||||
// set csv file encoding
|
|
||||||
$scope.setSeparator = function () {
|
|
||||||
$scope.csv.separator = $scope.separator;
|
|
||||||
};
|
|
||||||
// detect if csv file is loaded
|
|
||||||
$scope.$watch('csv.result', function () {
|
|
||||||
$scope.items = [];
|
|
||||||
var quotionRe = /^"(.*)"$/;
|
|
||||||
angular.forEach($scope.csv.result, function (item, index) {
|
|
||||||
// title
|
|
||||||
if (item.title) {
|
|
||||||
item.title = item.title.replace(quotionRe, '$1');
|
|
||||||
}
|
|
||||||
if (!item.title) {
|
|
||||||
item.importerror = true;
|
|
||||||
item.title_error = gettext('Error: Title is required.');
|
|
||||||
}
|
|
||||||
// text
|
|
||||||
if (item.text) {
|
|
||||||
item.text = item.text.replace(quotionRe, '$1');
|
|
||||||
}
|
|
||||||
// duration
|
|
||||||
if (item.duration) {
|
|
||||||
item.duration = item.duration.replace(quotionRe, '$1');
|
|
||||||
}
|
|
||||||
// comment
|
|
||||||
if (item.comment) {
|
|
||||||
item.comment = item.comment.replace(quotionRe, '$1');
|
|
||||||
}
|
|
||||||
// is_hidden
|
|
||||||
if (item.is_hidden) {
|
|
||||||
item.is_hidden = item.is_hidden.replace(quotionRe, '$1');
|
|
||||||
if (item.is_hidden == '1') {
|
|
||||||
item.type = 2;
|
|
||||||
} else {
|
|
||||||
item.type = 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
item.type = 1;
|
|
||||||
}
|
|
||||||
// set weight for right csv row order
|
|
||||||
// (Use 1000+ to protect existing items and prevent collision
|
|
||||||
// with new items which use weight 10000 as default.)
|
|
||||||
item.weight = 1000 + index;
|
|
||||||
$scope.items.push(item);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// import from csv file
|
|
||||||
$scope.import = function () {
|
|
||||||
$scope.csvImporting = true;
|
|
||||||
angular.forEach($scope.items, function (item) {
|
|
||||||
if (!item.importerror) {
|
|
||||||
Customslide.create(item).then(
|
|
||||||
function(success) {
|
|
||||||
item.imported = true;
|
|
||||||
// find related agenda item
|
|
||||||
Agenda.find(success.agenda_item_id).then(function(agendaItem) {
|
|
||||||
agendaItem.duration = item.duration;
|
|
||||||
agendaItem.comment = item.comment;
|
|
||||||
agendaItem.type = item.type;
|
|
||||||
agendaItem.weight = item.weight;
|
|
||||||
Agenda.save(agendaItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$scope.csvimported = true;
|
|
||||||
};
|
|
||||||
$scope.clear = function () {
|
|
||||||
$scope.csv.result = null;
|
|
||||||
};
|
|
||||||
// download CSV example file
|
|
||||||
$scope.downloadCSVExample = function () {
|
|
||||||
var element = document.getElementById('downloadLink');
|
|
||||||
var csvRows = [
|
|
||||||
// column header line
|
|
||||||
['title', 'text', 'duration', 'comment', 'is_hidden'],
|
|
||||||
// example entries
|
|
||||||
['Demo 1', 'Demo text 1', '1:00', 'test comment', ''],
|
|
||||||
['Break', '', '0:10', '', '1'],
|
|
||||||
['Demo 2', 'Demo text 2', '1:30', '', '']
|
|
||||||
|
|
||||||
];
|
|
||||||
var csvString = csvRows.join("%0A");
|
|
||||||
element.href = 'data:text/csv;charset=utf-8,' + csvString;
|
|
||||||
element.download = 'agenda-example.csv';
|
|
||||||
element.target = '_blank';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
.controller('ListOfSpeakersViewCtrl', [
|
.controller('ListOfSpeakersViewCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
'$state',
|
'$state',
|
||||||
'$http',
|
'$http',
|
||||||
'Projector',
|
'Projector',
|
||||||
'Assignment',
|
'Assignment', // TODO: Remove this after refactoring of data loading on start.
|
||||||
'Customslide',
|
'Topic', // TODO: Remove this after refactoring of data loading on start.
|
||||||
'Motion',
|
'Motion', // TODO: Remove this after refactoring of data loading on start.
|
||||||
'Agenda',
|
'Agenda',
|
||||||
function($scope, $state, $http, Projector, Assignment, Customslide, Motion, Agenda) {
|
function($scope, $state, $http, Projector, Assignment, Topic, Motion, Agenda) {
|
||||||
$scope.$watch(
|
$scope.$watch(
|
||||||
function() {
|
function() {
|
||||||
return Projector.lastModified(1);
|
return Projector.lastModified(1);
|
||||||
@ -572,10 +424,10 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'core/customslide':
|
case 'topics/topic':
|
||||||
Customslide.find(element.id).then(function(customslide) {
|
Topic.find(element.id).then(function(topic) {
|
||||||
Customslide.loadRelations(customslide, 'agenda_item').then(function() {
|
Topic.loadRelations(topic, 'agenda_item').then(function() {
|
||||||
$scope.AgendaItem = customslide.agenda_item;
|
$scope.AgendaItem = topic.agenda_item;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<i class="fa fa-sitemap fa-lg"></i>
|
<i class="fa fa-sitemap fa-lg"></i>
|
||||||
<translate>Sort agenda</translate>
|
<translate>Sort agenda</translate>
|
||||||
</a>
|
</a>
|
||||||
<a ui-sref="agenda.item.import" os-perms="agenda.can_manage" class="btn btn-default btn-sm">
|
<a ui-sref="topics.topic.import" os-perms="agenda.can_manage" class="btn btn-default btn-sm">
|
||||||
<i class="fa fa-download fa-lg"></i>
|
<i class="fa fa-download fa-lg"></i>
|
||||||
<translate>Import</translate>
|
<translate>Import</translate>
|
||||||
</a>
|
</a>
|
||||||
@ -202,7 +202,7 @@
|
|||||||
<span os-perms="!agenda.can_manage">
|
<span os-perms="!agenda.can_manage">
|
||||||
<i ng-if="item.closed" class="fa fa-check-square-o"></i>
|
<i ng-if="item.closed" class="fa fa-check-square-o"></i>
|
||||||
</span>
|
</span>
|
||||||
<input os-perms="agenda.can_manage" type="checkbox" ng-model="item.closed" ng-change="save(item.id);">
|
<input os-perms="agenda.can_manage" type="checkbox" ng-model="item.closed" ng-change="save(item);">
|
||||||
<!-- quickEdit columns -->
|
<!-- quickEdit columns -->
|
||||||
<td ng-show="item.quickEdit" os-perms="agenda.can_manage" colspan="3">
|
<td ng-show="item.quickEdit" os-perms="agenda.can_manage" colspan="3">
|
||||||
<h4>{{ item.getTitle() }} <span class="text-muted">– QuickEdit</span></h4>
|
<h4>{{ item.getTitle() }} <span class="text-muted">– QuickEdit</span></h4>
|
||||||
|
@ -82,8 +82,8 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
|
|||||||
// (from assignment controller use AssignmentForm factory instead to open dialog in front
|
// (from assignment controller use AssignmentForm factory instead to open dialog in front
|
||||||
// of current view without redirect)
|
// of current view without redirect)
|
||||||
.state('assignments.assignment.detail.update', {
|
.state('assignments.assignment.detail.update', {
|
||||||
onEnter: ['$stateParams', '$state', 'ngDialog', 'Assignment', 'Agenda',
|
onEnter: ['$stateParams', '$state', 'ngDialog', 'Assignment',
|
||||||
function($stateParams, $state, ngDialog, Assignment, Agenda) {
|
function($stateParams, $state, ngDialog, Assignment) {
|
||||||
ngDialog.open({
|
ngDialog.open({
|
||||||
template: 'static/templates/assignments/assignment-form.html',
|
template: 'static/templates/assignments/assignment-form.html',
|
||||||
controller: 'AssignmentUpdateCtrl',
|
controller: 'AssignmentUpdateCtrl',
|
||||||
|
@ -20,25 +20,6 @@ class ProjectorAccessPermissions(BaseAccessPermissions):
|
|||||||
return ProjectorSerializer
|
return ProjectorSerializer
|
||||||
|
|
||||||
|
|
||||||
class CustomSlideAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for CustomSlide and CustomSlideViewSet.
|
|
||||||
"""
|
|
||||||
def check_permissions(self, user):
|
|
||||||
"""
|
|
||||||
Returns True if the user has read access model instances.
|
|
||||||
"""
|
|
||||||
return user.has_perm('core.can_manage_projector')
|
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
|
||||||
"""
|
|
||||||
Returns serializer class.
|
|
||||||
"""
|
|
||||||
from .serializers import CustomSlideSerializer
|
|
||||||
|
|
||||||
return CustomSlideSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class TagAccessPermissions(BaseAccessPermissions):
|
class TagAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions container for Tag and TagViewSet.
|
Access permissions container for Tag and TagViewSet.
|
||||||
|
@ -24,7 +24,6 @@ class CoreAppConfig(AppConfig):
|
|||||||
from .views import (
|
from .views import (
|
||||||
ChatMessageViewSet,
|
ChatMessageViewSet,
|
||||||
ConfigViewSet,
|
ConfigViewSet,
|
||||||
CustomSlideViewSet,
|
|
||||||
ProjectorViewSet,
|
ProjectorViewSet,
|
||||||
TagViewSet,
|
TagViewSet,
|
||||||
)
|
)
|
||||||
@ -40,7 +39,6 @@ class CoreAppConfig(AppConfig):
|
|||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet)
|
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet)
|
||||||
router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet)
|
router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet)
|
||||||
router.register(self.get_model('CustomSlide').get_collection_string(), CustomSlideViewSet)
|
|
||||||
router.register(self.get_model('Tag').get_collection_string(), TagViewSet)
|
router.register(self.get_model('Tag').get_collection_string(), TagViewSet)
|
||||||
router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config')
|
router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config')
|
||||||
|
|
||||||
|
69
openslides/core/migrations/0005_auto_20160918_2104.py
Normal file
69
openslides/core/migrations/0005_auto_20160918_2104.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.1 on 2016-09-18 19:04
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from openslides.utils.autoupdate import (
|
||||||
|
inform_changed_data_receiver,
|
||||||
|
inform_deleted_data_receiver,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def move_custom_slides_to_topics(apps, schema_editor):
|
||||||
|
# Disconnect autoupdate. We do not want to trigger it here.
|
||||||
|
models.signals.post_save.disconnect(dispatch_uid='inform_changed_data_receiver')
|
||||||
|
models.signals.post_save.disconnect(dispatch_uid='inform_deleted_data_receiver')
|
||||||
|
|
||||||
|
# We get the model from the versioned app registry;
|
||||||
|
# if we directly import it, it will be the wrong version.
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
CustomSlide = apps.get_model('core', 'CustomSlide')
|
||||||
|
Item = apps.get_model('agenda', 'Item')
|
||||||
|
Topic = apps.get_model('topics', 'Topic')
|
||||||
|
|
||||||
|
# Copy data.
|
||||||
|
content_type_custom_slide = ContentType.objects.get_for_model(CustomSlide)
|
||||||
|
content_type_topic = ContentType.objects.get_for_model(Topic)
|
||||||
|
for custom_slide in CustomSlide.objects.all():
|
||||||
|
# This line does not create a new Item because this migration model has
|
||||||
|
# no method 'get_agenda_title()'. See agenda/signals.py.
|
||||||
|
topic = Topic.objects.create(title=custom_slide.title, text=custom_slide.text)
|
||||||
|
topic.attachments.add(*custom_slide.attachments.all())
|
||||||
|
item = Item.objects.get(object_id=custom_slide.pk, content_type=content_type_custom_slide)
|
||||||
|
item.object_id = topic.pk
|
||||||
|
item.content_type = content_type_topic
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
# Delete old data.
|
||||||
|
CustomSlide.objects.all().delete()
|
||||||
|
content_type_custom_slide.delete()
|
||||||
|
|
||||||
|
# Reconnect autoupdate.
|
||||||
|
models.signals.post_save.connect(
|
||||||
|
inform_changed_data_receiver,
|
||||||
|
dispatch_uid='inform_changed_data_receiver')
|
||||||
|
models.signals.post_delete.connect(
|
||||||
|
inform_deleted_data_receiver,
|
||||||
|
dispatch_uid='inform_deleted_data_receiver')
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0004_projector_resolution'),
|
||||||
|
('topics', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
move_custom_slides_to_topics
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='customslide',
|
||||||
|
name='attachments',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='CustomSlide',
|
||||||
|
),
|
||||||
|
]
|
@ -1,17 +1,14 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.contrib.sessions.models import Session as DjangoSession
|
from django.contrib.sessions.models import Session as DjangoSession
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
|
|
||||||
from openslides.mediafiles.models import Mediafile
|
|
||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
from openslides.utils.projector import ProjectorElement
|
from openslides.utils.projector import ProjectorElement
|
||||||
|
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
ChatMessageAccessPermissions,
|
ChatMessageAccessPermissions,
|
||||||
ConfigAccessPermissions,
|
ConfigAccessPermissions,
|
||||||
CustomSlideAccessPermissions,
|
|
||||||
ProjectorAccessPermissions,
|
ProjectorAccessPermissions,
|
||||||
TagAccessPermissions,
|
TagAccessPermissions,
|
||||||
)
|
)
|
||||||
@ -32,7 +29,7 @@ class Projector(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
{
|
{
|
||||||
"881d875cf01741718ca926279ac9c99c": {
|
"881d875cf01741718ca926279ac9c99c": {
|
||||||
"name": "core/customslide",
|
"name": "topics/topic",
|
||||||
"id": 1
|
"id": 1
|
||||||
},
|
},
|
||||||
"191c0878cdc04abfbd64f3177a21891a": {
|
"191c0878cdc04abfbd64f3177a21891a": {
|
||||||
@ -155,61 +152,6 @@ class Projector(RESTModelMixin, models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class CustomSlide(RESTModelMixin, models.Model):
|
|
||||||
"""
|
|
||||||
Model for slides with custom content.
|
|
||||||
"""
|
|
||||||
access_permissions = CustomSlideAccessPermissions()
|
|
||||||
|
|
||||||
title = models.CharField(
|
|
||||||
max_length=256)
|
|
||||||
text = models.TextField(
|
|
||||||
blank=True)
|
|
||||||
weight = models.IntegerField(
|
|
||||||
default=0)
|
|
||||||
attachments = models.ManyToManyField(
|
|
||||||
Mediafile,
|
|
||||||
blank=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
default_permissions = ()
|
|
||||||
ordering = ('weight', 'title', )
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
@property
|
|
||||||
def agenda_item(self):
|
|
||||||
"""
|
|
||||||
Returns the related agenda item.
|
|
||||||
"""
|
|
||||||
# TODO: Move the agenda app in the core app to fix circular dependencies
|
|
||||||
from openslides.agenda.models import Item
|
|
||||||
content_type = ContentType.objects.get_for_model(self)
|
|
||||||
return Item.objects.get(object_id=self.pk, content_type=content_type)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def agenda_item_id(self):
|
|
||||||
"""
|
|
||||||
Returns the id of the agenda item object related to this object.
|
|
||||||
"""
|
|
||||||
return self.agenda_item.pk
|
|
||||||
|
|
||||||
def get_agenda_title(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
def get_agenda_list_view_title(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
def get_search_index_string(self):
|
|
||||||
"""
|
|
||||||
Returns a string that can be indexed for the search.
|
|
||||||
"""
|
|
||||||
return " ".join((
|
|
||||||
self.title,
|
|
||||||
self.text))
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(RESTModelMixin, models.Model):
|
class Tag(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Model for tags. This tags can be used for other models like agenda items,
|
Model for tags. This tags can be used for other models like agenda items,
|
||||||
|
@ -3,28 +3,7 @@ from django.utils.timezone import now
|
|||||||
from ..utils.projector import ProjectorElement
|
from ..utils.projector import ProjectorElement
|
||||||
from .config import config
|
from .config import config
|
||||||
from .exceptions import ProjectorException
|
from .exceptions import ProjectorException
|
||||||
from .models import CustomSlide, Projector
|
from .models import Projector
|
||||||
|
|
||||||
|
|
||||||
class CustomSlideSlide(ProjectorElement):
|
|
||||||
"""
|
|
||||||
Slide definitions for custom slide model.
|
|
||||||
"""
|
|
||||||
name = 'core/customslide'
|
|
||||||
|
|
||||||
def check_data(self):
|
|
||||||
if not CustomSlide.objects.filter(pk=self.config_entry.get('id')).exists():
|
|
||||||
raise ProjectorException('Custom slide does not exist.')
|
|
||||||
|
|
||||||
def get_requirements(self, config_entry):
|
|
||||||
try:
|
|
||||||
custom_slide = CustomSlide.objects.get(pk=config_entry.get('id'))
|
|
||||||
except CustomSlide.DoesNotExist:
|
|
||||||
# Custom slide does not exist. Just do nothing.
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
yield custom_slide
|
|
||||||
yield custom_slide.agenda_item
|
|
||||||
|
|
||||||
|
|
||||||
class Clock(ProjectorElement):
|
class Clock(ProjectorElement):
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from openslides.utils.rest_api import Field, ModelSerializer, ValidationError
|
from openslides.utils.rest_api import Field, ModelSerializer, ValidationError
|
||||||
|
|
||||||
from .models import ChatMessage, CustomSlide, Projector, Tag
|
from .models import ChatMessage, Projector, Tag
|
||||||
|
|
||||||
|
|
||||||
class JSONSerializerField(Field):
|
class JSONSerializerField(Field):
|
||||||
@ -33,15 +33,6 @@ class ProjectorSerializer(ModelSerializer):
|
|||||||
fields = ('id', 'config', 'elements', 'scale', 'scroll', 'width', 'height',)
|
fields = ('id', 'config', 'elements', 'scale', 'scroll', 'width', 'height',)
|
||||||
|
|
||||||
|
|
||||||
class CustomSlideSerializer(ModelSerializer):
|
|
||||||
"""
|
|
||||||
Serializer for core.models.CustomSlide objects.
|
|
||||||
"""
|
|
||||||
class Meta:
|
|
||||||
model = CustomSlide
|
|
||||||
fields = ('id', 'title', 'text', 'weight', 'attachments', 'agenda_item_id')
|
|
||||||
|
|
||||||
|
|
||||||
class TagSerializer(ModelSerializer):
|
class TagSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for core.models.Tag objects.
|
Serializer for core.models.Tag objects.
|
||||||
|
@ -338,50 +338,6 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
.factory('Customslide', [
|
|
||||||
'DS',
|
|
||||||
'jsDataModel',
|
|
||||||
'gettext',
|
|
||||||
function(DS, jsDataModel, gettext) {
|
|
||||||
var name = 'core/customslide';
|
|
||||||
return DS.defineResource({
|
|
||||||
name: name,
|
|
||||||
useClass: jsDataModel,
|
|
||||||
verboseName: gettext('Agenda item'),
|
|
||||||
methods: {
|
|
||||||
getResourceName: function () {
|
|
||||||
return name;
|
|
||||||
},
|
|
||||||
getAgendaTitle: function () {
|
|
||||||
return this.title;
|
|
||||||
},
|
|
||||||
// link name which is shown in search result
|
|
||||||
getSearchResultName: function () {
|
|
||||||
return this.getAgendaTitle();
|
|
||||||
},
|
|
||||||
// subtitle of search result
|
|
||||||
getSearchResultSubtitle: function () {
|
|
||||||
return "Agenda item";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
relations: {
|
|
||||||
belongsTo: {
|
|
||||||
'agenda/item': {
|
|
||||||
localKey: 'agenda_item_id',
|
|
||||||
localField: 'agenda_item',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hasMany: {
|
|
||||||
'mediafiles/mediafile': {
|
|
||||||
localField: 'attachments',
|
|
||||||
localKeys: 'attachments_id',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
.factory('Tag', [
|
.factory('Tag', [
|
||||||
'DS',
|
'DS',
|
||||||
function(DS) {
|
function(DS) {
|
||||||
@ -544,10 +500,9 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
.run([
|
.run([
|
||||||
'ChatMessage',
|
'ChatMessage',
|
||||||
'Config',
|
'Config',
|
||||||
'Customslide',
|
|
||||||
'Projector',
|
'Projector',
|
||||||
'Tag',
|
'Tag',
|
||||||
function (ChatMessage, Config, Customslide, Projector, Tag) {}
|
function (ChatMessage, Config, Projector, Tag) {}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
}());
|
}());
|
||||||
|
@ -42,10 +42,6 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
|
|||||||
.config([
|
.config([
|
||||||
'slidesProvider',
|
'slidesProvider',
|
||||||
function(slidesProvider) {
|
function(slidesProvider) {
|
||||||
slidesProvider.registerSlide('core/customslide', {
|
|
||||||
template: 'static/templates/core/slide_customslide.html',
|
|
||||||
});
|
|
||||||
|
|
||||||
slidesProvider.registerSlide('core/clock', {
|
slidesProvider.registerSlide('core/clock', {
|
||||||
template: 'static/templates/core/slide_clock.html',
|
template: 'static/templates/core/slide_clock.html',
|
||||||
});
|
});
|
||||||
@ -151,18 +147,6 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
.controller('SlideCustomSlideCtrl', [
|
|
||||||
'$scope',
|
|
||||||
'Customslide',
|
|
||||||
function($scope, Customslide) {
|
|
||||||
// Attention! Each object that is used here has to be dealt on server side.
|
|
||||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
|
||||||
// class.
|
|
||||||
var id = $scope.element.id;
|
|
||||||
Customslide.bindOne(id, $scope, 'customslide');
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
.controller('SlideClockCtrl', [
|
.controller('SlideClockCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
function($scope) {
|
function($scope) {
|
||||||
|
@ -713,48 +713,6 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
templateUrl: 'static/templates/search.html',
|
templateUrl: 'static/templates/search.html',
|
||||||
})
|
})
|
||||||
|
|
||||||
// customslide
|
|
||||||
.state('core.customslide', {
|
|
||||||
url: '/customslide',
|
|
||||||
abstract: true,
|
|
||||||
template: "<ui-view/>",
|
|
||||||
})
|
|
||||||
.state('core.customslide.detail', {
|
|
||||||
resolve: {
|
|
||||||
customslide: function(Customslide, $stateParams) {
|
|
||||||
return Customslide.find($stateParams.id);
|
|
||||||
},
|
|
||||||
items: function(Agenda) {
|
|
||||||
return Agenda.findAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// redirects to customslide detail and opens customslide edit form dialog, uses edit url,
|
|
||||||
// used by ui-sref links from agenda only
|
|
||||||
// (from customslide controller use CustomSlideForm factory instead to open dialog in front
|
|
||||||
// of current view without redirect)
|
|
||||||
.state('core.customslide.detail.update', {
|
|
||||||
onEnter: ['$stateParams', '$state', 'ngDialog', 'Customslide', 'Agenda',
|
|
||||||
function($stateParams, $state, ngDialog, Customslide, Agenda) {
|
|
||||||
ngDialog.open({
|
|
||||||
template: 'static/templates/core/customslide-form.html',
|
|
||||||
controller: 'CustomslideUpdateCtrl',
|
|
||||||
className: 'ngdialog-theme-default wide-form',
|
|
||||||
resolve: {
|
|
||||||
customslide: function() {
|
|
||||||
return Customslide.find($stateParams.id);
|
|
||||||
},
|
|
||||||
items: function() {
|
|
||||||
return Agenda.findAll();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
preCloseCallback: function() {
|
|
||||||
$state.go('core.customslide.detail', {customslide: $stateParams.id});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
// tag
|
// tag
|
||||||
.state('core.tag', {
|
.state('core.tag', {
|
||||||
url: '/tag',
|
url: '/tag',
|
||||||
@ -1144,7 +1102,7 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
if ($scope.filterMotion && result.urlState == 'motions.motion.detail') {
|
if ($scope.filterMotion && result.urlState == 'motions.motion.detail') {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
if ($scope.filterAgenda && result.urlState == 'core.customslide.detail') {
|
if ($scope.filterAgenda && result.urlState == 'topics.topic.detail') {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
if ($scope.filterAssignment && result.urlState == 'assignments.assignment.detail') {
|
if ($scope.filterAssignment && result.urlState == 'assignments.assignment.detail') {
|
||||||
@ -1159,88 +1117,6 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
// Provide generic customslide form fields for create and update view
|
|
||||||
.factory('CustomslideForm', [
|
|
||||||
'gettextCatalog',
|
|
||||||
'Editor',
|
|
||||||
'Mediafile',
|
|
||||||
'Agenda',
|
|
||||||
'AgendaTree',
|
|
||||||
function (gettextCatalog, Editor, Mediafile, Agenda, AgendaTree) {
|
|
||||||
return {
|
|
||||||
// ngDialog for customslide form
|
|
||||||
getDialog: function (customslide) {
|
|
||||||
var resolve = {};
|
|
||||||
if (customslide) {
|
|
||||||
resolve = {
|
|
||||||
customslide: function(Customslide) {return Customslide.find(customslide.id);}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
resolve.mediafiles = function(Mediafile) {return Mediafile.findAll();};
|
|
||||||
return {
|
|
||||||
template: 'static/templates/core/customslide-form.html',
|
|
||||||
controller: (customslide) ? 'CustomslideUpdateCtrl' : 'CustomslideCreateCtrl',
|
|
||||||
className: 'ngdialog-theme-default wide-form',
|
|
||||||
closeByEscape: false,
|
|
||||||
closeByDocument: false,
|
|
||||||
resolve: (resolve) ? resolve : null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getFormFields: function () {
|
|
||||||
var images = Mediafile.getAllImages();
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: 'title',
|
|
||||||
type: 'input',
|
|
||||||
templateOptions: {
|
|
||||||
label: gettextCatalog.getString('Title'),
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'text',
|
|
||||||
type: 'editor',
|
|
||||||
templateOptions: {
|
|
||||||
label: gettextCatalog.getString('Text')
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
tinymceOption: Editor.getOptions(images)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'attachments_id',
|
|
||||||
type: 'select-multiple',
|
|
||||||
templateOptions: {
|
|
||||||
label: gettextCatalog.getString('Attachment'),
|
|
||||||
options: Mediafile.getAll(),
|
|
||||||
ngOptions: 'option.id as option.title_or_filename for option in to.options',
|
|
||||||
placeholder: gettextCatalog.getString('Select or search an attachment ...')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'showAsAgendaItem',
|
|
||||||
type: 'checkbox',
|
|
||||||
templateOptions: {
|
|
||||||
label: gettextCatalog.getString('Show as agenda item'),
|
|
||||||
description: gettextCatalog.getString('If deactivated it appears as internal item on agenda.')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'agenda_parent_item_id',
|
|
||||||
type: 'select-single',
|
|
||||||
templateOptions: {
|
|
||||||
label: gettextCatalog.getString('Parent item'),
|
|
||||||
options: AgendaTree.getFlatTree(Agenda.getAll()),
|
|
||||||
ngOptions: 'item.id as item.getListViewTitle() for item in to.options | notself : model.agenda_item_id',
|
|
||||||
placeholder: gettextCatalog.getString('Select a parent item ...')
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// Projector Control Controller
|
// Projector Control Controller
|
||||||
.controller('ProjectorControlCtrl', [
|
.controller('ProjectorControlCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
@ -1454,101 +1330,6 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
// Customslide Controllers
|
|
||||||
.controller('CustomslideDetailCtrl', [
|
|
||||||
'$scope',
|
|
||||||
'ngDialog',
|
|
||||||
'CustomslideForm',
|
|
||||||
'Customslide',
|
|
||||||
'customslide',
|
|
||||||
function($scope, ngDialog, CustomslideForm, Customslide, customslide) {
|
|
||||||
Customslide.bindOne(customslide.id, $scope, 'customslide');
|
|
||||||
Customslide.loadRelations(customslide, 'agenda_item');
|
|
||||||
// open edit dialog
|
|
||||||
$scope.openDialog = function (customslide) {
|
|
||||||
ngDialog.open(CustomslideForm.getDialog(customslide));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
.controller('CustomslideCreateCtrl', [
|
|
||||||
'$scope',
|
|
||||||
'$state',
|
|
||||||
'Customslide',
|
|
||||||
'CustomslideForm',
|
|
||||||
'Agenda',
|
|
||||||
'AgendaUpdate',
|
|
||||||
function($scope, $state, Customslide, CustomslideForm, Agenda, AgendaUpdate) {
|
|
||||||
$scope.customslide = {};
|
|
||||||
$scope.model = {};
|
|
||||||
$scope.model.showAsAgendaItem = true;
|
|
||||||
// get all form fields
|
|
||||||
$scope.formFields = CustomslideForm.getFormFields();
|
|
||||||
// save form
|
|
||||||
$scope.save = function (customslide) {
|
|
||||||
Customslide.create(customslide).then(
|
|
||||||
function(success) {
|
|
||||||
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
|
|
||||||
// see openslides.agenda.models.Item.ITEM_TYPE.
|
|
||||||
var changes = [{key: 'type', value: (customslide.showAsAgendaItem ? 1 : 2)},
|
|
||||||
{key: 'parent_id', value: customslide.agenda_parent_item_id}];
|
|
||||||
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
|
|
||||||
});
|
|
||||||
$scope.closeThisDialog();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
.controller('CustomslideUpdateCtrl', [
|
|
||||||
'$scope',
|
|
||||||
'$state',
|
|
||||||
'Customslide',
|
|
||||||
'CustomslideForm',
|
|
||||||
'Agenda',
|
|
||||||
'AgendaUpdate',
|
|
||||||
'customslide',
|
|
||||||
function($scope, $state, Customslide, CustomslideForm, Agenda, AgendaUpdate, customslide) {
|
|
||||||
Customslide.loadRelations(customslide, 'agenda_item');
|
|
||||||
$scope.alert = {};
|
|
||||||
// set initial values for form model by create deep copy of customslide object
|
|
||||||
// so list/detail view is not updated while editing
|
|
||||||
$scope.model = angular.copy(customslide);
|
|
||||||
// get all form fields
|
|
||||||
$scope.formFields = CustomslideForm.getFormFields();
|
|
||||||
for (var i = 0; i < $scope.formFields.length; i++) {
|
|
||||||
if ($scope.formFields[i].key == "showAsAgendaItem") {
|
|
||||||
// get state from agenda item (hidden/internal or agenda item)
|
|
||||||
$scope.formFields[i].defaultValue = !customslide.agenda_item.is_hidden;
|
|
||||||
} else if($scope.formFields[i].key == "agenda_parent_item_id") {
|
|
||||||
$scope.formFields[i].defaultValue = customslide.agenda_item.parent_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// save form
|
|
||||||
$scope.save = function (customslide) {
|
|
||||||
Customslide.create(customslide).then(
|
|
||||||
function(success) {
|
|
||||||
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
|
|
||||||
// see openslides.agenda.models.Item.ITEM_TYPE.
|
|
||||||
var changes = [{key: 'type', value: (customslide.showAsAgendaItem ? 1 : 2)},
|
|
||||||
{key: 'parent_id', value: customslide.agenda_parent_item_id}];
|
|
||||||
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
|
|
||||||
$scope.closeThisDialog();
|
|
||||||
}, function (error) {
|
|
||||||
// save error: revert all changes by restore
|
|
||||||
// (refresh) original customslide object from server
|
|
||||||
Customslide.refresh(customslide);
|
|
||||||
var message = '';
|
|
||||||
for (var e in error.data) {
|
|
||||||
message += e + ': ' + error.data[e] + ' ';
|
|
||||||
}
|
|
||||||
$scope.alert = {type: 'danger', msg: message, show: true};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// Tag Controller
|
// Tag Controller
|
||||||
.controller('TagListCtrl', [
|
.controller('TagListCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
<div ng-controller="SlideCustomSlideCtrl" class="content scrollcontent">
|
|
||||||
<h1>{{ customslide.agenda_item.getTitle() }}</h1>
|
|
||||||
<div ng-bind-html="customslide.text | trusted"></div>
|
|
||||||
</div>
|
|
@ -37,13 +37,12 @@ from openslides.utils.search import search
|
|||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
ChatMessageAccessPermissions,
|
ChatMessageAccessPermissions,
|
||||||
ConfigAccessPermissions,
|
ConfigAccessPermissions,
|
||||||
CustomSlideAccessPermissions,
|
|
||||||
ProjectorAccessPermissions,
|
ProjectorAccessPermissions,
|
||||||
TagAccessPermissions,
|
TagAccessPermissions,
|
||||||
)
|
)
|
||||||
from .config import config
|
from .config import config
|
||||||
from .exceptions import ConfigError, ConfigNotFound
|
from .exceptions import ConfigError, ConfigNotFound
|
||||||
from .models import ChatMessage, CustomSlide, Projector, Tag
|
from .models import ChatMessage, Projector, Tag
|
||||||
|
|
||||||
|
|
||||||
# Special Django views
|
# Special Django views
|
||||||
@ -449,27 +448,6 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
|
|||||||
return Response({'detail': message})
|
return Response({'detail': message})
|
||||||
|
|
||||||
|
|
||||||
class CustomSlideViewSet(ModelViewSet):
|
|
||||||
"""
|
|
||||||
API endpoint for custom slides.
|
|
||||||
|
|
||||||
There are the following views: metadata, list, retrieve, create,
|
|
||||||
partial_update, update and destroy.
|
|
||||||
"""
|
|
||||||
access_permissions = CustomSlideAccessPermissions()
|
|
||||||
queryset = CustomSlide.objects.all()
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
|
||||||
"""
|
|
||||||
Returns True if the user has required permissions.
|
|
||||||
"""
|
|
||||||
if self.action in ('list', 'retrieve'):
|
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
else:
|
|
||||||
result = self.request.user.has_perm('core.can_manage_projector')
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class TagViewSet(ModelViewSet):
|
class TagViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint for tags.
|
API endpoint for tags.
|
||||||
|
@ -17,6 +17,7 @@ INSTALLED_APPS = [
|
|||||||
'rest_framework',
|
'rest_framework',
|
||||||
'channels',
|
'channels',
|
||||||
'openslides.agenda',
|
'openslides.agenda',
|
||||||
|
'openslides.topics',
|
||||||
'openslides.motions',
|
'openslides.motions',
|
||||||
'openslides.assignments',
|
'openslides.assignments',
|
||||||
'openslides.mediafiles',
|
'openslides.mediafiles',
|
||||||
|
1
openslides/topics/__init__.py
Normal file
1
openslides/topics/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'openslides.topics.apps.TopicsAppConfig'
|
20
openslides/topics/access_permissions.py
Normal file
20
openslides/topics/access_permissions.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from ..utils.access_permissions import BaseAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
|
class TopicAccessPermissions(BaseAccessPermissions):
|
||||||
|
"""
|
||||||
|
Access permissions container for Topic and TopicViewSet.
|
||||||
|
"""
|
||||||
|
def check_permissions(self, user):
|
||||||
|
"""
|
||||||
|
Returns True if the user has read access model instances.
|
||||||
|
"""
|
||||||
|
return user.has_perm('agenda.can_see')
|
||||||
|
|
||||||
|
def get_serializer_class(self, user=None):
|
||||||
|
"""
|
||||||
|
Returns serializer class.
|
||||||
|
"""
|
||||||
|
from .serializers import TopicSerializer
|
||||||
|
|
||||||
|
return TopicSerializer
|
20
openslides/topics/apps.py
Normal file
20
openslides/topics/apps.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TopicsAppConfig(AppConfig):
|
||||||
|
name = 'openslides.topics'
|
||||||
|
verbose_name = 'OpenSlides Topics'
|
||||||
|
angular_site_module = True
|
||||||
|
angular_projector_module = True
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
# Load projector elements.
|
||||||
|
# Do this by just importing all from these files.
|
||||||
|
from . import projector # noqa
|
||||||
|
|
||||||
|
# Import all required stuff.
|
||||||
|
from ..utils.rest_api import router
|
||||||
|
from .views import TopicViewSet
|
||||||
|
|
||||||
|
# Register viewsets.
|
||||||
|
router.register(self.get_model('Topic').get_collection_string(), TopicViewSet)
|
31
openslides/topics/migrations/0001_initial.py
Normal file
31
openslides/topics/migrations/0001_initial.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.1 on 2016-09-18 20:20
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mediafiles', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Topic',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=256)),
|
||||||
|
('text', models.TextField(blank=True)),
|
||||||
|
('attachments', models.ManyToManyField(blank=True, to='mediafiles.Mediafile')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'default_permissions': (),
|
||||||
|
},
|
||||||
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
]
|
0
openslides/topics/migrations/__init__.py
Normal file
0
openslides/topics/migrations/__init__.py
Normal file
53
openslides/topics/models.py
Normal file
53
openslides/topics/models.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from ..agenda.models import Item
|
||||||
|
from ..mediafiles.models import Mediafile
|
||||||
|
from ..utils.models import RESTModelMixin
|
||||||
|
from .access_permissions import TopicAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
|
class Topic(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for slides with custom content. Used to be called custom slide.
|
||||||
|
"""
|
||||||
|
access_permissions = TopicAccessPermissions()
|
||||||
|
|
||||||
|
title = models.CharField(max_length=256)
|
||||||
|
text = models.TextField(blank=True)
|
||||||
|
attachments = models.ManyToManyField(Mediafile, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
@property
|
||||||
|
def agenda_item(self):
|
||||||
|
"""
|
||||||
|
Returns the related agenda item.
|
||||||
|
"""
|
||||||
|
content_type = ContentType.objects.get_for_model(self)
|
||||||
|
return Item.objects.get(object_id=self.pk, content_type=content_type)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def agenda_item_id(self):
|
||||||
|
"""
|
||||||
|
Returns the id of the agenda item object related to this object.
|
||||||
|
"""
|
||||||
|
return self.agenda_item.pk
|
||||||
|
|
||||||
|
def get_agenda_title(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_agenda_list_view_title(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_search_index_string(self):
|
||||||
|
"""
|
||||||
|
Returns a string that can be indexed for the search.
|
||||||
|
"""
|
||||||
|
return " ".join((
|
||||||
|
self.title,
|
||||||
|
self.text))
|
24
openslides/topics/projector.py
Normal file
24
openslides/topics/projector.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from ..core.exceptions import ProjectorException
|
||||||
|
from ..utils.projector import ProjectorElement
|
||||||
|
from .models import Topic
|
||||||
|
|
||||||
|
|
||||||
|
class TopicSlide(ProjectorElement):
|
||||||
|
"""
|
||||||
|
Slide definitions for topic model.
|
||||||
|
"""
|
||||||
|
name = 'topics/topic'
|
||||||
|
|
||||||
|
def check_data(self):
|
||||||
|
if not Topic.objects.filter(pk=self.config_entry.get('id')).exists():
|
||||||
|
raise ProjectorException('Topic does not exist.')
|
||||||
|
|
||||||
|
def get_requirements(self, config_entry):
|
||||||
|
try:
|
||||||
|
topic = Topic.objects.get(pk=config_entry.get('id'))
|
||||||
|
except Topic.DoesNotExist:
|
||||||
|
# Topic does not exist. Just do nothing.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
yield topic
|
||||||
|
yield topic.agenda_item
|
12
openslides/topics/serializers.py
Normal file
12
openslides/topics/serializers.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from openslides.utils.rest_api import ModelSerializer
|
||||||
|
|
||||||
|
from .models import Topic
|
||||||
|
|
||||||
|
|
||||||
|
class TopicSerializer(ModelSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for core.models.Topic objects.
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
model = Topic
|
||||||
|
fields = ('id', 'title', 'text', 'attachments', 'agenda_item_id')
|
53
openslides/topics/static/js/topics/base.js
Normal file
53
openslides/topics/static/js/topics/base.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
(function () {
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('OpenSlidesApp.topics', [])
|
||||||
|
|
||||||
|
.factory('Topic', [
|
||||||
|
'DS',
|
||||||
|
'jsDataModel',
|
||||||
|
'gettext',
|
||||||
|
function(DS, jsDataModel, gettext) {
|
||||||
|
var name = 'topics/topic';
|
||||||
|
return DS.defineResource({
|
||||||
|
name: name,
|
||||||
|
useClass: jsDataModel,
|
||||||
|
verboseName: gettext('Topic'),
|
||||||
|
methods: {
|
||||||
|
getResourceName: function () {
|
||||||
|
return name;
|
||||||
|
},
|
||||||
|
getAgendaTitle: function () {
|
||||||
|
return this.title;
|
||||||
|
},
|
||||||
|
// link name which is shown in search result
|
||||||
|
getSearchResultName: function () {
|
||||||
|
return this.getAgendaTitle();
|
||||||
|
},
|
||||||
|
// subtitle of search result
|
||||||
|
getSearchResultSubtitle: function () {
|
||||||
|
return 'Topic';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
belongsTo: {
|
||||||
|
'agenda/item': {
|
||||||
|
localKey: 'agenda_item_id',
|
||||||
|
localField: 'agenda_item',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasMany: {
|
||||||
|
'mediafiles/mediafile': {
|
||||||
|
localField: 'attachments',
|
||||||
|
localKeys: 'attachments_id',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
.run(['Topic', function(Topic) {}]);
|
||||||
|
|
||||||
|
}());
|
28
openslides/topics/static/js/topics/projector.js
Normal file
28
openslides/topics/static/js/topics/projector.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
(function () {
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('OpenSlidesApp.topics.projector', ['OpenSlidesApp.topics'])
|
||||||
|
|
||||||
|
.config([
|
||||||
|
'slidesProvider',
|
||||||
|
function (slidesProvider) {
|
||||||
|
slidesProvider.registerSlide('topics/topic', {
|
||||||
|
template: 'static/templates/topics/slide_topic.html'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
.controller('SlideTopicCtrl', [
|
||||||
|
'$scope',
|
||||||
|
'Topic',
|
||||||
|
function($scope, Topic) {
|
||||||
|
// Attention! Each object that is used here has to be dealt on server side.
|
||||||
|
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||||
|
// class.
|
||||||
|
var id = $scope.element.id;
|
||||||
|
Topic.bindOne(id, $scope, 'topic');
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
})();
|
391
openslides/topics/static/js/topics/site.js
Normal file
391
openslides/topics/static/js/topics/site.js
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
(function () {
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics'])
|
||||||
|
|
||||||
|
.config([
|
||||||
|
'$stateProvider',
|
||||||
|
function($stateProvider) {
|
||||||
|
$stateProvider
|
||||||
|
.state('topics', {
|
||||||
|
url: '/topics',
|
||||||
|
abstract: true,
|
||||||
|
template: "<ui-view/>",
|
||||||
|
})
|
||||||
|
|
||||||
|
.state('topics.topic', {
|
||||||
|
url: '/topic',
|
||||||
|
abstract: true,
|
||||||
|
template: "<ui-view/>",
|
||||||
|
})
|
||||||
|
.state('topics.topic.detail', {
|
||||||
|
resolve: {
|
||||||
|
topic: function(Topic, $stateParams) {
|
||||||
|
return Topic.find($stateParams.id);
|
||||||
|
},
|
||||||
|
items: function(Agenda) {
|
||||||
|
return Agenda.findAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// redirects to topic detail and opens topic edit form dialog, uses edit url,
|
||||||
|
// used by ui-sref links from agenda only
|
||||||
|
// (from topic controller use TopicForm factory instead to open dialog in front
|
||||||
|
// of current view without redirect)
|
||||||
|
.state('topics.topic.detail.update', {
|
||||||
|
onEnter: ['$stateParams', '$state', 'ngDialog', 'Topic',
|
||||||
|
function($stateParams, $state, ngDialog, Topic) {
|
||||||
|
ngDialog.open({
|
||||||
|
template: 'static/templates/topics/topic-form.html',
|
||||||
|
controller: 'TopicUpdateCtrl',
|
||||||
|
className: 'ngdialog-theme-default wide-form',
|
||||||
|
closeByEscape: false,
|
||||||
|
closeByDocument: false,
|
||||||
|
resolve: {
|
||||||
|
topic: function() {
|
||||||
|
return Topic.find($stateParams.id);
|
||||||
|
},
|
||||||
|
items: function(Agenda) {
|
||||||
|
return Agenda.findAll().catch(
|
||||||
|
function() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preCloseCallback: function() {
|
||||||
|
$state.go('topics.topic.detail', {topic: $stateParams.id});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
.state('topics.topic.import', {
|
||||||
|
url: '/import',
|
||||||
|
controller: 'TopicImportCtrl',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
.factory('TopicForm', [
|
||||||
|
'gettextCatalog',
|
||||||
|
'Editor',
|
||||||
|
'Mediafile',
|
||||||
|
'Agenda',
|
||||||
|
'AgendaTree',
|
||||||
|
function (gettextCatalog, Editor, Mediafile, Agenda, AgendaTree) {
|
||||||
|
return {
|
||||||
|
// ngDialog for topic form
|
||||||
|
getDialog: function (topic) {
|
||||||
|
var resolve = {};
|
||||||
|
if (topic) {
|
||||||
|
resolve = {
|
||||||
|
topic: function (Topic) {return Topic.find(topic.id);}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
resolve.mediafiles = function (Mediafile) {
|
||||||
|
return Mediafile.findAll();
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
template: 'static/templates/topics/topic-form.html',
|
||||||
|
controller: (topic) ? 'TopicUpdateCtrl' : 'TopicCreateCtrl',
|
||||||
|
className: 'ngdialog-theme-default wide-form',
|
||||||
|
closeByEscape: false,
|
||||||
|
closeByDocument: false,
|
||||||
|
resolve: (resolve) ? resolve : null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getFormFields: function () {
|
||||||
|
var images = Mediafile.getAllImages();
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'title',
|
||||||
|
type: 'input',
|
||||||
|
templateOptions: {
|
||||||
|
label: gettextCatalog.getString('Title'),
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'text',
|
||||||
|
type: 'editor',
|
||||||
|
templateOptions: {
|
||||||
|
label: gettextCatalog.getString('Text')
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
tinymceOption: Editor.getOptions(images)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attachments_id',
|
||||||
|
type: 'select-multiple',
|
||||||
|
templateOptions: {
|
||||||
|
label: gettextCatalog.getString('Attachment'),
|
||||||
|
options: Mediafile.getAll(),
|
||||||
|
ngOptions: 'option.id as option.title_or_filename for option in to.options',
|
||||||
|
placeholder: gettextCatalog.getString('Select or search an attachment ...')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'showAsAgendaItem',
|
||||||
|
type: 'checkbox',
|
||||||
|
templateOptions: {
|
||||||
|
label: gettextCatalog.getString('Show as agenda item'),
|
||||||
|
description: gettextCatalog.getString('If deactivated it appears as internal item on agenda.')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'agenda_parent_item_id',
|
||||||
|
type: 'select-single',
|
||||||
|
templateOptions: {
|
||||||
|
label: gettextCatalog.getString('Parent item'),
|
||||||
|
options: AgendaTree.getFlatTree(Agenda.getAll()),
|
||||||
|
ngOptions: 'item.id as item.getListViewTitle() for item in to.options | notself : model.agenda_item_id',
|
||||||
|
placeholder: gettextCatalog.getString('Select a parent item ...')
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
.controller('TopicDetailCtrl', [
|
||||||
|
'$scope',
|
||||||
|
'ngDialog',
|
||||||
|
'TopicForm',
|
||||||
|
'Topic',
|
||||||
|
'topic',
|
||||||
|
function($scope, ngDialog, TopicForm, Topic, topic) {
|
||||||
|
Topic.bindOne(topic.id, $scope, 'topic');
|
||||||
|
Topic.loadRelations(topic, 'agenda_item');
|
||||||
|
$scope.openDialog = function (topic) {
|
||||||
|
ngDialog.open(TopicForm.getDialog(topic));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
.controller('TopicCreateCtrl', [
|
||||||
|
'$scope',
|
||||||
|
'$state',
|
||||||
|
'Topic',
|
||||||
|
'TopicForm',
|
||||||
|
'Agenda',
|
||||||
|
'AgendaUpdate',
|
||||||
|
function($scope, $state, Topic, TopicForm, Agenda, AgendaUpdate) {
|
||||||
|
$scope.topic = {};
|
||||||
|
$scope.model = {};
|
||||||
|
$scope.model.showAsAgendaItem = true;
|
||||||
|
// get all form fields
|
||||||
|
$scope.formFields = TopicForm.getFormFields();
|
||||||
|
// save form
|
||||||
|
$scope.save = function (topic) {
|
||||||
|
Topic.create(topic).then(
|
||||||
|
function (success) {
|
||||||
|
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
|
||||||
|
// see openslides.agenda.models.Item.ITEM_TYPE.
|
||||||
|
var changes = [{key: 'type', value: (topic.showAsAgendaItem ? 1 : 2)},
|
||||||
|
{key: 'parent_id', value: topic.agenda_parent_item_id}];
|
||||||
|
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
|
||||||
|
});
|
||||||
|
$scope.closeThisDialog();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
.controller('TopicUpdateCtrl', [
|
||||||
|
'$scope',
|
||||||
|
'$state',
|
||||||
|
'Topic',
|
||||||
|
'TopicForm',
|
||||||
|
'Agenda',
|
||||||
|
'AgendaUpdate',
|
||||||
|
'topic',
|
||||||
|
function($scope, $state, Topic, TopicForm, Agenda, AgendaUpdate, topic) {
|
||||||
|
Topic.loadRelations(topic, 'agenda_item');
|
||||||
|
$scope.alert = {};
|
||||||
|
// set initial values for form model by create deep copy of topic object
|
||||||
|
// so list/detail view is not updated while editing
|
||||||
|
$scope.model = angular.copy(topic);
|
||||||
|
// get all form fields
|
||||||
|
$scope.formFields = TopicForm.getFormFields();
|
||||||
|
for (var i = 0; i < $scope.formFields.length; i++) {
|
||||||
|
if ($scope.formFields[i].key == "showAsAgendaItem") {
|
||||||
|
// get state from agenda item (hidden/internal or agenda item)
|
||||||
|
$scope.formFields[i].defaultValue = !topic.agenda_item.is_hidden;
|
||||||
|
} else if ($scope.formFields[i].key == "agenda_parent_item_id") {
|
||||||
|
$scope.formFields[i].defaultValue = topic.agenda_item.parent_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// save form
|
||||||
|
$scope.save = function (topic) {
|
||||||
|
Topic.create(topic).then(
|
||||||
|
function(success) {
|
||||||
|
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
|
||||||
|
// see openslides.agenda.models.Item.ITEM_TYPE.
|
||||||
|
var changes = [{key: 'type', value: (topic.showAsAgendaItem ? 1 : 2)},
|
||||||
|
{key: 'parent_id', value: topic.agenda_parent_item_id}];
|
||||||
|
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
|
||||||
|
$scope.closeThisDialog();
|
||||||
|
}, function (error) {
|
||||||
|
// save error: revert all changes by restore
|
||||||
|
// (refresh) original topic object from server
|
||||||
|
Topic.refresh(topic);
|
||||||
|
var message = '';
|
||||||
|
for (var e in error.data) {
|
||||||
|
message += e + ': ' + error.data[e] + ' ';
|
||||||
|
}
|
||||||
|
$scope.alert = {type: 'danger', msg: message, show: true};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
.controller('TopicImportCtrl', [
|
||||||
|
'$scope',
|
||||||
|
'gettext',
|
||||||
|
'Agenda',
|
||||||
|
'Topic',
|
||||||
|
function($scope, gettext, Agenda, Topic) {
|
||||||
|
// Big TODO: Change wording from "item" to "topic".
|
||||||
|
// import from textarea
|
||||||
|
$scope.importByLine = function () {
|
||||||
|
if ($scope.itemlist) {
|
||||||
|
$scope.titleItems = $scope.itemlist[0].split("\n");
|
||||||
|
$scope.importcounter = 0;
|
||||||
|
$scope.titleItems.forEach(function(title, index) {
|
||||||
|
var item = {title: title};
|
||||||
|
// TODO: create all items in bulk mode
|
||||||
|
Topic.create(item).then(
|
||||||
|
function(success) {
|
||||||
|
// find related agenda item
|
||||||
|
Agenda.find(success.agenda_item_id).then(function(item) {
|
||||||
|
// import all items as type AGENDA_ITEM = 1
|
||||||
|
item.type = 1;
|
||||||
|
item.weight = 1000 + index;
|
||||||
|
Agenda.save(item);
|
||||||
|
});
|
||||||
|
$scope.importcounter++;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// *** CSV import ***
|
||||||
|
// set initial data for csv import
|
||||||
|
$scope.items = [];
|
||||||
|
$scope.separator = ',';
|
||||||
|
$scope.encoding = 'UTF-8';
|
||||||
|
$scope.encodingOptions = ['UTF-8', 'ISO-8859-1'];
|
||||||
|
$scope.accept = '.csv, .txt';
|
||||||
|
$scope.csv = {
|
||||||
|
content: null,
|
||||||
|
header: true,
|
||||||
|
headerVisible: false,
|
||||||
|
separator: $scope.separator,
|
||||||
|
separatorVisible: false,
|
||||||
|
encoding: $scope.encoding,
|
||||||
|
encodingVisible: false,
|
||||||
|
accept: $scope.accept,
|
||||||
|
result: null
|
||||||
|
};
|
||||||
|
// set csv file encoding
|
||||||
|
$scope.setEncoding = function () {
|
||||||
|
$scope.csv.encoding = $scope.encoding;
|
||||||
|
};
|
||||||
|
// set csv file encoding
|
||||||
|
$scope.setSeparator = function () {
|
||||||
|
$scope.csv.separator = $scope.separator;
|
||||||
|
};
|
||||||
|
// detect if csv file is loaded
|
||||||
|
$scope.$watch('csv.result', function () {
|
||||||
|
$scope.items = [];
|
||||||
|
var quotionRe = /^"(.*)"$/;
|
||||||
|
angular.forEach($scope.csv.result, function (item, index) {
|
||||||
|
// title
|
||||||
|
if (item.title) {
|
||||||
|
item.title = item.title.replace(quotionRe, '$1');
|
||||||
|
}
|
||||||
|
if (!item.title) {
|
||||||
|
item.importerror = true;
|
||||||
|
item.title_error = gettext('Error: Title is required.');
|
||||||
|
}
|
||||||
|
// text
|
||||||
|
if (item.text) {
|
||||||
|
item.text = item.text.replace(quotionRe, '$1');
|
||||||
|
}
|
||||||
|
// duration
|
||||||
|
if (item.duration) {
|
||||||
|
item.duration = item.duration.replace(quotionRe, '$1');
|
||||||
|
}
|
||||||
|
// comment
|
||||||
|
if (item.comment) {
|
||||||
|
item.comment = item.comment.replace(quotionRe, '$1');
|
||||||
|
}
|
||||||
|
// is_hidden
|
||||||
|
if (item.is_hidden) {
|
||||||
|
item.is_hidden = item.is_hidden.replace(quotionRe, '$1');
|
||||||
|
if (item.is_hidden == '1') {
|
||||||
|
item.type = 2;
|
||||||
|
} else {
|
||||||
|
item.type = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.type = 1;
|
||||||
|
}
|
||||||
|
// set weight for right csv row order
|
||||||
|
// (Use 1000+ to protect existing items and prevent collision
|
||||||
|
// with new items which use weight 10000 as default.)
|
||||||
|
item.weight = 1000 + index;
|
||||||
|
$scope.items.push(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// import from csv file
|
||||||
|
$scope.import = function () {
|
||||||
|
$scope.csvImporting = true;
|
||||||
|
angular.forEach($scope.items, function (item) {
|
||||||
|
if (!item.importerror) {
|
||||||
|
Topic.create(item).then(
|
||||||
|
function(success) {
|
||||||
|
item.imported = true;
|
||||||
|
// find related agenda item
|
||||||
|
Agenda.find(success.agenda_item_id).then(function(agendaItem) {
|
||||||
|
agendaItem.duration = item.duration;
|
||||||
|
agendaItem.comment = item.comment;
|
||||||
|
agendaItem.type = item.type;
|
||||||
|
agendaItem.weight = item.weight;
|
||||||
|
Agenda.save(agendaItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$scope.csvimported = true;
|
||||||
|
};
|
||||||
|
$scope.clear = function () {
|
||||||
|
$scope.csv.result = null;
|
||||||
|
};
|
||||||
|
// download CSV example file
|
||||||
|
$scope.downloadCSVExample = function () {
|
||||||
|
var element = document.getElementById('downloadLink');
|
||||||
|
var csvRows = [
|
||||||
|
// column header line
|
||||||
|
['title', 'text', 'duration', 'comment', 'is_hidden'],
|
||||||
|
// example entries
|
||||||
|
['Demo 1', 'Demo text 1', '1:00', 'test comment', ''],
|
||||||
|
['Break', '', '0:10', '', '1'],
|
||||||
|
['Demo 2', 'Demo text 2', '1:30', '', '']
|
||||||
|
|
||||||
|
];
|
||||||
|
var csvString = csvRows.join("%0A");
|
||||||
|
element.href = 'data:text/csv;charset=utf-8,' + csvString;
|
||||||
|
element.download = 'agenda-example.csv';
|
||||||
|
element.target = '_blank';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
}());
|
@ -0,0 +1,4 @@
|
|||||||
|
<div ng-controller="SlideTopicCtrl" class="content scrollcontent">
|
||||||
|
<h1>{{ topic.agenda_item.getTitle() }}</h1>
|
||||||
|
<div ng-bind-html="topic.text | trusted"></div>
|
||||||
|
</div>
|
@ -6,34 +6,34 @@
|
|||||||
<translate>Back to overview</translate>
|
<translate>Back to overview</translate>
|
||||||
</a>
|
</a>
|
||||||
<!-- List of speakers -->
|
<!-- List of speakers -->
|
||||||
<a ui-sref="agenda.item.detail({id: customslide.agenda_item_id})" class="btn btn-sm btn-default">
|
<a ui-sref="agenda.item.detail({id: topic.agenda_item_id})" class="btn btn-sm btn-default">
|
||||||
<i class="fa fa-microphone fa-lg"></i>
|
<i class="fa fa-microphone fa-lg"></i>
|
||||||
<translate>List of speakers</translate>
|
<translate>List of speakers</translate>
|
||||||
</a>
|
</a>
|
||||||
<!-- project -->
|
<!-- project -->
|
||||||
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
|
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
|
||||||
ng-class="{ 'btn-primary': customslide.isProjected() }"
|
ng-class="{ 'btn-primary': topic.isProjected() }"
|
||||||
ng-click="customslide.project()"
|
ng-click="topic.project()"
|
||||||
title="{{ 'Project agenda item' | translate }}">
|
title="{{ 'Project topic' | translate }}">
|
||||||
<i class="fa fa-video-camera"></i>
|
<i class="fa fa-video-camera"></i>
|
||||||
</a>
|
</a>
|
||||||
<!-- edit -->
|
<!-- edit -->
|
||||||
<a os-perms="agenda.can_manage" ng-click="openDialog(customslide)"
|
<a os-perms="agenda.can_manage" ng-click="openDialog(topic)"
|
||||||
class="btn btn-default btn-sm"
|
class="btn btn-default btn-sm"
|
||||||
title="{{ 'Edit' | translate}}">
|
title="{{ 'Edit' | translate}}">
|
||||||
<i class="fa fa-pencil"></i>
|
<i class="fa fa-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ customslide.agenda_item.getTitle() }}</h1>
|
<h1>{{ topic.agenda_item.getTitle() }}</h1>
|
||||||
<h2 translate>Agenda item</h2>
|
<h2 translate>Topic</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<div ng-bind-html="customslide.text | trusted"></div>
|
<div ng-bind-html="topic.text | trusted"></div>
|
||||||
<h3 ng-if="customslide.attachments.length > 0" translate>Attachments</h3>
|
<h3 ng-if="topic.attachments.length > 0" translate>Attachments</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li ng-repeat="attachment in customslide.attachments">
|
<li ng-repeat="attachment in topic.attachments">
|
||||||
<a href="{{ attachment.mediafileUrl }}" target="_blank">
|
<a href="{{ attachment.mediafileUrl }}" target="_blank">
|
||||||
{{ attachment.title_or_filename }}
|
{{ attachment.title_or_filename }}
|
||||||
</a>
|
</a>
|
@ -1,13 +1,13 @@
|
|||||||
<h1 ng-if="model.id" translate>Edit agenda item</h1>
|
<h1 ng-if="model.id" translate>Edit topic</h1>
|
||||||
<h1 ng-if="!model.id" translate>New agenda item</h1>
|
<h1 ng-if="!model.id" translate>New topic</h1>
|
||||||
|
|
||||||
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}">
|
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}">
|
||||||
{{ alert.msg }}
|
{{ alert.msg }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form name="customslideForm" ng-submit="save(model)">
|
<form name="topicForm" ng-submit="save(model)">
|
||||||
<formly-form model="model" fields="formFields">
|
<formly-form model="model" fields="formFields">
|
||||||
<button type="submit" ng-disabled="customslideForm.$invalid" class="btn btn-primary" translate>
|
<button type="submit" ng-disabled="topicForm.$invalid" class="btn btn-primary" translate>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
<button type="button" ng-click="closeThisDialog()" class="btn btn-default" translate>
|
<button type="button" ng-click="closeThisDialog()" class="btn btn-default" translate>
|
@ -6,14 +6,13 @@
|
|||||||
<translate>Back to overview</translate>
|
<translate>Back to overview</translate>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<h1 translate>Import agenda items</h1>
|
<h1 translate>Import topics</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<h2 translate>Import by copy/paste</h2>
|
<h2 translate>Import by copy/paste</h2>
|
||||||
<p translate>Copy and paste your agenda item titles in this textbox.
|
<p translate>Copy and paste your topic titles in this textbox. Keep each item in a single line.</p>
|
||||||
Keep each item in a single line.</p>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-sm-6">
|
<div class="form-group col-sm-6">
|
||||||
@ -114,20 +113,20 @@ Keep each item in a single line.</p>
|
|||||||
<div ng-repeat="item in itemsFailed = (items | filter:{importerror:true})"></div>
|
<div ng-repeat="item in itemsFailed = (items | filter:{importerror:true})"></div>
|
||||||
<i class="fa fa-exclamation-triangle"></i>
|
<i class="fa fa-exclamation-triangle"></i>
|
||||||
{{ itemsFailed.length }}
|
{{ itemsFailed.length }}
|
||||||
<translate>agenda items will be not imported.</translate>
|
<translate>topics will be not imported.</translate>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div ng-repeat="item in itemsPassed = (items | filter:{importerror:false})"></div>
|
<div ng-repeat="item in itemsPassed = (items | filter:{importerror:false})"></div>
|
||||||
<i class="fa fa-check-circle-o fa-lg"></i>
|
<i class="fa fa-check-circle-o fa-lg"></i>
|
||||||
{{ items.length - itemsFailed.length }}
|
{{ items.length - itemsFailed.length }}
|
||||||
<translate>items will be imported.</translate>
|
<translate>topics will be imported.</translate>
|
||||||
</div>
|
</div>
|
||||||
<div ng-repeat="item in itemsImported = (items | filter:{imported:true})"></div>
|
<div ng-repeat="item in itemsImported = (items | filter:{imported:true})"></div>
|
||||||
<div ng-if="itemsImported.length > 0" class="text-success">
|
<div ng-if="itemsImported.length > 0" class="text-success">
|
||||||
<hr class="smallhr">
|
<hr class="smallhr">
|
||||||
<i class="fa fa-check-circle fa-lg"></i>
|
<i class="fa fa-check-circle fa-lg"></i>
|
||||||
{{ itemsImported.length }}
|
{{ itemsImported.length }}
|
||||||
<translate>items were successfully imported.</translate>
|
<translate>topics were successfully imported.</translate>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="spacer">
|
<div class="spacer">
|
||||||
@ -135,7 +134,7 @@ Keep each item in a single line.</p>
|
|||||||
Clear preview
|
Clear preview
|
||||||
</button>
|
</button>
|
||||||
<button ng-if="!csvImporting" ng-click="import()" class="btn btn-primary" translate>
|
<button ng-if="!csvImporting" ng-click="import()" class="btn btn-primary" translate>
|
||||||
Import {{ items.length - itemsFailed.length }} items
|
Import {{ items.length - itemsFailed.length }} topics
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer">
|
<div class="spacer">
|
25
openslides/topics/views.py
Normal file
25
openslides/topics/views.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from openslides.utils.rest_api import ModelViewSet
|
||||||
|
|
||||||
|
from .access_permissions import TopicAccessPermissions
|
||||||
|
from .models import Topic
|
||||||
|
|
||||||
|
|
||||||
|
class TopicViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint for topics.
|
||||||
|
|
||||||
|
There are the following views: metadata, list, retrieve, create,
|
||||||
|
partial_update, update and destroy.
|
||||||
|
"""
|
||||||
|
access_permissions = TopicAccessPermissions()
|
||||||
|
queryset = Topic.objects.all()
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
"""
|
||||||
|
Returns True if the user has required permissions.
|
||||||
|
"""
|
||||||
|
if self.action in ('list', 'retrieve'):
|
||||||
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
|
else:
|
||||||
|
result = self.request.user.has_perm('agenda.can_manage')
|
||||||
|
return result
|
@ -1,5 +1,5 @@
|
|||||||
from openslides.agenda.models import Item
|
from openslides.agenda.models import Item
|
||||||
from openslides.core.models import CustomSlide
|
from openslides.topics.models import Topic
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ class TestItemManager(TestCase):
|
|||||||
Test that get_root_and_children needs only one db query.
|
Test that get_root_and_children needs only one db query.
|
||||||
"""
|
"""
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
CustomSlide.objects.create(title='item{}'.format(i))
|
Topic.objects.create(title='item{}'.format(i))
|
||||||
|
|
||||||
with self.assertNumQueries(1):
|
with self.assertNumQueries(1):
|
||||||
Item.objects.get_root_and_children()
|
Item.objects.get_root_and_children()
|
||||||
|
@ -3,15 +3,15 @@ import json
|
|||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from openslides.agenda.models import Item
|
from openslides.agenda.models import Item
|
||||||
from openslides.core.models import CustomSlide
|
from openslides.topics.models import Topic
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
class AgendaTreeTest(TestCase):
|
class AgendaTreeTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
CustomSlide.objects.create(title='item1')
|
Topic.objects.create(title='item1')
|
||||||
item2 = CustomSlide.objects.create(title='item2').agenda_item
|
item2 = Topic.objects.create(title='item2').agenda_item
|
||||||
item3 = CustomSlide.objects.create(title='item2a').agenda_item
|
item3 = Topic.objects.create(title='item2a').agenda_item
|
||||||
item3.parent = item2
|
item3.parent = item2
|
||||||
item3.save()
|
item3.save()
|
||||||
self.client = APIClient()
|
self.client = APIClient()
|
||||||
@ -90,7 +90,7 @@ class TestAgendaPDF(TestCase):
|
|||||||
"""
|
"""
|
||||||
Tests that a requst on the pdf-page returns with statuscode 200.
|
Tests that a requst on the pdf-page returns with statuscode 200.
|
||||||
"""
|
"""
|
||||||
CustomSlide.objects.create(title='item1')
|
Topic.objects.create(title='item1')
|
||||||
self.client.login(username='admin', password='admin')
|
self.client.login(username='admin', password='admin')
|
||||||
|
|
||||||
response = self.client.get('/agenda/print/')
|
response = self.client.get('/agenda/print/')
|
||||||
|
@ -5,7 +5,8 @@ from rest_framework.test import APIClient
|
|||||||
|
|
||||||
from openslides.agenda.models import Item, Speaker
|
from openslides.agenda.models import Item, Speaker
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.core.models import CustomSlide, Projector
|
from openslides.core.models import Projector
|
||||||
|
from openslides.topics.models import Topic
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
@ -16,7 +17,7 @@ class RetrieveItem(TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = APIClient()
|
self.client = APIClient()
|
||||||
config['general_system_enable_anonymous'] = True
|
config['general_system_enable_anonymous'] = True
|
||||||
self.item = CustomSlide.objects.create(title='test_title_Idais2pheepeiz5uph1c').agenda_item
|
self.item = Topic.objects.create(title='test_title_Idais2pheepeiz5uph1c').agenda_item
|
||||||
|
|
||||||
def test_normal_by_anonymous_without_perm_to_see_hidden_items(self):
|
def test_normal_by_anonymous_without_perm_to_see_hidden_items(self):
|
||||||
group = get_user_model().groups.field.related_model.objects.get(pk=1) # Group with pk 1 is for anonymous users.
|
group = get_user_model().groups.field.related_model.objects.get(pk=1) # Group with pk 1 is for anonymous users.
|
||||||
@ -47,7 +48,7 @@ class ManageSpeaker(TestCase):
|
|||||||
self.client = APIClient()
|
self.client = APIClient()
|
||||||
self.client.login(username='admin', password='admin')
|
self.client.login(username='admin', password='admin')
|
||||||
|
|
||||||
self.item = CustomSlide.objects.create(title='test_title_aZaedij4gohn5eeQu8fe').agenda_item
|
self.item = Topic.objects.create(title='test_title_aZaedij4gohn5eeQu8fe').agenda_item
|
||||||
self.user = get_user_model().objects.create_user(
|
self.user = get_user_model().objects.create_user(
|
||||||
username='test_user_jooSaex1bo5ooPhuphae',
|
username='test_user_jooSaex1bo5ooPhuphae',
|
||||||
password='test_password_e6paev4zeeh9n')
|
password='test_password_e6paev4zeeh9n')
|
||||||
@ -164,7 +165,7 @@ class Speak(TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = APIClient()
|
self.client = APIClient()
|
||||||
self.client.login(username='admin', password='admin')
|
self.client.login(username='admin', password='admin')
|
||||||
self.item = CustomSlide.objects.create(title='test_title_KooDueco3zaiGhiraiho').agenda_item
|
self.item = Topic.objects.create(title='test_title_KooDueco3zaiGhiraiho').agenda_item
|
||||||
self.user = get_user_model().objects.create_user(
|
self.user = get_user_model().objects.create_user(
|
||||||
username='test_user_Aigh4vohb3seecha4aa4',
|
username='test_user_Aigh4vohb3seecha4aa4',
|
||||||
password='test_password_eneupeeVo5deilixoo8j')
|
password='test_password_eneupeeVo5deilixoo8j')
|
||||||
@ -273,19 +274,19 @@ class Numbering(TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = APIClient()
|
self.client = APIClient()
|
||||||
self.client.login(username='admin', password='admin')
|
self.client.login(username='admin', password='admin')
|
||||||
self.item_1 = CustomSlide.objects.create(title='test_title_thuha8eef7ohXar3eech').agenda_item
|
self.item_1 = Topic.objects.create(title='test_title_thuha8eef7ohXar3eech').agenda_item
|
||||||
self.item_1.type = Item.AGENDA_ITEM
|
self.item_1.type = Item.AGENDA_ITEM
|
||||||
self.item_1.weight = 1
|
self.item_1.weight = 1
|
||||||
self.item_1.save()
|
self.item_1.save()
|
||||||
self.item_2 = CustomSlide.objects.create(title='test_title_eisah7thuxa1eingaeLo').agenda_item
|
self.item_2 = Topic.objects.create(title='test_title_eisah7thuxa1eingaeLo').agenda_item
|
||||||
self.item_2.type = Item.AGENDA_ITEM
|
self.item_2.type = Item.AGENDA_ITEM
|
||||||
self.item_2.weight = 2
|
self.item_2.weight = 2
|
||||||
self.item_2.save()
|
self.item_2.save()
|
||||||
self.item_2_1 = CustomSlide.objects.create(title='test_title_Qui0audoaz5gie1phish').agenda_item
|
self.item_2_1 = Topic.objects.create(title='test_title_Qui0audoaz5gie1phish').agenda_item
|
||||||
self.item_2_1.type = Item.AGENDA_ITEM
|
self.item_2_1.type = Item.AGENDA_ITEM
|
||||||
self.item_2_1.parent = self.item_2
|
self.item_2_1.parent = self.item_2
|
||||||
self.item_2_1.save()
|
self.item_2_1.save()
|
||||||
self.item_3 = CustomSlide.objects.create(title='test_title_ah7tphisheineisgaeLo').agenda_item
|
self.item_3 = Topic.objects.create(title='test_title_ah7tphisheineisgaeLo').agenda_item
|
||||||
self.item_3.type = Item.AGENDA_ITEM
|
self.item_3.type = Item.AGENDA_ITEM
|
||||||
self.item_3.weight = 3
|
self.item_3.weight = 3
|
||||||
self.item_3.save()
|
self.item_3.save()
|
||||||
|
@ -6,7 +6,8 @@ from rest_framework.test import APIClient
|
|||||||
|
|
||||||
from openslides import __version__ as version
|
from openslides import __version__ as version
|
||||||
from openslides.core.config import ConfigVariable, config
|
from openslides.core.config import ConfigVariable, config
|
||||||
from openslides.core.models import CustomSlide, Projector
|
from openslides.core.models import Projector
|
||||||
|
from openslides.topics.models import Topic
|
||||||
from openslides.utils.rest_api import ValidationError
|
from openslides.utils.rest_api import ValidationError
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
@ -17,10 +18,10 @@ class ProjectorAPI(TestCase):
|
|||||||
"""
|
"""
|
||||||
def test_slide_on_default_projector(self):
|
def test_slide_on_default_projector(self):
|
||||||
self.client.login(username='admin', password='admin')
|
self.client.login(username='admin', password='admin')
|
||||||
customslide = CustomSlide.objects.create(title='title_que1olaish5Wei7que6i', text='text_aishah8Eh7eQuie5ooji')
|
topic = Topic.objects.create(title='title_que1olaish5Wei7que6i', text='text_aishah8Eh7eQuie5ooji')
|
||||||
default_projector = Projector.objects.get(pk=1)
|
default_projector = Projector.objects.get(pk=1)
|
||||||
default_projector.config = {
|
default_projector.config = {
|
||||||
'aae4a07b26534cfb9af4232f361dce73': {'name': 'core/customslide', 'id': customslide.id}}
|
'aae4a07b26534cfb9af4232f361dce73': {'name': 'topics/topic', 'id': topic.id}}
|
||||||
default_projector.save()
|
default_projector.save()
|
||||||
|
|
||||||
response = self.client.get(reverse('projector-detail', args=['1']))
|
response = self.client.get(reverse('projector-detail', args=['1']))
|
||||||
@ -30,9 +31,9 @@ class ProjectorAPI(TestCase):
|
|||||||
'id': 1,
|
'id': 1,
|
||||||
'elements': {
|
'elements': {
|
||||||
'aae4a07b26534cfb9af4232f361dce73':
|
'aae4a07b26534cfb9af4232f361dce73':
|
||||||
{'id': customslide.id,
|
{'id': topic.id,
|
||||||
'uuid': 'aae4a07b26534cfb9af4232f361dce73',
|
'uuid': 'aae4a07b26534cfb9af4232f361dce73',
|
||||||
'name': 'core/customslide'}},
|
'name': 'topics/topic'}},
|
||||||
'scale': 0,
|
'scale': 0,
|
||||||
'scroll': 0,
|
'scroll': 0,
|
||||||
'width': 1024,
|
'width': 1024,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from openslides.agenda.models import Item, Speaker
|
from openslides.agenda.models import Item, Speaker
|
||||||
from openslides.core.models import CustomSlide
|
from openslides.topics.models import Topic
|
||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
@ -7,8 +7,8 @@ from openslides.utils.test import TestCase
|
|||||||
|
|
||||||
class ListOfSpeakerModelTests(TestCase):
|
class ListOfSpeakerModelTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.item1 = CustomSlide.objects.create(title='item1').agenda_item
|
self.item1 = Topic.objects.create(title='item1').agenda_item
|
||||||
self.item2 = CustomSlide.objects.create(title='item2').agenda_item
|
self.item2 = Topic.objects.create(title='item2').agenda_item
|
||||||
self.speaker1 = User.objects.create(username='user1')
|
self.speaker1 = User.objects.create(username='user1')
|
||||||
self.speaker2 = User.objects.create(username='user2')
|
self.speaker2 = User.objects.create(username='user2')
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user