Angular Client

* Split angular app into a site- and a projector app
* Created client slide api and slides for customslide and user
* JS-function to activate a slide
This commit is contained in:
Oskar Hahn 2015-06-17 09:45:00 +02:00
parent a5d9f0bb42
commit a4c00d5ee3
22 changed files with 455 additions and 257 deletions

View File

@ -1,5 +1,18 @@
angular.module('OpenSlidesApp.agenda', []) 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) { .config(function($stateProvider) {
$stateProvider $stateProvider
.state('agenda', { .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) { .controller('ItemListCtrl', function($scope, Agenda, tree) {
Agenda.bindAll({}, $scope, 'items'); Agenda.bindAll({}, $scope, 'items');

View File

@ -1,5 +1,17 @@
angular.module('OpenSlidesApp.assignments', []) 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) { .config(function($stateProvider) {
$stateProvider $stateProvider
.state('assignments', { .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) { .controller('AssignmentListCtrl', function($scope, Assignment, phases) {
Assignment.bindAll({}, $scope, 'assignments'); Assignment.bindAll({}, $scope, 'assignments');
// get all item types via OPTIONS request // 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'))) ('can_use_chat', ugettext_noop('Can use the chat')))
@property @property
def projector_elements(self): def elements(self):
""" """
A generator to retrieve all projector elements given in the config A generator to retrieve all projector elements given in the config
field. For every element the method get_data() is called and its 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. Slide definitions for custom slide model.
""" """
name = 'core/customslide' name = 'core/customslide'
scripts = 'core/customslide_slide.js'
def get_context(self): def get_context(self):
pk = self.config_entry.get('id') pk = self.config_entry.get('id')
if not CustomSlide.objects.filter(pk=pk).exists(): if not CustomSlide.objects.filter(pk=pk).exists():
raise ProjectorException(_('Custom slide does not exist.')) raise ProjectorException(_('Custom slide does not exist.'))
return [{ return {'id': pk}
'collection': 'core/customslide',
'id': pk}]
def get_requirements(self, config_entry): def get_requirements(self, config_entry):
self.config_entry = config_entry pk = config_entry.get('id')
try: if pk is not None:
pk = self.get_context()[0]['id']
except ProjectorException:
# Custom slide does not exist so just do nothing.
pass
else:
yield ProjectorRequirement( yield ProjectorRequirement(
view_class=CustomSlideViewSet, view_class=CustomSlideViewSet,
view_action='retrieve', view_action='retrieve',
@ -42,7 +34,6 @@ class Clock(ProjectorElement):
Clock on the projector. Clock on the projector.
""" """
name = 'core/clock' name = 'core/clock'
scripts = 'core/clock.js'
def get_context(self): def get_context(self):
return {'server_time': now().timestamp()} return {'server_time': now().timestamp()}
@ -73,7 +64,6 @@ class Countdown(ProjectorElement):
To hide a running countdown add {"hidden": true}. To hide a running countdown add {"hidden": true}.
""" """
name = 'core/countdown' name = 'core/countdown'
scripts = 'core/countdown.js'
def get_context(self): def get_context(self):
if self.config_entry.get('countdown_time') is None: if self.config_entry.get('countdown_time') is None:
@ -88,7 +78,6 @@ class Message(ProjectorElement):
Short message on the projector. Rendered as overlay. Short message on the projector. Rendered as overlay.
""" """
name = 'core/message' name = 'core/message'
scripts = 'core/message.js'
def get_context(self): def get_context(self):
if self.config_entry.get('message') is None: if self.config_entry.get('message') is None:

View File

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

View File

@ -1,24 +1,38 @@
angular.module('OpenSlidesApp', [ angular.module('OpenSlidesApp', [
'ui.router',
'angular-loading-bar', 'angular-loading-bar',
'js-data', 'js-data',
'gettext', 'gettext',
'ngBootbox',
'ngFabForm',
'ngMessages',
'ngAnimate', 'ngAnimate',
'ngCsvImport',
'ngSanitize',
'ui.bootstrap', 'ui.bootstrap',
'ui.select',
'ui.tree', 'ui.tree',
'xeditable', ]);
'OpenSlidesApp.core',
angular.module('OpenSlidesApp.projector', [
'OpenSlidesApp',
'OpenSlidesApp.core.projector',
'OpenSlidesApp.agenda', 'OpenSlidesApp.agenda',
'OpenSlidesApp.motions', 'OpenSlidesApp.motions',
'OpenSlidesApp.assignments', 'OpenSlidesApp.assignments',
'OpenSlidesApp.users', 'OpenSlidesApp.users.projector',
'OpenSlidesApp.mediafiles', '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) { .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', []) 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) { .config(function($stateProvider, $urlMatcherFactoryProvider) {
// Make the trailing slash optional // Make the trailing slash optional
$urlMatcherFactoryProvider.strictMode(false) $urlMatcherFactoryProvider.strictMode(false)
@ -80,6 +194,13 @@ angular.module('OpenSlidesApp.core', [])
url: '/', url: '/',
templateUrl: 'static/templates/dashboard.html' templateUrl: 'static/templates/dashboard.html'
}) })
.state('projector', {
url: '/projector',
data: {extern: true},
onEnter: function($window) {
$window.location.href = this.url;
}
})
.state('core', { .state('core', {
url: '/core', url: '/core',
abstract: true, abstract: true,
@ -108,22 +229,6 @@ angular.module('OpenSlidesApp.core', [])
$locationProvider.html5Mode(true); $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 for ng-fab-form
.config(function(ngFabFormProvider) { .config(function(ngFabFormProvider) {
ngFabFormProvider.extendConfig({ 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) { .provider('runtimeStates', function($stateProvider) {
this.$get = function($q, $timeout, $state) { this.$get = function($q, $timeout, $state) {
return { return {
@ -141,6 +248,7 @@ angular.module('OpenSlidesApp.core', [])
} }
}) })
// Load the django url patterns
.run(function(runtimeStates, $http) { .run(function(runtimeStates, $http) {
$http.get('/core/url_patterns/').then(function(data) { $http.get('/core/url_patterns/').then(function(data) {
for (var pattern in data.data) { for (var pattern in data.data) {
@ -155,87 +263,20 @@ angular.module('OpenSlidesApp.core', [])
}); });
}) })
.run(function(DS, autoupdate) { // options for angular-xeditable
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
.run(function(editableOptions) { .run(function(editableOptions) {
editableOptions.theme = 'bs3'; editableOptions.theme = 'bs3';
}) })
.factory('autoupdate', function() { // Activate an Element from the Rest-API on the projector
//TODO: use config here // At the moment it only activates item on projector 1
var url = "http://" + location.host + "/sockjs"; .factory('projectorActivate', function($http) {
return function(model, id) {
var Autoupdate = { return $http.post(
socket: null, '/rest/core/projector/1/prune_elements/',
message_receivers: [], [{name: model.name, id: id}]
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/'
});
}) })
.controller("LanguageCtrl", function ($scope, gettextCatalog) { .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 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 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 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 charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<base href="/"> <base href="/">
@ -133,7 +133,7 @@
<span class="text" translate>Home</span> <span class="text" translate>Home</span>
</a> </a>
<li> <li>
<a href="#TODO"> <a ui-sref="projector">
<span class="ico"><i class="fa fa-video-camera fa-lg"></i></span> <span class="ico"><i class="fa fa-video-camera fa-lg"></i></span>
<span class="text" translate>Projector</span> <span class="text" translate>Projector</span>
</a> </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/$', url(r'^core/version/$',
views.VersionView.as_view(), views.VersionView.as_view(),
name='core_version'), 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) 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): class ProjectorViewSet(ReadOnlyModelViewSet):
""" """
API endpoint to list, retrieve and update the projector slide info. API endpoint to list, retrieve and update the projector slide info.

View File

@ -1,5 +1,17 @@
angular.module('OpenSlidesApp.mediafiles', []) 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) { .config(function($stateProvider) {
$stateProvider $stateProvider
.state('mediafiles', { .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) { .controller('MediafileListCtrl', function($scope, $http, Mediafile) {
Mediafile.bindAll({}, $scope, 'mediafiles'); Mediafile.bindAll({}, $scope, 'mediafiles');
@ -80,4 +85,3 @@ angular.module('OpenSlidesApp.mediafiles', [])
); );
}; };
}); });

View File

@ -1,5 +1,29 @@
angular.module('OpenSlidesApp.motions', []) 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) { .config(function($stateProvider) {
$stateProvider $stateProvider
.state('motions', { .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) { .controller('MotionListCtrl', function($scope, Motion) {
Motion.bindAll({}, $scope, 'motions'); Motion.bindAll({}, $scope, 'motions');

View File

@ -1,17 +1,17 @@
from django.conf.urls import include, patterns, url from django.conf.urls import include, patterns, url
from django.views.generic import RedirectView from django.views.generic import RedirectView
from openslides.core.views import IndexView
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^(?P<url>.*[^/])$', RedirectView.as_view(url='/%(url)s/')), url(r'^(?P<url>.*[^/])$', RedirectView.as_view(url='/%(url)s/')),
url(r'^rest/', include(router.urls)), url(r'^rest/', include(router.urls)),
url(r'^', include('openslides.core.urls')),
url(r'^agenda/', include('openslides.agenda.urls')), url(r'^agenda/', include('openslides.agenda.urls')),
url(r'^assignments/', include('openslides.assignments.urls')), url(r'^assignments/', include('openslides.assignments.urls')),
url(r'^motions/', include('openslides.motions.urls')), url(r'^motions/', include('openslides.motions.urls')),
url(r'^users/', include('openslides.users.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. Slide definitions for user model.
""" """
name = 'users/user' name = 'users/user'
scripts = 'users/user_slide.js'
def get_context(self): def get_context(self):
pk = self.config_entry.get('id') pk = self.config_entry.get('id')
try: if not User.objects.filter(pk=pk).exists():
user = User.objects.get(pk=pk)
except User.DoesNotExist:
raise ProjectorException(_('User does not exist.')) raise ProjectorException(_('User does not exist.'))
result = [{ return {'id': pk}
'collection': 'users/user',
'id': pk}]
for group in user.groups.all():
result.append({
'collection': 'users/group',
'id': group.pk})
return result
def get_requirements(self, config_entry): def get_requirements(self, config_entry):
self.config_entry = config_entry pk = config_entry.get('id')
try: if pk is not None:
context = self.get_context() yield ProjectorRequirement(
except ProjectorException: view_class=UserViewSet,
# User does not exist so just do nothing. view_action='retrive',
pass pk=pk)
else:
for item in context: for group in User.objects.get(pk=pk).groups.all():
yield ProjectorRequirement( yield ProjectorRequirement(
view_class=UserViewSet if item['collection'] == 'users/user' else GroupViewSet, view_class=GroupViewSet,
view_action='retrieve', view_action='retrieve',
pk=str(item['id'])) pk=str(group.pk))

View File

@ -1,5 +1,56 @@
angular.module('OpenSlidesApp.users', []) 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) { .config(function($stateProvider) {
$stateProvider $stateProvider
.state('users', { .state('users', {
@ -126,52 +177,6 @@ angular.module('OpenSlidesApp.users', [])
$rootScope.operator = operator; $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 * 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'); User.bindAll({}, $scope, 'users');
// setup table sorting // setup table sorting
@ -264,6 +269,10 @@ angular.module('OpenSlidesApp.users', [])
$scope.delete = function (user) { $scope.delete = function (user) {
User.destroy(user.id); User.destroy(user.id);
}; };
$scope.project = function(user) {
projectorActivate(User, user.id).error(function() {console.log('success')});
};
}) })
.controller('UserDetailCtrl', function($scope, User, user) { .controller('UserDetailCtrl', function($scope, User, user) {
@ -369,9 +378,25 @@ angular.module('OpenSlidesApp.users', [])
// DS.flush(); // 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 // this is code from angular.js. Find a way to call this function from this file
function getBlockNodes(nodes) { 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" class="optional">{{ user.last_login | date:'yyyy-MM-dd HH:mm:ss'}}
<td os-perms="users.can_manage core.can_manage_projector" class="nobr"> <td os-perms="users.can_manage core.can_manage_projector" class="nobr">
<!-- projector, TODO: add link to activate slidea--> <!-- 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 }}"> title="{{ 'Show' | translate }}">
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
</a> </a>

View File

@ -8,13 +8,12 @@ class ProjectorElement(object, metaclass=SignalConnectMetaClass):
Base class for an element on the projector. Base class for an element on the projector.
Every app which wants to add projector elements has to create classes Every app which wants to add projector elements has to create classes
subclassing from this base class with different names. The name and subclassing from this base class with different names. The name attribute
scripts attributes have to be set. The metaclass has to be set. The metaclass (SignalConnectMetaClass) does the rest of the
(SignalConnectMetaClass) does the rest of the magic. magic.
""" """
signal = Signal() signal = Signal()
name = None name = None
scripts = None
def __init__(self, **kwargs): def __init__(self, **kwargs):
""" """
@ -45,20 +44,8 @@ class ProjectorElement(object, metaclass=SignalConnectMetaClass):
assert self.config_entry.get('name') == self.name, ( assert self.config_entry.get('name') == self.name, (
'To get data of a projector element, the correct config entry has to be given.') 'To get data of a projector element, the correct config entry has to be given.')
return { return {
'scripts': self.get_scripts(),
'context': self.get_context()} '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): def get_context(self):
""" """
Returns the context of the projector element. 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(response.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(response.content.decode()), { self.assertEqual(json.loads(response.content.decode()), {
'id': 1,
'config': [{'name': 'core/customslide', 'id': customslide.id}], 'config': [{'name': 'core/customslide', 'id': customslide.id}],
'projector_elements': [ 'elements': [
{'name': 'core/customslide', {'name': 'core/customslide',
'scripts': 'core/customslide_slide.js', 'context': {'id': customslide.id}}]})
'context': [
{'collection': 'core/customslide',
'id': customslide.id}]}]})
def test_invalid_slide_on_default_projector(self): def test_invalid_slide_on_default_projector(self):
self.client.login(username='admin', password='admin') 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(response.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(response.content.decode()), { self.assertEqual(json.loads(response.content.decode()), {
'id': 1,
'config': [{'name': 'invalid_slide'}], 'config': [{'name': 'invalid_slide'}],
'projector_elements': [ 'elements': [
{'name': 'invalid_slide', {'name': 'invalid_slide',
'error': 'Projector element does not exist.'}]}) 'error': 'Projector element does not exist.'}]})