Merge pull request #1625 from boehlke/master
Add upload feature to mediafiles module.
This commit is contained in:
commit
4600fd9b02
@ -24,6 +24,7 @@
|
|||||||
"sockjs": "~0.3.4",
|
"sockjs": "~0.3.4",
|
||||||
"font-awesome-bower": "4.3.0",
|
"font-awesome-bower": "4.3.0",
|
||||||
"js-data": "~2.3.0",
|
"js-data": "~2.3.0",
|
||||||
"js-data-angular": "~3.0.0"
|
"js-data-angular": "~3.0.0",
|
||||||
|
"ng-file-upload": "~7.0.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
openslides/mediafiles/migrations/0002_auto_20150906_1246.py
Normal file
20
openslides/mediafiles/migrations/0002_auto_20150906_1246.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mediafiles', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='mediafile',
|
||||||
|
name='is_presentable',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mediafile',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(max_length=255, null=True, unique=True, blank=True, verbose_name='Title'),
|
||||||
|
),
|
||||||
|
]
|
20
openslides/mediafiles/migrations/0003_auto_20150917_1226.py
Normal file
20
openslides/mediafiles/migrations/0003_auto_20150917_1226.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('mediafiles', '0002_auto_20150906_1246'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='mediafile',
|
||||||
|
name='filetype',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mediafile',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(unique=True, verbose_name='Title', max_length=255, default='', blank=True),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
@ -1,5 +1,3 @@
|
|||||||
import mimetypes
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.translation import ugettext_lazy, ugettext_noop
|
from django.utils.translation import ugettext_lazy, ugettext_noop
|
||||||
@ -13,7 +11,6 @@ class Mediafile(RESTModelMixin, models.Model):
|
|||||||
Class for uploaded files which can be delivered under a certain url.
|
Class for uploaded files which can be delivered under a certain url.
|
||||||
"""
|
"""
|
||||||
slide_callback_name = 'mediafile'
|
slide_callback_name = 'mediafile'
|
||||||
PRESENTABLE_FILE_TYPES = ['application/pdf']
|
|
||||||
|
|
||||||
mediafile = models.FileField(upload_to='file', verbose_name=ugettext_lazy('File'))
|
mediafile = models.FileField(upload_to='file', verbose_name=ugettext_lazy('File'))
|
||||||
"""
|
"""
|
||||||
@ -21,7 +18,7 @@ class Mediafile(RESTModelMixin, models.Model):
|
|||||||
for more information.
|
for more information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
title = models.CharField(max_length=255, unique=True, verbose_name=ugettext_lazy('Title'))
|
title = models.CharField(max_length=255, unique=True, blank=True, verbose_name=ugettext_lazy('Title'))
|
||||||
"""A string representing the title of the file."""
|
"""A string representing the title of the file."""
|
||||||
|
|
||||||
uploader = models.ForeignKey(User, null=True, blank=True, verbose_name=ugettext_lazy('Uploaded by'))
|
uploader = models.ForeignKey(User, null=True, blank=True, verbose_name=ugettext_lazy('Uploaded by'))
|
||||||
@ -30,15 +27,6 @@ class Mediafile(RESTModelMixin, models.Model):
|
|||||||
timestamp = models.DateTimeField(auto_now_add=True)
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
"""A DateTimeField to save the upload date and time."""
|
"""A DateTimeField to save the upload date and time."""
|
||||||
|
|
||||||
filetype = models.CharField(max_length=255, editable=False)
|
|
||||||
"""A string used to show the type of the file."""
|
|
||||||
|
|
||||||
is_presentable = models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
verbose_name=ugettext_lazy("Is Presentable"),
|
|
||||||
help_text=ugettext_lazy("If checked, this file can be presented on the projector. "
|
|
||||||
"Currently, this is only possible for PDFs."))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""
|
"""
|
||||||
Meta class for the mediafile model.
|
Meta class for the mediafile model.
|
||||||
@ -55,16 +43,6 @@ class Mediafile(RESTModelMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Method to read filetype and then save to the database.
|
|
||||||
"""
|
|
||||||
if self.mediafile:
|
|
||||||
self.filetype = mimetypes.guess_type(self.mediafile.path)[0] or ugettext_noop('unknown')
|
|
||||||
else:
|
|
||||||
self.filetype = ugettext_noop('unknown')
|
|
||||||
return super(Mediafile, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_filesize(self):
|
def get_filesize(self):
|
||||||
"""
|
"""
|
||||||
Transforms bytes to kilobytes or megabytes. Returns the size as string.
|
Transforms bytes to kilobytes or megabytes. Returns the size as string.
|
||||||
|
@ -16,11 +16,9 @@ class MediafileSlide(ProjectorElement):
|
|||||||
def get_context(self):
|
def get_context(self):
|
||||||
pk = self.config_entry.get('id')
|
pk = self.config_entry.get('id')
|
||||||
try:
|
try:
|
||||||
mediafile = Mediafile.objects.get(pk=pk)
|
Mediafile.objects.get(pk=pk)
|
||||||
except Mediafile.DoesNotExist:
|
except Mediafile.DoesNotExist:
|
||||||
raise ProjectorException(_('File does not exist.'))
|
raise ProjectorException(_('File does not exist.'))
|
||||||
if not (mediafile.is_presentable and mediafile.filetype == 'application/pdf'):
|
|
||||||
raise ProjectorException(_('File is not presentable.'))
|
|
||||||
return {'id': pk}
|
return {'id': pk}
|
||||||
|
|
||||||
def get_requirements(self, config_entry):
|
def get_requirements(self, config_entry):
|
||||||
|
@ -1,14 +1,41 @@
|
|||||||
from openslides.utils.rest_api import ModelSerializer, SerializerMethodField
|
import mimetypes
|
||||||
|
|
||||||
|
from django.db import models as dbmodels
|
||||||
|
|
||||||
|
from ..utils.rest_api import FileField, ModelSerializer, SerializerMethodField
|
||||||
from .models import Mediafile
|
from .models import Mediafile
|
||||||
|
|
||||||
|
|
||||||
|
class AngularCompatibleFileField(FileField):
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if data == '':
|
||||||
|
return None
|
||||||
|
return super(AngularCompatibleFileField, self).to_internal_value(data)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
'name': value.name,
|
||||||
|
'type': mimetypes.guess_type(value.path)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class MediafileSerializer(ModelSerializer):
|
class MediafileSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for mediafile.models.Mediafile objects.
|
Serializer for mediafile.models.Mediafile objects.
|
||||||
"""
|
"""
|
||||||
filesize = SerializerMethodField()
|
filesize = SerializerMethodField()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
This constructor overwrites the FileField field serializer to return the file meta data in a way that the
|
||||||
|
angualarjs upload module likes
|
||||||
|
"""
|
||||||
|
super(MediafileSerializer, self).__init__(*args, **kwargs)
|
||||||
|
self.serializer_field_mapping[dbmodels.FileField] = AngularCompatibleFileField
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Mediafile
|
model = Mediafile
|
||||||
fields = (
|
fields = (
|
||||||
@ -18,8 +45,7 @@ class MediafileSerializer(ModelSerializer):
|
|||||||
'uploader',
|
'uploader',
|
||||||
'filesize',
|
'filesize',
|
||||||
'filetype',
|
'filetype',
|
||||||
'timestamp',
|
'timestamp',)
|
||||||
'is_presentable',)
|
|
||||||
|
|
||||||
def get_filesize(self, mediafile):
|
def get_filesize(self, mediafile):
|
||||||
return mediafile.get_filesize()
|
return mediafile.get_filesize()
|
||||||
|
@ -5,13 +5,47 @@ angular.module('OpenSlidesApp.mediafiles', [])
|
|||||||
.factory('Mediafile', ['DS', function(DS) {
|
.factory('Mediafile', ['DS', function(DS) {
|
||||||
return DS.defineResource({
|
return DS.defineResource({
|
||||||
name: 'mediafiles/mediafile',
|
name: 'mediafiles/mediafile',
|
||||||
|
computed: {
|
||||||
|
is_presentable: ['filetype', function (filetype) {
|
||||||
|
var PRESENTABLE_FILE_TYPES = ['application/pdf']
|
||||||
|
return _.contains(PRESENTABLE_FILE_TYPES, filetype);
|
||||||
|
}],
|
||||||
|
filename: [function () {
|
||||||
|
var filename = this.mediafile.name;
|
||||||
|
return /\/(.+?)$/.exec(filename)[1];
|
||||||
|
}],
|
||||||
|
title_or_filename: ['title', 'mediafile', function (title) {
|
||||||
|
return title || this.filename;
|
||||||
|
}]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}])
|
}])
|
||||||
|
|
||||||
.run(['Mediafile', function(Mediafile) {}]);
|
.run(['Mediafile', function(Mediafile) {}]);
|
||||||
|
|
||||||
|
function uploadFile($timeout, $scope, $state, Upload, mediafile) {
|
||||||
|
return function(file) {
|
||||||
|
file.upload = Upload.upload({
|
||||||
|
url: '/rest/mediafiles/mediafile/' + (mediafile ? mediafile.id : ''),
|
||||||
|
method: mediafile ? 'PUT' : 'POST',
|
||||||
|
fields: {title: file.title},
|
||||||
|
file: file.mediafile,
|
||||||
|
fileFormDataName: 'mediafile'
|
||||||
|
});
|
||||||
|
|
||||||
angular.module('OpenSlidesApp.mediafiles.site', ['OpenSlidesApp.mediafiles'])
|
file.upload.then(function (response) {
|
||||||
|
$timeout(function () {
|
||||||
|
file.result = response.data;
|
||||||
|
$state.go('mediafiles.mediafile.list');
|
||||||
|
});
|
||||||
|
}, function (response) {
|
||||||
|
if (response.status > 0)
|
||||||
|
$scope.errorMsg = response.status + ': ' + response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.mediafiles'])
|
||||||
|
|
||||||
.config([
|
.config([
|
||||||
'mainMenuProvider',
|
'mainMenuProvider',
|
||||||
@ -45,6 +79,18 @@ angular.module('OpenSlidesApp.mediafiles.site', ['OpenSlidesApp.mediafiles'])
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.state('mediafiles.mediafile.create', {})
|
.state('mediafiles.mediafile.create', {})
|
||||||
|
.state('mediafiles.mediafile.detail', {
|
||||||
|
url: '/{id:int}',
|
||||||
|
abstract: true,
|
||||||
|
resolve: {
|
||||||
|
mediafile: function(Mediafile, $stateParams) {
|
||||||
|
var id = $stateParams.id;
|
||||||
|
var file = Mediafile.find(id);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: "<ui-view/>",
|
||||||
|
})
|
||||||
.state('mediafiles.mediafile.detail.update', {
|
.state('mediafiles.mediafile.detail.update', {
|
||||||
views: {
|
views: {
|
||||||
'@mediafiles.mediafile': {}
|
'@mediafiles.mediafile': {}
|
||||||
@ -52,7 +98,7 @@ angular.module('OpenSlidesApp.mediafiles.site', ['OpenSlidesApp.mediafiles'])
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
.controller('MediafileListCtrl', function($scope, $http, Mediafile) {
|
.controller('MediafileListCtrl', function($scope, $http, $timeout, Upload, Mediafile) {
|
||||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||||
|
|
||||||
// setup table sorting
|
// setup table sorting
|
||||||
@ -78,26 +124,14 @@ angular.module('OpenSlidesApp.mediafiles.site', ['OpenSlidesApp.mediafiles'])
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
.controller('MediafileCreateCtrl', function($scope, $state, Mediafile) {
|
.controller('MediafileCreateCtrl', function($scope, $state, $timeout, Upload) {
|
||||||
$scope.mediafile = {};
|
$scope.mediafile = {};
|
||||||
$scope.save = function(mediafile) {
|
$scope.save = uploadFile($timeout, $scope, $state, Upload);
|
||||||
Mediafile.create(mediafile).then(
|
|
||||||
function(success) {
|
|
||||||
$state.go('mediafiles.mediafile.list');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
|
|
||||||
.controller('MediafileUpdateCtrl', function($scope, $state, Mediafile, mediafile) {
|
.controller('MediafileUpdateCtrl', function($scope, $state, $timeout, Upload, Mediafile, mediafile) {
|
||||||
$scope.mediafile = mediafile;
|
$scope.mediafile = mediafile;
|
||||||
$scope.save = function (mediafile) {
|
$scope.save = uploadFile($timeout, $scope, $state, Upload, mediafile);
|
||||||
Mediafile.save(mediafile).then(
|
|
||||||
function(success) {
|
|
||||||
$state.go('mediafiles.mediafile.list');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,22 +9,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form name="mediafileForm">
|
<form name="mediafileForm">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div ng-if="mediafile.id">
|
||||||
|
<span translate>Current value: </span>{{ mediafile.filename }}
|
||||||
|
</div>
|
||||||
|
<input type="file" ngf-select ng-model="mediafile.mediafile" required/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inputTitle" translate>Title</label>
|
<label for="inputTitle" translate>Title</label>
|
||||||
<input type="text" ng-model="mediafile.title" class="form-control" name="inputTitle">
|
<input type="text" ng-model="mediafile.title" class="form-control" name="inputTitle">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<span ng-show="mediafile.mediafile.result">Upload Successful</span>
|
||||||
<input type="file" ng-model="mediafile.mediafile"/>
|
<span class="err" ng-show="errorMsg">{{ errorMsg }}</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<i ng-show="mediafile.file.$error.required">*required</i><br>
|
||||||
<label class="checkbox-inline">
|
<i ng-show="mediafile.file.$error.maxSize">File too large
|
||||||
<input type="checkbox" ng-model="mediafile.is_presentable" ng-checked="mediafile.is_presentable" name="checkboxPresentable">
|
{{ picFile.size / 1000000|number:1}}MB: max {{ mediafile.mediafile.$errorParam}}</i>
|
||||||
<translate>Is presentable</translate>
|
|
||||||
<p class="help-block" translate>If checked, this file can be presented on the projector. Currently, this is only possible for PDFs.</p>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" ng-click="save(mediafile)" class="btn btn-primary" translate>
|
<button type="submit" ng-click="save(mediafile)" class="btn btn-primary" translate>
|
||||||
Save
|
Save
|
||||||
|
@ -11,16 +11,16 @@
|
|||||||
<div class="col-sm-8"></div>
|
<div class="col-sm-8"></div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<input type="text" os-focus-me ng-model="filter.search" class="form-control"
|
<input type="text" os-focus-me ng-model="filter.search" class="form-control"
|
||||||
placeholder="{{ 'Filter' | translate}}">
|
placeholder="{{ 'Filter' | translate }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table table-striped table-bordered table-hover">
|
<table class="table table-striped table-bordered table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th ng-click="toggleSort('title')" class="sortable">
|
<th ng-click="toggleSort('title_or_filename')" class="sortable">
|
||||||
<translate>Title</translate>
|
<translate>Title</translate>
|
||||||
<i class="pull-right fa" ng-show="sortColumn === 'title' && header.sortable != false"
|
<i class="pull-right fa" ng-show="sortColumn === 'title_or_filename' && header.sortable != false"
|
||||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||||
</i>
|
</i>
|
||||||
<th ng-click="toggleSort('filetype')" class="sortable">
|
<th ng-click="toggleSort('filetype')" class="sortable">
|
||||||
@ -45,24 +45,27 @@
|
|||||||
</i>
|
</i>
|
||||||
<th os-perms="mediafiles.can_manage core.can_manage_projector" class="minimum">
|
<th os-perms="mediafiles.can_manage core.can_manage_projector" class="minimum">
|
||||||
<translate>Actions</translate>
|
<translate>Actions</translate>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-repeat="mediafile in mediafiles | filter: filter.search |
|
<tr ng-repeat="mediafile in mediafiles | filter: filter.search |
|
||||||
orderBy: sortColumn:reverse">
|
orderBy: sortColumn:reverse">
|
||||||
<td><a ng-href="{{ mediafile.mediafile }}" target="_self">{{ mediafile.title }}</a>
|
<td><a ng-href="{{ mediafile.mediafile }}" target="_self">{{ mediafile.title_or_filename }}</a>
|
||||||
<td class="optional">{{ mediafile.filetype }}
|
<td class="optional">{{ mediafile.filetype }}
|
||||||
<td>{{ mediafile.filesize }}
|
<td>{{ mediafile.filesize }}
|
||||||
<td>{{ mediafile.timestamp }}
|
<td>{{ mediafile.timestamp }}
|
||||||
<td>{{ mediafile.uploader }}
|
<td>{{ mediafile.uploader }}
|
||||||
<td os-perms="mediafiles.can_manage core.can_manage_projector" class="nobr">
|
<td os-perms="mediafiles.can_manage core.can_manage_projector" class="nobr">
|
||||||
<!-- projector, TODO: add link to activate slide -->
|
<!-- projector, TODO: add link to activate slide -->
|
||||||
<a href="#TODO" ng-if="mediafile.is_presentable" os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
|
<a href="#TODO" ng-if="mediafile.is_presentable" os-perms-lite="core.can_manage_projector"
|
||||||
|
class="btn btn-default btn-sm"
|
||||||
title="{{ 'Show' | translate }}">
|
title="{{ 'Show' | translate }}">
|
||||||
<i class="fa fa-video-camera"></i>
|
<i class="fa fa-video-camera"></i>
|
||||||
</a>
|
</a>
|
||||||
<!-- edit -->
|
<!-- edit -->
|
||||||
<a ui-sref="mediafiles.mediafile.detail.update({id: mediafile.id })" os-perms="mediafiles.can_manage"
|
<a ui-sref="mediafiles.mediafile.detail.update({id: mediafile.id })" os-perms="mediafiles.can_manage"
|
||||||
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>
|
||||||
<!-- delete -->
|
<!-- delete -->
|
||||||
@ -70,4 +73,6 @@
|
|||||||
title="{{ 'Delete' | translate }}">
|
title="{{ 'Delete' | translate }}">
|
||||||
<i class="fa fa-trash-o"></i>
|
<i class="fa fa-trash-o"></i>
|
||||||
</a>
|
</a>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from openslides.utils.rest_api import ModelViewSet
|
from ..utils.rest_api import ModelViewSet
|
||||||
|
|
||||||
from .models import Mediafile
|
from .models import Mediafile
|
||||||
from .serializers import MediafileSerializer
|
from .serializers import MediafileSerializer
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ from rest_framework.serializers import ( # noqa
|
|||||||
CharField,
|
CharField,
|
||||||
DictField,
|
DictField,
|
||||||
Field,
|
Field,
|
||||||
|
FileField,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
ListField,
|
ListField,
|
||||||
ListSerializer,
|
ListSerializer,
|
||||||
|
Loading…
Reference in New Issue
Block a user