Merge pull request #1552 from ostcar/projector_client

Projector client
This commit is contained in:
Oskar Hahn 2015-06-18 23:29:14 +02:00
commit e24ae3fd2a
22 changed files with 455 additions and 257 deletions

View File

@ -1,5 +1,18 @@
angular.module('OpenSlidesApp.agenda', [])
.factory('Agenda', function(DS) {
return DS.defineResource({
name: 'agenda/item',
endpoint: '/rest/agenda/item/'
});
})
// Make sure that the Agenda resource is loaded.
.run(function(Agenda) {});
angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
.config(function($stateProvider) {
$stateProvider
.state('agenda', {
@ -68,13 +81,6 @@ angular.module('OpenSlidesApp.agenda', [])
});
})
.factory('Agenda', function(DS) {
return DS.defineResource({
name: 'agenda/item',
endpoint: '/rest/agenda/item/'
});
})
.controller('ItemListCtrl', function($scope, Agenda, tree) {
Agenda.bindAll({}, $scope, 'items');

View File

@ -1,5 +1,17 @@
angular.module('OpenSlidesApp.assignments', [])
.factory('Assignment', function(DS) {
return DS.defineResource({
name: 'assignments/assignment',
endpoint: '/rest/assignments/assignment/'
});
})
.run(function(Assignment) {});
angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
.config(function($stateProvider) {
$stateProvider
.state('assignments', {
@ -37,13 +49,6 @@ angular.module('OpenSlidesApp.assignments', [])
});
})
.factory('Assignment', function(DS) {
return DS.defineResource({
name: 'assignments/assignment',
endpoint: '/rest/assignments/assignment/'
});
})
.controller('AssignmentListCtrl', function($scope, Assignment, phases) {
Assignment.bindAll({}, $scope, 'assignments');
// get all item types via OPTIONS request

View File

@ -41,7 +41,7 @@ class Projector(RESTModelMixin, models.Model):
('can_use_chat', ugettext_noop('Can use the chat')))
@property
def projector_elements(self):
def elements(self):
"""
A generator to retrieve all projector elements given in the config
field. For every element the method get_data() is called and its

View File

@ -13,24 +13,16 @@ class CustomSlideSlide(ProjectorElement):
Slide definitions for custom slide model.
"""
name = 'core/customslide'
scripts = 'core/customslide_slide.js'
def get_context(self):
pk = self.config_entry.get('id')
if not CustomSlide.objects.filter(pk=pk).exists():
raise ProjectorException(_('Custom slide does not exist.'))
return [{
'collection': 'core/customslide',
'id': pk}]
return {'id': pk}
def get_requirements(self, config_entry):
self.config_entry = config_entry
try:
pk = self.get_context()[0]['id']
except ProjectorException:
# Custom slide does not exist so just do nothing.
pass
else:
pk = config_entry.get('id')
if pk is not None:
yield ProjectorRequirement(
view_class=CustomSlideViewSet,
view_action='retrieve',
@ -42,7 +34,6 @@ class Clock(ProjectorElement):
Clock on the projector.
"""
name = 'core/clock'
scripts = 'core/clock.js'
def get_context(self):
return {'server_time': now().timestamp()}
@ -73,7 +64,6 @@ class Countdown(ProjectorElement):
To hide a running countdown add {"hidden": true}.
"""
name = 'core/countdown'
scripts = 'core/countdown.js'
def get_context(self):
if self.config_entry.get('countdown_time') is None:
@ -88,7 +78,6 @@ class Message(ProjectorElement):
Short message on the projector. Rendered as overlay.
"""
name = 'core/message'
scripts = 'core/message.js'
def get_context(self):
if self.config_entry.get('message') is None:

View File

@ -33,7 +33,7 @@ class ProjectorSerializer(ModelSerializer):
class Meta:
model = Projector
fields = ('config', 'projector_elements', )
fields = ('id', 'config', 'elements', )
class CustomSlideSerializer(ModelSerializer):

View File

@ -1,24 +1,38 @@
angular.module('OpenSlidesApp', [
'ui.router',
'angular-loading-bar',
'js-data',
'gettext',
'ngBootbox',
'ngFabForm',
'ngMessages',
'ngAnimate',
'ngCsvImport',
'ngSanitize',
'ui.bootstrap',
'ui.select',
'ui.tree',
'xeditable',
'OpenSlidesApp.core',
]);
angular.module('OpenSlidesApp.projector', [
'OpenSlidesApp',
'OpenSlidesApp.core.projector',
'OpenSlidesApp.agenda',
'OpenSlidesApp.motions',
'OpenSlidesApp.assignments',
'OpenSlidesApp.users',
'OpenSlidesApp.users.projector',
'OpenSlidesApp.mediafiles',
]);
angular.module('OpenSlidesApp.site', [
'OpenSlidesApp',
'ui.router',
'ngBootbox',
'ngFabForm',
'ngMessages',
'ngCsvImport',
'ngSanitize', // TODO: remove this as global dependency
'ui.select',
'xeditable',
'OpenSlidesApp.core.site',
'OpenSlidesApp.agenda.site',
'OpenSlidesApp.motions.site',
'OpenSlidesApp.assignments.site',
'OpenSlidesApp.users.site',
'OpenSlidesApp.mediafiles.site',
])
.config(function($urlRouterProvider, $locationProvider) {

View File

@ -1,5 +1,119 @@
// The core module used for the OpenSlides site and the projector
angular.module('OpenSlidesApp.core', [])
.config(function(DSProvider) {
// Reloads everything after 5 minutes.
// TODO: * find a way only to reload things that are still needed
DSProvider.defaults.maxAge = 5 * 60 * 1000; // 5 minutes
DSProvider.defaults.reapAction = 'none';
DSProvider.defaults.afterReap = function(model, items) {
if (items.length > 5) {
model.findAll({}, {bypassCache: true});
} else {
_.forEach(items, function (item) {
model.refresh(item[model.idAttribute]);
});
}
};
})
.run(function(DS, autoupdate) {
autoupdate.on_message(function(data) {
// TODO: when MODEL.find() is called after this
// a new request is fired. This could be a bug in DS
// TODO: Do not send the status code to the client, but make the decission
// on the server side. It is an implementation detail, that tornado
// sends request to wsgi, which should not concern the client.
console.log("Received object: " + data.collection + ", " + data.id);
if (data.status_code == 200) {
DS.inject(data.collection, data.data);
} else if (data.status_code == 404) {
DS.eject(data.collection, data.id);
}
// TODO: handle other statuscodes
});
})
.run(function($rootScope, Config) {
// Puts the config object into each scope.
// TODO: maybe rootscope.config has to set before findAll() is finished
Config.findAll().then(function() {
$rootScope.config = function(key) {
try {
return Config.get(key).value;
}
catch(err) {
console.log("Unkown config key: " + key);
return ''
}
}
});
})
.factory('autoupdate', function() {
var url = location.origin + "/sockjs";
var Autoupdate = {
socket: null,
message_receivers: [],
connect: function() {
var autoupdate = this;
this.socket = new SockJS(url);
this.socket.onmessage = function(event) {
_.forEach(autoupdate.message_receivers, function(receiver) {
receiver(event.data);
});
}
this.socket.onclose = function() {
setTimeout(autoupdate.connect, 5000);
}
},
on_message: function(receiver) {
this.message_receivers.push(receiver);
}
};
Autoupdate.connect();
return Autoupdate;
})
.factory('Customslide', function(DS) {
return DS.defineResource({
name: 'core/customslide',
endpoint: '/rest/core/customslide/'
});
})
.factory('Tag', function(DS) {
return DS.defineResource({
name: 'core/tag',
endpoint: '/rest/core/tag/'
});
})
.factory('Config', function(DS) {
return DS.defineResource({
name: 'config/config',
idAttribute: 'key',
endpoint: '/rest/config/config/'
});
})
.factory('Projector', function(DS) {
return DS.defineResource({
name: 'core/projector',
endpoint: '/rest/core/projector/',
});
})
.run(function(Projector, Config, Tag, Customslide){});
// The core module for the OpenSlides site
angular.module('OpenSlidesApp.core.site', ['OpenSlidesApp.core'])
.config(function($stateProvider, $urlMatcherFactoryProvider) {
// Make the trailing slash optional
$urlMatcherFactoryProvider.strictMode(false)
@ -80,6 +194,13 @@ angular.module('OpenSlidesApp.core', [])
url: '/',
templateUrl: 'static/templates/dashboard.html'
})
.state('projector', {
url: '/projector',
data: {extern: true},
onEnter: function($window) {
$window.location.href = this.url;
}
})
.state('core', {
url: '/core',
abstract: true,
@ -108,22 +229,6 @@ angular.module('OpenSlidesApp.core', [])
$locationProvider.html5Mode(true);
})
.config(function(DSProvider) {
// Reloads everything after 5 minutes.
// TODO: * find a way only to reload things that are still needed
DSProvider.defaults.maxAge = 5 * 60 * 1000; // 5 minutes
DSProvider.defaults.reapAction = 'none';
DSProvider.defaults.afterReap = function(model, items) {
if (items.length > 5) {
model.findAll({}, {bypassCache: true});
} else {
_.forEach(items, function (item) {
model.refresh(item[model.idAttribute]);
});
}
};
})
// config for ng-fab-form
.config(function(ngFabFormProvider) {
ngFabFormProvider.extendConfig({
@ -131,6 +236,8 @@ angular.module('OpenSlidesApp.core', [])
});
})
// Helper to add ui.router states at runtime.
// Needed for the django url_patterns.
.provider('runtimeStates', function($stateProvider) {
this.$get = function($q, $timeout, $state) {
return {
@ -141,6 +248,7 @@ angular.module('OpenSlidesApp.core', [])
}
})
// Load the django url patterns
.run(function(runtimeStates, $http) {
$http.get('/core/url_patterns/').then(function(data) {
for (var pattern in data.data) {
@ -155,87 +263,20 @@ angular.module('OpenSlidesApp.core', [])
});
})
.run(function(DS, autoupdate) {
autoupdate.on_message(function(data) {
// TODO: when MODEL.find() is called after this
// a new request is fired. This could be a bug in DS
// TODO: Do not send the status code to the client, but make the decission
// on the server side. It is an implementation detail, that tornado
// sends request to wsgi, which should not concern the client.
if (data.status_code == 200) {
DS.inject(data.collection, data.data);
} else if (data.status_code == 404) {
DS.eject(data.collection, data.id);
}
// TODO: handle other statuscodes
});
})
.run(function($rootScope, Config) {
// Puts the config object into each scope.
// TODO: maybe rootscope.config has to set before findAll() is finished
Config.findAll().then(function() {;
$rootScope.config = function(key) {
return Config.get(key).value;
}
});
})
//options for angular-xeditable
// options for angular-xeditable
.run(function(editableOptions) {
editableOptions.theme = 'bs3';
})
.factory('autoupdate', function() {
//TODO: use config here
var url = "http://" + location.host + "/sockjs";
var Autoupdate = {
socket: null,
message_receivers: [],
connect: function() {
var autoupdate = this;
this.socket = new SockJS(url);
this.socket.onmessage = function(event) {
_.forEach(autoupdate.message_receivers, function(receiver) {
receiver(event.data);
});
}
this.socket.onclose = function() {
setTimeout(autoupdate.connect, 5000);
}
},
on_message: function(receiver) {
this.message_receivers.push(receiver);
}
// Activate an Element from the Rest-API on the projector
// At the moment it only activates item on projector 1
.factory('projectorActivate', function($http) {
return function(model, id) {
return $http.post(
'/rest/core/projector/1/prune_elements/',
[{name: model.name, id: id}]
);
};
Autoupdate.connect();
return Autoupdate;
})
.factory('Customslide', function(DS) {
return DS.defineResource({
name: 'core/customslide',
endpoint: '/rest/core/customslide/'
});
})
.factory('Tag', function(DS) {
return DS.defineResource({
name: 'core/tag',
endpoint: '/rest/core/tag/'
});
})
.factory('Config', function(DS) {
return DS.defineResource({
name: 'config/config',
idAttribute: 'key',
endpoint: '/rest/config/config/'
});
})
.controller("LanguageCtrl", function ($scope, gettextCatalog) {
@ -346,3 +387,82 @@ angular.module('OpenSlidesApp.core', [])
}
};
});
// The core module for the OpenSlides projector
angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
// Provider to register slides in a .config() statement.
.provider('slides', function() {
var slidesMap = {};
this.registerSlide = function(name, config) {
slidesMap[name] = config;
return this;
};
this.$get = function($templateRequest, $q) {
var self = this;
return {
getElements: function(projector) {
var elements = [];
var factory = this;
_.forEach(projector.elements, function(element) {
if (element.name in slidesMap) {
element.template = slidesMap[element.name].template;
elements.push(element);
} else {
console.log("Unknown slide: " + element.name);
}
});
return elements;
}
}
};
})
.config(function(slidesProvider) {
slidesProvider.registerSlide('core/customslide', {
template: 'static/templates/core/slide_customslide.html',
});
slidesProvider.registerSlide('core/clock', {
template: 'static/templates/core/slide_clock.html',
});
})
.filter('osServertime',function() {
return function(serverOffset) {
var date = new Date();
return date.setTime(date.getTime() - serverOffset);
};
})
.controller('ProjectorCtrl', function($scope, Projector, slides) {
Projector.find(1).then(function() {
$scope.$watch(function () {
return Projector.lastModified(1);
}, function () {
$scope.elements = [];
_.forEach(slides.getElements(Projector.get(1)), function(element) {
$scope.elements.push(element);
});
});
});
})
.controller('SlideCustomSlideCtr', 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.context.id;
Customslide.find(id);
Customslide.bindOne(id, $scope, 'customslide');
})
.controller('SlideClockCtr', function($scope) {
// 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.
$scope.serverOffset = Date.parse(new Date().toUTCString()) - $scope.element.context.server_time;
});

View File

@ -0,0 +1,3 @@
<div ng-controller="SlideClockCtr">
{{ serverOffset | osServertime | date:'HH:mm' }} Uhr
</div>

View File

@ -0,0 +1,4 @@
<div ng-controller="SlideCustomSlideCtr">
<h1>{{ customslide.title }}</h1>
{{ customslide.text }}
</div>

View File

@ -2,7 +2,7 @@
<!--[if lt IE 7]> <html lang="en" ng-app="myApp" class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html lang="en" ng-app="myApp" class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html lang="en" ng-app="myApp" class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html lang="en" ng-app="OpenSlidesApp" class="no-js"> <!--<![endif]-->
<!--[if gt IE 8]><!--> <html lang="en" ng-app="OpenSlidesApp.site" class="no-js"> <!--<![endif]-->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<base href="/">
@ -133,7 +133,7 @@
<span class="text" translate>Home</span>
</a>
<li>
<a href="#TODO">
<a ui-sref="projector">
<span class="ico"><i class="fa fa-video-camera fa-lg"></i></span>
<span class="text" translate>Projector</span>
</a>

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en" ng-app="OpenSlidesApp.projector" class="no-js">
<meta charset="utf-8">
<base href="/">
<title>OpenSlides Projector</title>
<link rel="stylesheet" href="static/css/openslides-libs.css">
<link rel="stylesheet" href="static/css/projector.css">
<script src="static/js/openslides-libs.js"></script>
<div id="header">
<img id="logo" src="/static/img/logo.png" alt="OpenSlides" />
<span class="navbar-text optional">{{ config('general_event_name') }}</span>
</div>
<div id="content" ng-controller="ProjectorCtrl">
<div ng-repeat="element in elements"><div ng-include="element.template"></div></div>
</div>
<script src="static/js/app.js"></script>
<script src="static/js/core.js"></script>
<script src="static/js/agenda/agenda.js"></script>
<script src="static/js/motions/motions.js"></script>
<script src="static/js/assignments/assignments.js"></script>
<script src="static/js/users/users.js"></script>
<script src="static/js/mediafiles/mediafiles.js"></script>

View File

@ -10,4 +10,11 @@ urlpatterns = patterns(
url(r'^core/version/$',
views.VersionView.as_view(),
name='core_version'),
# View for the projectors are handelt by angular.
url(r'^projector.*$', views.ProjectorView.as_view()),
# Main entry point for all angular pages.
# Has to be the last entry in the urls.py
url(r'^.*$', views.IndexView.as_view()),
)

View File

@ -43,6 +43,17 @@ class IndexView(utils_views.CSRFMixin, utils_views.View):
return HttpResponse(content)
class ProjectorView(utils_views.View):
"""
Access the projector.
"""
def get(self, *args, **kwargs):
with open(finders.find('templates/projector.html')) as f:
content = f.read()
return HttpResponse(content)
class ProjectorViewSet(ReadOnlyModelViewSet):
"""
API endpoint to list, retrieve and update the projector slide info.

View File

@ -1,5 +1,17 @@
angular.module('OpenSlidesApp.mediafiles', [])
.factory('Mediafile', function(DS) {
return DS.defineResource({
name: 'mediafiles/mediafile',
endpoint: '/rest/mediafiles/mediafile/'
});
})
.run(function(Mediafile) {});
angular.module('OpenSlidesApp.mediafiles.site', ['OpenSlidesApp.mediafiles'])
.config(function($stateProvider) {
$stateProvider
.state('mediafiles', {
@ -26,13 +38,6 @@ angular.module('OpenSlidesApp.mediafiles', [])
});
})
.factory('Mediafile', function(DS) {
return DS.defineResource({
name: 'mediafiles/mediafile',
endpoint: '/rest/mediafiles/mediafile/'
});
})
.controller('MediafileListCtrl', function($scope, $http, Mediafile) {
Mediafile.bindAll({}, $scope, 'mediafiles');
@ -80,4 +85,3 @@ angular.module('OpenSlidesApp.mediafiles', [])
);
};
});

View File

@ -1,5 +1,29 @@
angular.module('OpenSlidesApp.motions', [])
.factory('Motion', function(DS) {
return DS.defineResource({
name: 'motions/motion',
endpoint: '/rest/motions/motion/'
});
})
.factory('Category', function(DS) {
return DS.defineResource({
name: 'motions/category',
endpoint: '/rest/motions/category/'
});
})
.factory('Workflow', function(DS) {
return DS.defineResource({
name: 'motions/workflow',
endpoint: '/rest/motions/workflow/'
});
})
.run(function(Motion, Category, Workflow) {});
angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
.config(function($stateProvider) {
$stateProvider
.state('motions', {
@ -104,25 +128,6 @@ angular.module('OpenSlidesApp.motions', [])
})
})
.factory('Motion', function(DS) {
return DS.defineResource({
name: 'motions/motion',
endpoint: '/rest/motions/motion/'
});
})
.factory('Category', function(DS) {
return DS.defineResource({
name: 'motions/category',
endpoint: '/rest/motions/category/'
});
})
.factory('Workflow', function(DS) {
return DS.defineResource({
name: 'motions/workflow',
endpoint: '/rest/motions/workflow/'
});
})
.controller('MotionListCtrl', function($scope, Motion) {
Motion.bindAll({}, $scope, 'motions');

View File

@ -1,17 +1,17 @@
from django.conf.urls import include, patterns, url
from django.views.generic import RedirectView
from openslides.core.views import IndexView
from openslides.utils.rest_api import router
urlpatterns = patterns(
'',
url(r'^(?P<url>.*[^/])$', RedirectView.as_view(url='/%(url)s/')),
url(r'^rest/', include(router.urls)),
url(r'^', include('openslides.core.urls')),
url(r'^agenda/', include('openslides.agenda.urls')),
url(r'^assignments/', include('openslides.assignments.urls')),
url(r'^motions/', include('openslides.motions.urls')),
url(r'^users/', include('openslides.users.urls')),
url(r'^.*$', IndexView.as_view()),
# The urls.py for the core app has to be the last entry in the urls.py
url(r'^', include('openslides.core.urls')),
)

View File

@ -12,33 +12,23 @@ class UserSlide(ProjectorElement):
Slide definitions for user model.
"""
name = 'users/user'
scripts = 'users/user_slide.js'
def get_context(self):
pk = self.config_entry.get('id')
try:
user = User.objects.get(pk=pk)
except User.DoesNotExist:
if not User.objects.filter(pk=pk).exists():
raise ProjectorException(_('User does not exist.'))
result = [{
'collection': 'users/user',
'id': pk}]
for group in user.groups.all():
result.append({
'collection': 'users/group',
'id': group.pk})
return result
return {'id': pk}
def get_requirements(self, config_entry):
self.config_entry = config_entry
try:
context = self.get_context()
except ProjectorException:
# User does not exist so just do nothing.
pass
else:
for item in context:
pk = config_entry.get('id')
if pk is not None:
yield ProjectorRequirement(
view_class=UserViewSet,
view_action='retrive',
pk=pk)
for group in User.objects.get(pk=pk).groups.all():
yield ProjectorRequirement(
view_class=UserViewSet if item['collection'] == 'users/user' else GroupViewSet,
view_class=GroupViewSet,
view_action='retrieve',
pk=str(item['id']))
pk=str(group.pk))

View File

@ -1,5 +1,56 @@
angular.module('OpenSlidesApp.users', [])
.factory('User', function(DS, Group) {
return DS.defineResource({
name: 'users/user',
endpoint: '/rest/users/user/',
methods: {
get_short_name: function() {
// should be the same as in the python user model.
var firstName = _.trim(this.first_name),
lastName = _.trim(this.last_name),
name;
if (firstName && lastName) {
// TODO: check config
name = [firstName, lastName].join(' ');
} else {
name = firstName || lastName || this.username;
}
return name;
},
getPerms: function() {
var allPerms = [];
_.forEach(this.groups, function(groupId) {
// Get group from server
Group.find(groupId);
// But do not work with the returned promise, because in
// this case this method can not be called in $watch
group = Group.get(groupId);
if (group) {
_.forEach(group.permissions, function(perm) {
allPerms.push(perm);
});
}
});
return _.uniq(allPerms);
},
},
});
})
.factory('Group', function(DS) {
return DS.defineResource({
name: 'users/group',
endpoint: '/rest/users/group/'
});
})
.run(function(User, Group) {});
angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
.config(function($stateProvider) {
$stateProvider
.state('users', {
@ -126,52 +177,6 @@ angular.module('OpenSlidesApp.users', [])
$rootScope.operator = operator;
})
.factory('User', function(DS, Group) {
return DS.defineResource({
name: 'users/user',
endpoint: '/rest/users/user/',
methods: {
get_short_name: function() {
// should be the same as in the python user model.
var firstName = _.trim(this.first_name),
lastName = _.trim(this.last_name),
name;
if (firstName && lastName) {
// TODO: check config
name = [firstName, lastName].join(' ');
} else {
name = firstName || lastName || this.username;
}
return name;
},
getPerms: function() {
var allPerms = [];
_.forEach(this.groups, function(groupId) {
// Get group from server
Group.find(groupId);
// But do not work with the returned promise, because in
// this case this method can not be called in $watch
group = Group.get(groupId);
if (group) {
_.forEach(group.permissions, function(perm) {
allPerms.push(perm);
});
}
});
return _.uniq(allPerms);
},
},
});
})
.factory('Group', function(DS) {
return DS.defineResource({
name: 'users/group',
endpoint: '/rest/users/group/'
});
})
/*
* Directive to check for permissions
*
@ -239,7 +244,7 @@ angular.module('OpenSlidesApp.users', [])
};
}])
.controller('UserListCtrl', function($scope, User) {
.controller('UserListCtrl', function($scope, User, projectorActivate) {
User.bindAll({}, $scope, 'users');
// setup table sorting
@ -264,6 +269,10 @@ angular.module('OpenSlidesApp.users', [])
$scope.delete = function (user) {
User.destroy(user.id);
};
$scope.project = function(user) {
projectorActivate(User, user.id).error(function() {console.log('success')});
};
})
.controller('UserDetailCtrl', function($scope, User, user) {
@ -369,9 +378,25 @@ angular.module('OpenSlidesApp.users', [])
// DS.flush();
});
};
});
angular.module('OpenSlidesApp.users.projector', ['OpenSlidesApp.users'])
.config(function(slidesProvider) {
slidesProvider.registerSlide('users/user', {
template: 'static/templates/users/slide_user.html',
});
})
.controller('SlideUserCtr', function($scope, User) {
// 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.context.id;
User.find(id);
User.bindOne(id, $scope, 'user');
});
// this is code from angular.js. Find a way to call this function from this file
function getBlockNodes(nodes) {

View File

@ -0,0 +1,3 @@
<div ng-controller="SlideUserCtr">
<h1>{{ user.username }}</h1>
</div>

View File

@ -106,7 +106,8 @@
<td os-perms="users.can_manage" class="optional">{{ user.last_login | date:'yyyy-MM-dd HH:mm:ss'}}
<td os-perms="users.can_manage core.can_manage_projector" class="nobr">
<!-- projector, TODO: add link to activate slidea-->
<a href="#TODO" 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-click="project(user)"
title="{{ 'Show' | translate }}">
<i class="fa fa-video-camera"></i>
</a>

View File

@ -8,13 +8,12 @@ class ProjectorElement(object, metaclass=SignalConnectMetaClass):
Base class for an element on the projector.
Every app which wants to add projector elements has to create classes
subclassing from this base class with different names. The name and
scripts attributes have to be set. The metaclass
(SignalConnectMetaClass) does the rest of the magic.
subclassing from this base class with different names. The name attribute
has to be set. The metaclass (SignalConnectMetaClass) does the rest of the
magic.
"""
signal = Signal()
name = None
scripts = None
def __init__(self, **kwargs):
"""
@ -45,20 +44,8 @@ class ProjectorElement(object, metaclass=SignalConnectMetaClass):
assert self.config_entry.get('name') == self.name, (
'To get data of a projector element, the correct config entry has to be given.')
return {
'scripts': self.get_scripts(),
'context': self.get_context()}
def get_scripts(self):
"""
Returns ...?
"""
# TODO: Write docstring
if self.scripts is None:
raise NotImplementedError(
'A projector element class must define either a '
'get_scripts method or have a scripts argument.')
return self.scripts
def get_context(self):
"""
Returns the context of the projector element.

View File

@ -23,13 +23,11 @@ class ProjectorAPI(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(response.content.decode()), {
'id': 1,
'config': [{'name': 'core/customslide', 'id': customslide.id}],
'projector_elements': [
'elements': [
{'name': 'core/customslide',
'scripts': 'core/customslide_slide.js',
'context': [
{'collection': 'core/customslide',
'id': customslide.id}]}]})
'context': {'id': customslide.id}}]})
def test_invalid_slide_on_default_projector(self):
self.client.login(username='admin', password='admin')
@ -41,8 +39,9 @@ class ProjectorAPI(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(response.content.decode()), {
'id': 1,
'config': [{'name': 'invalid_slide'}],
'projector_elements': [
'elements': [
{'name': 'invalid_slide',
'error': 'Projector element does not exist.'}]})