Updated js-data to 2.0

Added js-data relation for the motion and agenda app

Added improved load function
This commit is contained in:
Oskar Hahn 2015-07-06 09:19:42 +02:00
parent ec50b6e67f
commit 6674ea85b7
18 changed files with 230 additions and 132 deletions

View File

@ -18,11 +18,11 @@
"angular-gettext": "~2.0.2",
"angular-sanitize": "~1.3.15",
"angular-xeditable": "~0.1.9",
"js-data": "~1.8.0",
"js-data-angular": "~2.1.0",
"ng-fab-form": "~1.2.7",
"ngBootbox": "~0.0.5",
"sockjs": "~0.3.4",
"font-awesome-bower": "4.3.0"
"font-awesome-bower": "4.3.0",
"js-data": "~2.3.0",
"js-data-angular": "~3.0.0"
}
}

View File

@ -0,0 +1,15 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda', '0002_auto_20150630_0144'),
]
operations = [
migrations.AlterField(
model_name='speaker',
name='item',
field=models.ForeignKey(related_name='speakers', to='agenda.Item'),
),
]

View File

@ -235,11 +235,10 @@ class Item(RESTModelMixin, models.Model):
dictionary contains a prefix, the speaker and its type. Types
are old_speaker, actual_speaker and coming_speaker.
"""
speaker_query = Speaker.objects.filter(item=self) # TODO: Why not self.speaker_set?
list_of_speakers = []
# Parse old speakers
old_speakers = speaker_query.exclude(begin_time=None).exclude(end_time=None).order_by('end_time')
old_speakers = self.speakers.exclude(begin_time=None).exclude(end_time=None).order_by('end_time')
if old_speakers_count is None:
old_speakers_count = old_speakers.count()
last_old_speakers_count = max(0, old_speakers.count() - old_speakers_count)
@ -260,7 +259,7 @@ class Item(RESTModelMixin, models.Model):
# Parse actual speaker
try:
actual_speaker = speaker_query.filter(end_time=None).exclude(begin_time=None).get()
actual_speaker = self.speakers.filter(end_time=None).exclude(begin_time=None).get()
except Speaker.DoesNotExist:
pass
else:
@ -272,7 +271,7 @@ class Item(RESTModelMixin, models.Model):
'last_in_group': True})
# Parse coming speakers
coming_speakers = speaker_query.filter(begin_time=None).order_by('weight')
coming_speakers = self.speakers.filter(begin_time=None).order_by('weight')
if coming_speakers_count is None:
coming_speakers_count = coming_speakers.count()
coming_speakers = coming_speakers[:max(0, coming_speakers_count)]
@ -296,7 +295,7 @@ class Item(RESTModelMixin, models.Model):
Returns the speaker object of the user who is next.
"""
try:
return self.speaker_set.filter(begin_time=None).order_by('weight')[0]
return self.speakers.filter(begin_time=None).order_by('weight')[0]
except IndexError:
# The list of speakers is empty.
return None
@ -366,7 +365,7 @@ class Speaker(RESTModelMixin, models.Model):
ForeinKey to the user who speaks.
"""
item = models.ForeignKey(Item)
item = models.ForeignKey(Item, related_name='speakers')
"""
ForeinKey to the AgendaItem to which the user want to speak.
"""

View File

@ -21,7 +21,9 @@ class SpeakerSerializer(ModelSerializer):
'user',
'begin_time',
'end_time',
'weight')
'weight',
'item', # js-data needs the item-id in the nested object to define relations.
)
class RelatedItemRelatedField(RelatedField):
@ -47,7 +49,7 @@ class ItemSerializer(ModelSerializer):
get_title_supplement = CharField(read_only=True)
content_object = RelatedItemRelatedField(read_only=True)
item_no = CharField(read_only=True)
speaker_set = SpeakerSerializer(many=True, read_only=True)
speakers = SpeakerSerializer(many=True, read_only=True)
class Meta:
model = Item
@ -63,7 +65,7 @@ class ItemSerializer(ModelSerializer):
'closed',
'type',
'duration',
'speaker_set',
'speakers',
'speaker_list_closed',
'content_object',
'tags',)

View File

@ -1,21 +1,48 @@
angular.module('OpenSlidesApp.agenda', [])
"use strict";
.factory('Agenda', function(DS, jsDataModel) {
angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
.factory('Speaker', ['DS', function(DS) {
return DS.defineResource({
name: 'agenda/speaker',
relations: {
belongsTo: {
'users/user': {
localField: 'user',
localKey: 'user_id',
}
}
}
});
}])
.factory('Agenda', ['DS', 'Speaker', 'jsDataModel', function(DS, Speaker, jsDataModel) {
var name = 'agenda/item'
return DS.defineResource({
name: name,
endpoint: '/rest/agenda/item/',
useClass: jsDataModel,
methods: {
getResourceName: function () {
return name;
}
},
relations: {
hasMany: {
'core/tag': {
localField: 'tags',
localKeys: 'tags_id',
},
'agenda/speaker': {
localField: 'speakers',
foreignKey: 'item_id',
}
}
}
});
})
}])
// Make sure that the Agenda resource is loaded.
.run(function(Agenda) {});
.run(['Agenda', function(Agenda) {}]);
angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
@ -130,7 +157,7 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
$scope.isAgendaProjected = function () {
// Returns true if there is a projector element with the same
// name and agenda is active.
var projector = Projector.get(id=1);
var projector = Projector.get(1);
if (typeof projector === 'undefined') return false;
var self = this;
return _.findIndex(projector.elements, function(element) {

View File

@ -55,8 +55,8 @@
* check permissions
-->
<ol>
<li ng-repeat="speaker in item.speaker_set">
{{ (users | filter: {id: speaker.user})[0].get_full_name() }}
<li ng-repeat="speaker in item.speakers">
{{ speaker.user.get_full_name() }}
<button os-perms="agenda.can_manage" ng-click="removeSpeaker(speaker.id)"
class="btn btn-default btn-xs">
<i class="fa fa-times"></i>
@ -65,7 +65,7 @@
</ol>
<div os-perms="agenda.can_manage" class="form-group col-sm-6">
<alert ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">{{alert.msg}}</alert>
<alert ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">{{ alert.msg }}</alert>
<div class="input-group">
<ui-select ng-model="speaker.selected" ng-change="addSpeaker(speaker.selected.id)">
<ui-select-match placeholder="{{ 'Select or search a participant...' | translate }}">

View File

@ -1,10 +1,11 @@
"use strict";
angular.module('OpenSlidesApp.assignments', [])
.factory('Assignment', function(DS, jsDataModel) {
.factory('Assignment', ['DS', 'jsDataModel', function(DS, jsDataModel) {
var name = 'assignments/assignment'
return DS.defineResource({
name: name,
endpoint: '/rest/assignments/assignment/',
useClass: jsDataModel,
methods: {
getResourceName: function () {
@ -12,9 +13,9 @@ angular.module('OpenSlidesApp.assignments', [])
}
}
});
})
}])
.run(function(Assignment) {});
.run(['Assignment', function(Assignment) {}]);
angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])

View File

@ -1,3 +1,5 @@
"use strict";
// The core module used for the OpenSlides site and the projector
angular.module('OpenSlidesApp.core', [
'angular-loading-bar',
@ -8,11 +10,12 @@ angular.module('OpenSlidesApp.core', [
'ui.tree',
])
.config(function(DSProvider) {
.config(['DSProvider', 'DSHttpAdapterProvider', function(DSProvider, DSHttpAdapterProvider) {
// 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.basePath = '/rest';
DSProvider.defaults.afterReap = function(model, items) {
if (items.length > 5) {
model.findAll({}, {bypassCache: true});
@ -22,44 +25,8 @@ 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.
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, Projector) {
// 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 ''
}
}
});
// Loads all projector data
Projector.findAll();
})
DSHttpAdapterProvider.defaults.forceTrailingSlash = true;
}])
.factory('autoupdate', function() {
var url = location.origin + "/sockjs";
@ -89,7 +56,43 @@ angular.module('OpenSlidesApp.core', [
return Autoupdate;
})
.factory('jsDataModel', function($http, Projector) {
.run(['DS', 'autoupdate', 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(['$rootScope', 'Config', 'Projector', function($rootScope, Config, Projector) {
// Puts the config object into each scope.
Config.findAll().then(function() {
$rootScope.config = function(key) {
try {
return Config.get(key).value;
}
catch(err) {
console.log("Unkown config key: " + key);
return ''
}
}
});
// Loads all projector data
Projector.findAll();
}])
.factory('jsDataModel', ['$http', 'Projector', function($http, Projector) {
var BaseModel = function() {};
BaseModel.prototype.project = function() {
return $http.post(
@ -100,7 +103,7 @@ angular.module('OpenSlidesApp.core', [
BaseModel.prototype.isProjected = function() {
// Returns true if there is a projector element with the same
// name and the same id.
var projector = Projector.get(id=1);
var projector = Projector.get(1);
if (typeof projector === 'undefined') return false;
var self = this;
return _.findIndex(projector.elements, function(element) {
@ -111,13 +114,12 @@ angular.module('OpenSlidesApp.core', [
}) > -1;
}
return BaseModel;
})
}])
.factory('Customslide', function(DS, jsDataModel) {
.factory('Customslide', ['DS', 'jsDataModel', function(DS, jsDataModel) {
var name = 'core/customslide'
return DS.defineResource({
name: name,
endpoint: '/rest/core/customslide/',
useClass: jsDataModel,
methods: {
getResourceName: function () {
@ -125,31 +127,29 @@ angular.module('OpenSlidesApp.core', [
},
},
});
})
}])
.factory('Tag', function(DS) {
.factory('Tag', ['DS', function(DS) {
return DS.defineResource({
name: 'core/tag',
endpoint: '/rest/core/tag/'
});
})
}])
.factory('Config', function(DS) {
.factory('Config', ['DS', function(DS) {
return DS.defineResource({
name: 'core/config',
idAttribute: 'key',
endpoint: '/rest/core/config/'
});
})
}])
.factory('Projector', function(DS) {
.factory('Projector', ['DS', function(DS) {
return DS.defineResource({
name: 'core/projector',
endpoint: '/rest/core/projector/',
});
})
}])
.run(function(Projector, Config, Tag, Customslide){});
// Make sure that the DS factories are loaded by making them a dependency
.run(['Projector', 'Config', 'Tag', 'Customslide', function(Projector, Config, Tag, Customslide){}]);
// The core module for the OpenSlides site

View File

@ -94,12 +94,33 @@ class AppsJsView(utils_views.View):
static=settings.STATIC_URL,
path=path)
for path in app_js_files]
# Use javascript loadScript function from
# http://balpha.de/2011/10/jquery-script-insertion-and-its-consequences-for-debugging/
return HttpResponse(
"""
var loadScript = function (path) {
var result = $.Deferred(),
script = document.createElement("script");
script.async = "async";
script.type = "text/javascript";
script.src = path;
script.onload = script.onreadystatechange = function(_, isAbort) {
if (!script.readyState || /loaded|complete/.test(script.readyState)) {
if (isAbort)
result.reject();
else
result.resolve();
}
};
script.onerror = function () { result.reject(); };
$("head")[0].appendChild(script);
return result.promise();
};
""" +
"angular.module('OpenSlidesApp.{app}', {angular_modules});"
"var deferres = [];"
"{js_files}.forEach(function(js_file)deferres.push($.getScript(js_file)));"
"$.when.apply(this, deferres).done(function()angular.bootstrap(document,['OpenSlidesApp.{app}']));"
"{js_files}.forEach(function(js_file)deferres.push(loadScript(js_file)));"
"$.when.apply(this,deferres).done(function()angular.bootstrap(document,['OpenSlidesApp.{app}']));"
.format(
app=kwargs.get('openslides_app'),
angular_modules=angular_modules,

View File

@ -1,13 +1,14 @@
"use strict";
angular.module('OpenSlidesApp.mediafiles', [])
.factory('Mediafile', function(DS) {
.factory('Mediafile', ['DS', function(DS) {
return DS.defineResource({
name: 'mediafiles/mediafile',
endpoint: '/rest/mediafiles/mediafile/'
});
})
}])
.run(function(Mediafile) {});
.run(['Mediafile', function(Mediafile) {}]);
angular.module('OpenSlidesApp.mediafiles.site', ['OpenSlidesApp.mediafiles'])

View File

@ -218,13 +218,13 @@ class MotionSerializer(ModelSerializer):
motion.category = validated_data.get('category')
motion.reset_state(validated_data.get('workflow', int(config['motions_workflow'])))
motion.save()
if validated_data['submitters']:
if validated_data.get('submitters'):
motion.submitters.add(*validated_data['submitters'])
else:
motion.submitters.add(validated_data['request_user'])
motion.supporters.add(*validated_data['supporters'])
motion.attachments.add(*validated_data['attachments'])
motion.tags.add(*validated_data['tags'])
motion.supporters.add(*validated_data.get('supporters', []))
motion.attachments.add(*validated_data.get('attachments', []))
motion.tags.add(*validated_data.get('tags', []))
return motion
@transaction.atomic

View File

@ -1,10 +1,11 @@
"use strict";
angular.module('OpenSlidesApp.motions', [])
.factory('Motion', function(DS, jsDataModel) {
.factory('Motion', ['DS', 'jsDataModel', function(DS, jsDataModel) {
var name = 'motions/motion'
return DS.defineResource({
name: name,
endpoint: '/rest/motions/motion/',
useClass: jsDataModel,
methods: {
getResourceName: function () {
@ -12,6 +13,7 @@ angular.module('OpenSlidesApp.motions', [])
},
getVersion: function(versionId) {
versionId = versionId || this.active_version;
var index;
if (versionId == -1) {
index = this.versions.length - 1;
} else {
@ -30,23 +32,47 @@ angular.module('OpenSlidesApp.motions', [])
getReason: function(versionId) {
return this.getVersion(versionId).reason;
}
},
relations: {
belongsTo: {
'motions/category': {
localField: 'category',
localKey: 'category_id',
},
},
hasMany: {
'core/tag': {
localField: 'tags',
localKeys: 'tags_id',
},
'users/user': [
{
localField: 'submitters',
localKeys: 'submitters_id',
},
'supporters': {
localField: 'supporters',
localKeys: 'supporters_id',
}
],
}
}
});
})
.factory('Category', function(DS) {
}])
.factory('Category', ['DS', function(DS) {
return DS.defineResource({
name: 'motions/category',
endpoint: '/rest/motions/category/'
});
})
.factory('Workflow', function(DS) {
}])
.factory('Workflow', ['DS', function(DS) {
return DS.defineResource({
name: 'motions/workflow',
endpoint: '/rest/motions/workflow/'
});
})
}])
.run(function(Motion, Category, Workflow) {});
.run(['Motion', 'Category', 'Workflow', function(Motion, Category, Workflow) {}]);
angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
@ -107,7 +133,10 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
},
users: function(User) {
return User.findAll();
}
},
tags: function(Tag) {
return Tag.findAll();
},
}
})
.state('motions.motion.detail.update', {
@ -211,11 +240,6 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
$scope.motion = {};
$scope.save = function (motion) {
motion.tags = []; // TODO: REST API should do it! (Bug in Django REST framework)
motion.attachments = []; // TODO: REST API should do it! (Bug in Django REST framework)
if (!motion.supporters) {
motion.supporters = []; // TODO: REST API should do it! (Bug in Django REST framework)
}
Motion.create(motion).then(
function(success) {
$state.go('motions.motion.list');

View File

@ -2,9 +2,10 @@
{{ motion.getTitle() }}
<small>
<translate>Motion</translate> {{ motion.identifier }}
<span ng-if="motion.versions.length > 1" >| Version {{ (motion.versions | filter: {id: motion.active_version})[0].version_number }}</span>
<span ng-if="motion.versions.length > 1" >| Version {{ motion.active_version }}</span>
</small>
</h1>
{{ motion.tags }}
<div id="submenu">
<a ui-sref="motions.motion.list" class="btn btn-sm btn-default">
@ -16,7 +17,7 @@
<translate>PDF</translate>
</a>
<!-- 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': motion.isProjected() }"
ng-click="motion.project()"
title="{{ 'Project motion' | translate }}">
@ -43,11 +44,11 @@
<div class="well">
<h3 translate>Submitters</h3>
<div ng-repeat="submitter in motion.submitters">
{{ (users | filter: {id: submitter})[0].get_full_name() }}<br>
{{ submitter.get_full_name() }}<br>
</div>
<h3 translate>Category</h3>
{{ (categories | filter: {id: motion.category})[0].name }}</a>
{{ motion.category.name }}</a>
<h3 translate>Voting result</h3>
-

View File

@ -22,7 +22,7 @@
<div class="form-group">
<label for="selectSubmitter" translate>Submitter</label>
<select multiple size="3" ng-options="user.id as user.get_short_name() for user in users"
ng-model="motion.submitters" class="form-control" name="selectSubmitter" required>
ng-model="motion.submitters_id" class="form-control" name="selectSubmitter" required>
</select>
<!-- TODO: use modern ui-select component with more information per user
<ui-select multipe ng-model="motion.submitter" theme="bootstrap" name="selectSubmitter">
@ -50,25 +50,25 @@
<div class="form-group">
<label for="selectCategory" translate>Category</label>
<select ng-options="category.id as category.name for category in categories"
ng-model="motion.category" class="form-control" name="selectCategory">
ng-model="motion.category_id" class="form-control" name="selectCategory">
</select>
</div>
<div class="form-group">
<label for="selectTags" translate>Tags</label>
<select ng-options="tag.id as tag.name for tag in tags"
ng-model="motion.tags" class="form-control" name="selectTags">
<select multiple ng-options="tag.id as tag.name for tag in tags"
ng-model="motion.tags_id" class="form-control" name="selectTags">
</select>
</div>
<div class="form-group">
<label for="selectAttachments" translate>Attachments</label>
<select ng-options="file.id as file.title for file in mediafiles"
ng-model="motion.attachments" class="form-control" name="selectAttachments">
ng-model="motion.attachments_id" class="form-control" name="selectAttachments">
</select>
</div>
<!-- TODO: show only if supporters is enabled -->
<div class="form-group">
<label for="selectSupporter" translate>Supporters</label>
<ui-select ng-model="motion.supporter" theme="bootstrap" name="selectSupporter">
<ui-select multiple ng-model="motion.supporters_id" theme="bootstrap" name="selectSupporter">
<ui-select-match placeholder="{{ 'Select or search a participant...' | translate }}">
{{ $select.selected.get_short_name() }}
</ui-select-match>

View File

@ -66,18 +66,17 @@
<td> <!--TOOD: add agenda item reference -->
<td><a ui-sref="motions.motion.detail({id: motion.id})">{{ motion.identifier }}</a>
<td><a ui-sref="motions.motion.detail({id: motion.id})">
<!-- TODO: make it easier to get active version -->
{{ (motion.versions | filter: {id: motion.active_version})[0].title }}
{{ motion.getTitle() }}
</a>
<td class="optional">
<div ng-repeat="submitter in motion.submitters">
{{ (users | filter: {id: submitter})[0].get_full_name() }}<br>
{{ submitter.get_full_name() }}<br>
</div>
<td class="optional">
{{ (categories | filter: {id: motion.category})[0].name }}
{{ motion.category.name }}
<td os-perms="motions.can_manage core.can_manage_projector" class="nobr">
<!-- 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': motion.isProjected() }"
ng-click="motion.project()"
title="{{ 'Project motion' | translate }}">

View File

@ -1,10 +1,11 @@
"use strict";
angular.module('OpenSlidesApp.users', [])
.factory('User', function(DS, Group, jsDataModel) {
.factory('User', ['DS', 'Group', 'jsDataModel', function(DS, Group, jsDataModel) {
var name = 'users/user'
return DS.defineResource({
name: name,
endpoint: '/rest/users/user/',
useClass: jsDataModel,
methods: {
getResourceName: function () {
@ -52,7 +53,7 @@ angular.module('OpenSlidesApp.users', [])
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);
var group = Group.get(groupId);
if (group) {
_.forEach(group.permissions, function(perm) {
allPerms.push(perm);
@ -63,16 +64,15 @@ angular.module('OpenSlidesApp.users', [])
},
},
});
})
}])
.factory('Group', function(DS) {
.factory('Group', ['DS', function(DS) {
return DS.defineResource({
name: 'users/group',
endpoint: '/rest/users/group/'
});
})
}])
.run(function(User, Group) {});
.run(['User', 'Group', function(User, Group) {}]);
angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])

View File

@ -148,8 +148,9 @@ class Speak(TestCase):
def test_begin_speach_next_speaker(self):
speaker = Speaker.objects.add(self.user, self.item)
Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
self.assertTrue(Speaker.objects.get(pk=speaker.pk).begin_time is None)
response = self.client.put(reverse('item-speak', args=[self.item.pk]))
self.assertEqual(response.status_code, 200)
self.assertFalse(Speaker.objects.get(pk=speaker.pk).begin_time is None)

View File

@ -17,12 +17,19 @@ class CreateMotion(TestCase):
self.client.login(username='admin', password='admin')
def test_simple(self):
"""
Tests that a motion is created with a specific title and text.
The created motion should have an identifier and the admin user should
be the submitter.
"""
response = self.client.post(
reverse('motion-list'),
{'title': 'test_title_OoCoo3MeiT9li5Iengu9',
'text': 'test_text_thuoz0iecheiheereiCi'})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
motion = Motion.objects.get()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(motion.title, 'test_title_OoCoo3MeiT9li5Iengu9')
self.assertEqual(motion.identifier, '1')
self.assertTrue(motion.submitters.exists())