From cfdaaa446406141e8686fdbbb7af805043efd1eb Mon Sep 17 00:00:00 2001 From: j <0x006A@0x2620.org> Date: Mon, 27 May 2013 20:06:56 +0000 Subject: [PATCH] minimal edits with a sortable list interface and 'add selected item/in/out support' --- pandora/config.0xdb.jsonc | 5 + pandora/config.indiancinema.jsonc | 5 + pandora/config.padma.jsonc | 6 + pandora/config.pandora.jsonc | 6 + pandora/edit/managers.py | 133 +++++++ pandora/edit/migrations/0002_cleanup.py | 321 +++++++++++++++++ pandora/edit/models.py | 303 +++++++++++++++- pandora/edit/views.py | 455 +++++++++++++++++++----- pandora/text/views.py | 4 +- pandora/urls.py | 1 + static/js/pandora/URL.js | 14 + static/js/pandora/editPanel.js | 200 +++++++++++ static/js/pandora/mainPanel.js | 3 + static/js/pandora/rightPanel.js | 2 + static/js/pandora/sectionButtons.js | 4 +- static/js/pandora/sectionSelect.js | 6 +- static/js/pandora/utils.js | 15 +- 17 files changed, 1363 insertions(+), 120 deletions(-) create mode 100644 pandora/edit/managers.py create mode 100644 pandora/edit/migrations/0002_cleanup.py create mode 100644 static/js/pandora/editPanel.js diff --git a/pandora/config.0xdb.jsonc b/pandora/config.0xdb.jsonc index 1a5ae5f7..99b694da 100644 --- a/pandora/config.0xdb.jsonc +++ b/pandora/config.0xdb.jsonc @@ -791,6 +791,11 @@ "showMapControls": false, "showMapLabels": false, "showFolder": { + "edits": { + "personal": true, + "favorite": true, + "featured": true + } "items": { "personal": true, "favorite": true, diff --git a/pandora/config.indiancinema.jsonc b/pandora/config.indiancinema.jsonc index a7f94947..c2e1e52f 100644 --- a/pandora/config.indiancinema.jsonc +++ b/pandora/config.indiancinema.jsonc @@ -829,6 +829,11 @@ "showMapControls": false, "showMapLabels": false, "showFolder": { + "edits": { + "personal": true, + "favorite": true, + "featured": true + }, "items": { "personal": true, "favorite": true, diff --git a/pandora/config.padma.jsonc b/pandora/config.padma.jsonc index 00cdc498..5072fadb 100644 --- a/pandora/config.padma.jsonc +++ b/pandora/config.padma.jsonc @@ -713,6 +713,12 @@ "showMapControls": false, "showMapLabels": false, "showFolder": { + "edits": { + "personal": true, + "favorite": true, + "featured": true, + "volumes": true + }, "items": { "personal": true, "favorite": true, diff --git a/pandora/config.pandora.jsonc b/pandora/config.pandora.jsonc index 6a9edcea..8df2e93b 100644 --- a/pandora/config.pandora.jsonc +++ b/pandora/config.pandora.jsonc @@ -632,6 +632,12 @@ "showMapControls": false, "showMapLabels": false, "showFolder": { + "edits": { + "personal": true, + "favorite": true, + "featured": true, + "volumes": true + }, "items": { "personal": true, "favorite": true, diff --git a/pandora/edit/managers.py b/pandora/edit/managers.py new file mode 100644 index 00000000..4e06e1e4 --- /dev/null +++ b/pandora/edit/managers.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from django.db.models import Q, Manager + + +def parseCondition(condition, user): + ''' + ''' + k = condition.get('key', 'name') + k = { + 'user': 'user__username', + 'position': 'position__position', + 'posterFrames': 'poster_frames', + }.get(k, k) + if not k: + k = 'name' + v = condition['value'] + op = condition.get('operator') + if not op: + op = '=' + if op.startswith('!'): + op = op[1:] + exclude = True + else: + exclude = False + if k == 'id': + v = v.split(":") + if len(v) >= 2: + v = (v[0], ":".join(v[1:])) + q = Q(user__username=v[0], name=v[1]) + else: + q = Q(id__in=[]) + return q + if k == 'subscribed': + key = 'subscribed_users__username' + v = user.username + elif isinstance(v, bool): #featured and public flag + key = k + else: + key = "%s%s" % (k, { + '==': '__iexact', + '^': '__istartswith', + '$': '__iendswith', + }.get(op, '__icontains')) + key = str(key) + if exclude: + q = ~Q(**{key: v}) + else: + q = Q(**{key: v}) + return q + +def parseConditions(conditions, operator, user): + ''' + conditions: [ + { + value: "war" + } + { + key: "year", + value: "1970-1980, + operator: "!=" + }, + { + key: "country", + value: "f", + operator: "^" + } + ], + operator: "&" + ''' + conn = [] + for condition in conditions: + if 'conditions' in condition: + q = parseConditions(condition['conditions'], + condition.get('operator', '&'), user) + if q: + conn.append(q) + pass + else: + conn.append(parseCondition(condition, user)) + if conn: + q = conn[0] + for c in conn[1:]: + if operator == '|': + q = q | c + else: + q = q & c + return q + return None + + +class EditManager(Manager): + + def get_query_set(self): + return super(EditManager, self).get_query_set() + + def find(self, data, user): + ''' + query: { + conditions: [ + { + value: "war" + } + { + key: "year", + value: "1970-1980, + operator: "!=" + }, + { + key: "country", + value: "f", + operator: "^" + } + ], + operator: "&" + } + ''' + + #join query with operator + qs = self.get_query_set() + conditions = parseConditions(data['query'].get('conditions', []), + data['query'].get('operator', '&'), + user) + if conditions: + qs = qs.filter(conditions) + + if user.is_anonymous(): + qs = qs.filter(Q(status='public') | Q(status='featured')) + else: + qs = qs.filter(Q(status='public') | Q(status='featured') | Q(user=user)) + return qs + + diff --git a/pandora/edit/migrations/0002_cleanup.py b/pandora/edit/migrations/0002_cleanup.py new file mode 100644 index 00000000..5df168af --- /dev/null +++ b/pandora/edit/migrations/0002_cleanup.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Position' + db.create_table('edit_position', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('edit', self.gf('django.db.models.fields.related.ForeignKey')(related_name='position', to=orm['edit.Edit'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='edit_position', to=orm['auth.User'])), + ('section', self.gf('django.db.models.fields.CharField')(max_length='255')), + ('position', self.gf('django.db.models.fields.IntegerField')(default=0)), + )) + db.send_create_signal('edit', ['Position']) + + # Adding unique constraint on 'Position', fields ['user', 'edit', 'section'] + db.create_unique('edit_position', ['user_id', 'edit_id', 'section']) + + # Deleting field 'Clip.position' + db.delete_column('edit_clip', 'position') + + # Deleting field 'Clip.edit_position' + db.delete_column('edit_clip', 'edit_position') + + # Adding field 'Clip.index' + db.add_column('edit_clip', 'index', + self.gf('django.db.models.fields.IntegerField')(default=0), + keep_default=False) + + # Adding field 'Clip.annotation' + db.add_column('edit_clip', 'annotation', + self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='editclip', null=True, to=orm['annotation.Annotation']), + keep_default=False) + + + # Changing field 'Clip.item' + db.alter_column('edit_clip', 'item_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['item.Item'])) + # Deleting field 'Edit.public' + db.delete_column('edit_edit', 'public') + + # Deleting field 'Edit.duration' + db.delete_column('edit_edit', 'duration') + + # Adding field 'Edit.status' + db.add_column('edit_edit', 'status', + self.gf('django.db.models.fields.CharField')(default='private', max_length=20), + keep_default=False) + + # Adding field 'Edit.description' + db.add_column('edit_edit', 'description', + self.gf('django.db.models.fields.TextField')(default=''), + keep_default=False) + + # Adding field 'Edit.rightslevel' + db.add_column('edit_edit', 'rightslevel', + self.gf('django.db.models.fields.IntegerField')(default=0, db_index=True), + keep_default=False) + + # Adding field 'Edit.icon' + db.add_column('edit_edit', 'icon', + self.gf('django.db.models.fields.files.ImageField')(default=None, max_length=100, null=True, blank=True), + keep_default=False) + + # Adding field 'Edit.poster_frames' + db.add_column('edit_edit', 'poster_frames', + self.gf('ox.django.fields.TupleField')(default=[]), + keep_default=False) + + # Adding M2M table for field subscribed_users on 'Edit' + db.create_table('edit_edit_subscribed_users', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('edit', models.ForeignKey(orm['edit.edit'], null=False)), + ('user', models.ForeignKey(orm['auth.user'], null=False)) + )) + db.create_unique('edit_edit_subscribed_users', ['edit_id', 'user_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'Position', fields ['user', 'edit', 'section'] + db.delete_unique('edit_position', ['user_id', 'edit_id', 'section']) + + # Deleting model 'Position' + db.delete_table('edit_position') + + # Adding field 'Clip.position' + db.add_column('edit_clip', 'position', + self.gf('django.db.models.fields.IntegerField')(default=0), + keep_default=False) + + # Adding field 'Clip.edit_position' + db.add_column('edit_clip', 'edit_position', + self.gf('django.db.models.fields.FloatField')(default=0), + keep_default=False) + + # Deleting field 'Clip.index' + db.delete_column('edit_clip', 'index') + + # Deleting field 'Clip.annotation' + db.delete_column('edit_clip', 'annotation_id') + + + # Changing field 'Clip.item' + db.alter_column('edit_clip', 'item_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['item.Item'])) + # Adding field 'Edit.public' + db.add_column('edit_edit', 'public', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + # Adding field 'Edit.duration' + db.add_column('edit_edit', 'duration', + self.gf('django.db.models.fields.FloatField')(default=0), + keep_default=False) + + # Deleting field 'Edit.status' + db.delete_column('edit_edit', 'status') + + # Deleting field 'Edit.description' + db.delete_column('edit_edit', 'description') + + # Deleting field 'Edit.rightslevel' + db.delete_column('edit_edit', 'rightslevel') + + # Deleting field 'Edit.icon' + db.delete_column('edit_edit', 'icon') + + # Deleting field 'Edit.poster_frames' + db.delete_column('edit_edit', 'poster_frames') + + # Removing M2M table for field subscribed_users on 'Edit' + db.delete_table('edit_edit_subscribed_users') + + + models = { + 'annotation.annotation': { + 'Meta': {'object_name': 'Annotation'}, + 'clip': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'annotations'", 'null': 'True', 'to': "orm['clip.Clip']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'end': ('django.db.models.fields.FloatField', [], {'default': '-1', 'db_index': 'True'}), + 'findvalue': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'item': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'annotations'", 'to': "orm['item.Item']"}), + 'layer': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'public_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'unique': 'True', 'null': 'True'}), + 'sortvalue': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '1000', 'null': 'True', 'blank': 'True'}), + 'start': ('django.db.models.fields.FloatField', [], {'default': '-1', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'value': ('django.db.models.fields.TextField', [], {}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'clip.clip': { + 'Meta': {'unique_together': "(('item', 'start', 'end'),)", 'object_name': 'Clip'}, + 'aspect_ratio': ('django.db.models.fields.FloatField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'duration': ('django.db.models.fields.FloatField', [], {'default': '0', 'db_index': 'True'}), + 'end': ('django.db.models.fields.FloatField', [], {'default': '-1'}), + 'findvalue': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_index': 'True'}), + 'hue': ('django.db.models.fields.FloatField', [], {'default': '0', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'item': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'clips'", 'to': "orm['item.Item']"}), + 'keywords': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'lightness': ('django.db.models.fields.FloatField', [], {'default': '0', 'db_index': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'notes': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'saturation': ('django.db.models.fields.FloatField', [], {'default': '0', 'db_index': 'True'}), + 'sort': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'matching_clips'", 'to': "orm['item.ItemSort']"}), + 'sortvalue': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'start': ('django.db.models.fields.FloatField', [], {'default': '-1', 'db_index': 'True'}), + 'subtitles': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'user': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), + 'volume': ('django.db.models.fields.FloatField', [], {'default': '0', 'null': 'True', 'db_index': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'edit.clip': { + 'Meta': {'object_name': 'Clip'}, + 'annotation': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'editclip'", 'null': 'True', 'to': "orm['annotation.Annotation']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'edit': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'clips'", 'to': "orm['edit.Edit']"}), + 'end': ('django.db.models.fields.FloatField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'item': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'editclip'", 'null': 'True', 'to': "orm['item.Item']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'start': ('django.db.models.fields.FloatField', [], {'default': '0'}) + }, + 'edit.edit': { + 'Meta': {'unique_together': "(('user', 'name'),)", 'object_name': 'Edit'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'icon': ('django.db.models.fields.files.ImageField', [], {'default': 'None', 'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'poster_frames': ('ox.django.fields.TupleField', [], {'default': '[]'}), + 'rightslevel': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'private'", 'max_length': '20'}), + 'subscribed_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'subscribed_edits'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'edit.position': { + 'Meta': {'unique_together': "(('user', 'edit', 'section'),)", 'object_name': 'Position'}, + 'edit': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'position'", 'to': "orm['edit.Edit']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'section': ('django.db.models.fields.CharField', [], {'max_length': "'255'"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_position'", 'to': "orm['auth.User']"}) + }, + 'item.item': { + 'Meta': {'object_name': 'Item'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'data': ('ox.django.fields.DictField', [], {'default': '{}'}), + 'external_data': ('ox.django.fields.DictField', [], {'default': '{}'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'items'", 'blank': 'True', 'to': "orm['auth.Group']"}), + 'icon': ('django.db.models.fields.files.ImageField', [], {'default': 'None', 'max_length': '100', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'itemId': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128', 'blank': 'True'}), + 'json': ('ox.django.fields.DictField', [], {'default': '{}'}), + 'level': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'oxdbId': ('django.db.models.fields.CharField', [], {'max_length': '42', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'poster': ('django.db.models.fields.files.ImageField', [], {'default': 'None', 'max_length': '100', 'blank': 'True'}), + 'poster_frame': ('django.db.models.fields.FloatField', [], {'default': '-1'}), + 'poster_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'poster_source': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'poster_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'rendered': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'stream_aspect': ('django.db.models.fields.FloatField', [], {'default': '1.3333333333333333'}), + 'stream_info': ('ox.django.fields.DictField', [], {'default': '{}'}), + 'torrent': ('django.db.models.fields.files.FileField', [], {'default': 'None', 'max_length': '1000', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'null': 'True', 'to': "orm['auth.User']"}) + }, + 'item.itemsort': { + 'Meta': {'object_name': 'ItemSort'}, + 'accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'aspectratio': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'bitrate': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'cinematographer': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'codirector': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'color': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'composer': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'cutsperminute': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'director': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'duration': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'editor': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'genre': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'height': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'hue': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'imdbId': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'item': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'sort'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['item.Item']"}), + 'itemId': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'lightness': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'lyricist': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'numberofactors': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'numberofcuts': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'numberoffiles': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'parts': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'pixels': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'producer': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'productionCompany': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'random': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'resolution': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'rightslevel': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'runtime': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'saturation': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'size': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'sound': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'timesaccessed': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'volume': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'width': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'words': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'wordsperminute': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'writer': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'db_index': 'True'}), + 'year': ('django.db.models.fields.CharField', [], {'max_length': '4', 'null': 'True', 'db_index': 'True'}) + } + } + + complete_apps = ['edit'] \ No newline at end of file diff --git a/pandora/edit/models.py b/pandora/edit/models.py index 3aba2cdb..fe91f263 100644 --- a/pandora/edit/models.py +++ b/pandora/edit/models.py @@ -2,51 +2,318 @@ # vi:si:et:sw=4:sts=4:ts=4 from __future__ import division, with_statement -from django.db import models -from django.contrib.auth.models import User +import re +import os +import shutil +import ox +from django.conf import settings +from django.db import models +from django.db.models import Max +from django.contrib.auth.models import User +from ox.django.fields import TupleField + +from annotation.models import Annotation +from item.models import Item + +from archive import extract + +import managers class Edit(models.Model): class Meta: unique_together = ("user", "name") + objects = managers.EditManager() + created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) user = models.ForeignKey(User) name = models.CharField(max_length=255) - public = models.BooleanField(default=False) - duration = models.FloatField(default=0) - #FIXME: how to deal with width/height? + status = models.CharField(max_length=20, default='private') + _status = ['private', 'public', 'featured'] + description = models.TextField(default='') + rightslevel = models.IntegerField(db_index=True, default=0) + + icon = models.ImageField(default=None, blank=True, null=True, + upload_to=lambda i, x: i.path("icon.jpg")) + + poster_frames = TupleField(default=[], editable=False) + subscribed_users = models.ManyToManyField(User, related_name='subscribed_edits') + def __unicode__(self): return u'%s (%s)' % (self.title, self.user) + + def get_id(self): + return u'%s:%s' % (self.user.username, self.name) + + def get_absolute_url(self): + return ('/edits/%s' % quote(self.get_id())).replace('%3A', ':') + + def add_clip(self, data): + clip = Clip(edit=self) + if 'annotation' in data: + clip.annotation = Annotation.objects.get(public_id=data['annotation']) + else: + clip.item = Item.objects.get(itemId=data['item']) + clip.start = data['in'] + clip.end = data['out'] + clip.index = Clip.objects.filter(edit=self).aggregate(Max('index'))['index__max'] + if clip.index == None: + clip.index = 0 + else: + clip.index +=1 + clip.save() + return clip + + def accessible(self, user): + return self.user == user or self.status in ('public', 'featured') def editable(self, user): - #FIXME: make permissions work - if self.user == user or user.is_staff: + if not user or user.is_anonymous(): + return False + if self.user == user or \ + user.is_staff or \ + user.get_profile().capability('canEditFeaturedEdits') == True: return True return False - ''' - #creating a new file from clips seams to work not to bad, needs testing for frame accuracy - ffmpeg -i 96p.webm -ss 123.33 -t 3 -vcodec copy -acodec copy 1.webm - ffmpeg -i 96p.webm -ss 323.33 -t 4 -vcodec copy -acodec copy 2.webm - ffmpeg -i 96p.webm -ss 423.33 -t 1 -vcodec copy -acodec copy 3.webm - mkvmerge 1.webm +2.webm +3.webm -o cutup.webm - ''' + def edit(self, data, user): + for key in data: + if key == 'status': + value = data[key] + if value not in self._status: + value = self._status[0] + if value == 'private': + for user in self.subscribed_users.all(): + self.subscribed_users.remove(user) + qs = Position.objects.filter(user=user, + section='section', edit=self) + if qs.count() > 1: + pos = qs[0] + pos.section = 'personal' + pos.save() + elif value == 'featured': + if user.get_profile().capability('canEditFeaturedEdits'): + pos, created = Position.objects.get_or_create(edit=self, user=user, + section='featured') + if created: + qs = Position.objects.filter(user=user, section='featured') + pos.position = qs.aggregate(Max('position'))['position__max'] + 1 + pos.save() + Position.objects.filter(edit=self).exclude(id=pos.id).delete() + else: + value = self.status + elif self.status == 'featured' and value == 'public': + Position.objects.filter(edit=self).delete() + pos, created = Position.objects.get_or_create(edit=self, + user=self.user,section='personal') + qs = Position.objects.filter(user=self.user, + section='personal') + pos.position = qs.aggregate(Max('position'))['position__max'] + 1 + pos.save() + for u in self.subscribed_users.all(): + pos, created = Position.objects.get_or_create(edit=self, user=u, + section='public') + qs = Position.objects.filter(user=u, section='public') + pos.position = qs.aggregate(Max('position'))['position__max'] + 1 + pos.save() + self.status = value + elif key == 'name': + data['name'] = re.sub(' \[\d+\]$', '', data['name']).strip() + if not data['name']: + data['name'] = "Untitled" + name = data['name'] + num = 1 + while Edit.objects.filter(name=name, user=self.user).exclude(id=self.id).count()>0: + num += 1 + name = data['name'] + ' [%d]' % num + self.name = name + elif key == 'description': + self.description = ox.sanitize_html(data['description']) + elif key == 'rightslevel': + self.rightslevel = int(data['rightslevel']) + + if 'position' in data: + pos, created = Position.objects.get_or_create(edit=self, user=user) + pos.position = data['position'] + pos.section = 'featured' + if self.status == 'private': + pos.section = 'personal' + pos.save() + if 'type' in data: + self.type = data['type'] == 'pdf' and 'pdf' or 'html' + if 'posterFrames' in data: + self.poster_frames = tuple(data['posterFrames']) + self.save() + if 'posterFrames' in data: + self.update_icon() + + def path(self, name=''): + h = "%07d" % self.id + return os.path.join('edits', h[:2], h[2:4], h[4:6], h[6:], name) + + def get_items(self, user=None): + return Item.objects.filter(editclips__id__in=self.clips.all()).distinct() + + def update_icon(self): + frames = [] + if not self.poster_frames: + items = self.get_items(self.user).filter(rendered=True) + if items.count(): + poster_frames = [] + for i in range(0, items.count(), max(1, int(items.count()/4))): + poster_frames.append({ + 'item': items[int(i)].itemId, + 'position': items[int(i)].poster_frame + }) + self.poster_frames = tuple(poster_frames) + self.save() + for i in self.poster_frames: + s = Item.objects.filter(itemId=i['item']) + if qs.count() > 0: + frame = qs[0].frame(i['position']) + if frame: + frames.append(frame) + self.icon.name = self.path('icon.jpg') + icon = self.icon.path + if frames: + while len(frames) < 4: + frames += frames + folder = os.path.dirname(icon) + ox.makedirs(folder) + for f in glob("%s/icon*.jpg" % folder): + os.unlink(f) + cmd = [ + settings.LIST_ICON, + '-f', ','.join(frames), + '-o', icon + ] + p = subprocess.Popen(cmd) + p.wait() + self.save() + + def get_icon(self, size=16): + path = self.path('icon%d.jpg' % size) + path = os.path.join(settings.MEDIA_ROOT, path) + if not os.path.exists(path): + folder = os.path.dirname(path) + ox.makedirs(folder) + if self.icon and os.path.exists(self.icon.path): + source = self.icon.path + max_size = min(self.icon.width, self.icon.height) + else: + source = os.path.join(settings.STATIC_ROOT, 'jpg/list256.jpg') + max_size = 256 + if size < max_size: + extract.resize_image(source, path, size=size) + else: + path = source + return path + + def json(self, keys=None, user=None): + if not keys: + keys=[ + 'description', + 'editable', + 'rightslevel', + 'id', + 'clips', + 'name', + 'posterFrames', + 'status', + 'subscribed', + 'user' + ] + response = { + 'type': 'static' + } + _map = { + 'posterFrames': 'poster_frames' + } + for key in keys: + if key == 'id': + response[key] = self.get_id() + elif key == 'clips': + response[key] = [c.json(user) for c in self.clips.all().order_by('index')] + elif key == 'editable': + response[key] = self.editable(user) + elif key == 'user': + response[key] = self.user.username + elif key == 'subscribers': + response[key] = self.subscribed_users.all().count() + elif key == 'subscribed': + if user and not user.is_anonymous(): + response[key] = self.subscribed_users.filter(id=user.id).exists() + elif hasattr(self, _map.get(key, key)): + response[key] = getattr(self, _map.get(key,key)) + return response + + def render(self): + #creating a new file from clips + tmp = tempfile.mkdtemp() + clips = [] + for clip in self.clips.all().order_by('index'): + data = clip.json() + clips.append(os.path.join(tmp, '%06d.webm' % data['index'])) + cmd = ['avconv', '-i', path, + '-ss', data['in'], '-t', data['out'], + '-vcodec', 'copy', '-acodec', 'copy', + clips[-1]] + #p = subprocess.Popen(cmd) + #p.wait() + cmd = ['mkvmerge', clips[0]] \ + + ['+'+c for c in clips[1:]] \ + + [os.path.join(tmp, 'render.webm')] + #p = subprocess.Popen(cmd) + #p.wait() + shutil.rmtree(tmp) class Clip(models.Model): created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) - edit = models.ForeignKey(Edit) - position = models.IntegerField(default=0) #clip position - edit_position = models.FloatField(default=0) #Position in seconds on edit - item = models.ForeignKey("item.Item") + edit = models.ForeignKey(Edit, related_name='clips') + index = models.IntegerField(default=0) + item = models.ForeignKey(Item, null=True, default=None, related_name='editclip') + annotation = models.ForeignKey(Annotation, null=True, default=None, related_name='editclip') start = models.FloatField(default=0) end = models.FloatField(default=0) def __unicode__(self): + if self.annotation: + return u'%s' % self.annotation.public_id return u'%s/%0.3f-%0.3f' % (self.item.itemId, self.start, self.end) + + def json(self, user=None): + data = { + 'id': ox.toAZ(self.id), + 'index': self.index + } + if self.annotation: + data['annotation'] = self.annotation.public_id + data['in'] = self.annotation.start + data['out'] = self.annotation.end + else: + data['item'] = self.item.itemId + data['in'] = self.start + data['out'] = self.end + data['duration'] = data['out'] - data['in'] + return data + +class Position(models.Model): + + class Meta: + unique_together = ("user", "edit", "section") + + edit = models.ForeignKey(Edit, related_name='position') + user = models.ForeignKey(User, related_name='edit_position') + section = models.CharField(max_length='255') + position = models.IntegerField(default=0) + + def __unicode__(self): + return u'%s/%s/%s' % (self.section, self.position, self.edit) + diff --git a/pandora/edit/views.py b/pandora/edit/views.py index 46bdf725..1aa80476 100644 --- a/pandora/edit/views.py +++ b/pandora/edit/views.py @@ -1,41 +1,49 @@ # -*- coding: utf-8 -*- # vi:si:et:sw=4:sts=4:ts=4 from __future__ import division +import os +import re +import ox from ox.utils import json from ox.django.decorators import login_required_json from ox.django.shortcuts import render_to_json_response, get_object_or_404_json, json_response - +from django.db.models import Max +from ox.django.http import HttpFileResponse from ox.django.api import actions + +from item import utils + import models +def get_edit_or_404_json(id): + id = id.split(':') + username = id[0] + name = ":".join(id[1:]) + return get_object_or_404_json(models.Edit, user__username=username, name=name) @login_required_json def addClip(request): ''' takes { - item: string, edit: string, - start: float, - end: float, + item: string, + in: float, + out: float, + annotation: string } + add clip with item/in/out or annotation to edit with id returns { } ''' + response = json_response() data = json.loads(request.POST['data']) - list = get_object_or_404_json(models.Timeline, pk=data['list']) - if 'item' in data: - item = get_object_or_404_json(models.Item, pk=data['item']) - if list.editable(request.user): - list.add(item) - response = json_response(status=200, text='item added') - else: - response = json_response(status=403, text='not allowed') - elif 'query' in data: - response = json_response(status=501, text='not implemented') - + edit = get_edit_or_404_json(data['edit']) + if edit.editable(request.user): + clip = edit.add_clip(data) + response['data'] = clip.json(request.user) else: - response = json_response(status=501, text='not implemented') + response = json_response(status=403, text='permission denied') return render_to_json_response(response) actions.register(addClip, cache=False) @@ -44,112 +52,172 @@ actions.register(addClip, cache=False) def removeClip(request): ''' takes { - item: string + edit: string + ids: [string] } returns { } ''' + response = json_response() data = json.loads(request.POST['data']) - list = get_object_or_404_json(models.Timeline, pk=data['list']) - if 'item' in data: - item = get_object_or_404_json(models.Item, pk=data['item']) - if list.editable(request.user): - list.remove(item) - response = json_response(status=200, text='item removed') - else: - response = json_response(status=403, text='not allowed') - elif 'query' in data: - response = json_response(status=501, text='not implemented') - + edit = get_edit_or_404_json(data['edit']) + if 'id' in data: + ids = [data['id']] else: - response = json_response(status=501, text='not implemented') + ids = data['ids'] + ids = map(ox.fromAZ, ids) + if edit.editable(request.user): + for clip in edit.clips.filter(id__in=ids): + clip.delete() + else: + response = json_response(status=403, text='permission denied') return render_to_json_response(response) actions.register(removeClip, cache=False) - -def getTimeline(request): - ''' - takes { - name: string, - user: string - } - returns { - ... - } - -could be - timeline: { - 0: { - itemId:, start, end - }, - 123: { - itemId:, start, end - } - } -or implicit timeline position - timeline: [ - { - itemId:, start, end - }, - { - itemId:, start, end - } - ] - - ''' - response = json_response(status=501, text='not implemented') - return render_to_json_response(response) -actions.register(getTimeline) - - @login_required_json -def addTimeline(request): +def editClip(request): ''' - takes { - ... + takes { + id: string, + in: float } returns { - ... } ''' + response = json_response() data = json.loads(request.POST['data']) - if models.Timeline.filter(name=data['name'], user=request.user).count() == 0: - list = models.Timeline(name=data['name'], user=request.user) - list.save() - response = json_response(status=200, text='created') + clip = get_object_or_404_json(models.Clip, pk=ox.fromAZ(data['id'])) + if clip.edit.editable(request.user): + for key in ('in', 'out'): + if key in data: + if clip.annotation: + clip.start = clip.annotation.start + clip.end = clip.annotation.end + clip.annotation = None + setattr(clip, {'in': 'start', 'out': 'end'}.get(key), float(data[key])) + clip.save() + response['data'] = clip.json(user=request.user) else: - response = json_response(status=200, text='list already exists') - response['data']['errors'] = { - 'name': 'List already exists' - } + response = json_response(status=403, text='permission denied') return render_to_json_response(response) -actions.register(addTimeline, cache=False) - +actions.register(editClip, cache=False) @login_required_json -def editTimeline(request): +def sortClips(request): + ''' + takes { + edit: string + ids: [string] + } + returns { + } + ''' + data = json.loads(request.POST['data']) + edit = get_edit_or_404_json(data['edit']) + response = json_response() + ids = map(ox.fromAZ, data['ids']) + if edit.editable(request.user): + index = 0 + for i in ids: + models.Clip.objects.filter(edit=edit, id=i).update(index=index) + index += 1 + else: + response = json_response(status=403, text='permission denied') + return render_to_json_response(response) +actions.register(sortClips, cache=False) + +def getEdit(request): ''' takes { - ... + id: + } + returns { + id: + clips: + } + ''' + data = json.loads(request.POST['data']) + if 'id' in data: + edit = get_edit_or_404_json(data['id']) + response = json_response() + if edit.accessible(request.user): + response['data'] = edit.json(user=request.user) + else: + response = json_response(status=403, text='not allowed') + else: + response = json_response(status=404, text='not found') + return render_to_json_response(response) +actions.register(getEdit) + +@login_required_json +def addEdit(request): + ''' + takes { + name } returns { ... } ''' data = json.loads(request.POST['data']) - list = get_object_or_404_json(models.Timeline, pk=data['list']) - if list.editable(request.user): - for key in data: - if key in ('name', 'public'): - setattr(list, key, data['key']) + data['name'] = re.sub(' \[\d+\]$', '', data.get('name', 'Untitled')).strip() + name = data['name'] + if not name: + name = "Untitled" + num = 1 + created = False + while not created: + edit, created = models.Edit.objects.get_or_create(name=name, user=request.user) + num += 1 + name = data['name'] + ' [%d]' % num + + del data['name'] + if data: + edit.edit(data, request.user) + else: + edit.save() + if edit.status == 'featured': + pos, created = models.Position.objects.get_or_create(edit=edit, + user=request.user, section='featured') + qs = models.Position.objects.filter(section='featured') + else: + pos, created = models.Position.objects.get_or_create(edit=edit, + user=request.user, section='personal') + qs = models.Position.objects.filter(user=request.user, section='personal') + pos.position = qs.aggregate(Max('position'))['position__max'] + 1 + pos.save() + response = json_response(status=200, text='created') + response['data'] = edit.json(user=request.user) + return render_to_json_response(response) +actions.register(addEdit, cache=False) + +@login_required_json +def editEdit(request): + ''' + takes { + id + } + returns { + ... + } + ''' + data = json.loads(request.POST['data']) + edit = get_edit_or_404_json(data['id']) + response = json_response() + if edit.editable(request.user): + edit.edit(data, request.user) + response['data'] = edit.json(keys=[ + 'description', 'editable', 'rightslevel', + 'id', 'name', 'posterFrames', 'status', + 'subscribed', 'user' + ], user=request.user) else: response = json_response(status=403, text='not allowed') return render_to_json_response(response) -actions.register(editTimeline, cache=False) - +actions.register(editEdit, cache=False) @login_required_json -def removeTimeline(request): +def removeEdit(request): ''' takes { ... @@ -159,10 +227,211 @@ def removeTimeline(request): } ''' data = json.loads(request.POST['data']) - list = get_object_or_404_json(models.Timeline, pk=data['list']) - if list.editable(request.user): - list.delete() + edit = get_edit_or_404_json(data['id']) + response = json_response() + if edit.editable(request.user): + edit.delete() else: response = json_response(status=403, text='not allowed') return render_to_json_response(response) -actions.register(removeTimeline, cache=False) +actions.register(removeEdit, cache=False) + +def _order_query(qs, sort): + order_by = [] + for e in sort: + operator = e['operator'] + if operator != '-': + operator = '' + key = { + 'subscribed': 'subscribed_users', + 'items': 'numberofitems' + }.get(e['key'], e['key']) + order = '%s%s' % (operator, key) + order_by.append(order) + if key == 'subscribers': + qs = qs.annotate(subscribers=Sum('subscribed_users')) + if order_by: + qs = qs.order_by(*order_by) + qs = qs.distinct() + return qs + +def parse_query(data, user): + query = {} + query['range'] = [0, 100] + query['sort'] = [{'key':'user', 'operator':'+'}, {'key':'name', 'operator':'+'}] + for key in ('keys', 'group', 'edit', 'range', 'position', 'positions', 'sort'): + if key in data: + query[key] = data[key] + query['qs'] = models.Edit.objects.find(data, user).exclude(name='') + return query + + +def findEdits(request): + ''' + takes { + query: { + conditions: [ + { + key: 'user', + value: 'something', + operator: '=' + } + ] + operator: "," + }, + sort: [{key: 'name', operator: '+'}], + range: [0, 100] + keys: [] + } + + possible query keys: + name, user, featured, subscribed + + possible keys: + name, user, featured, subscribed, query + + } + returns { + items: [object] + } + ''' + data = json.loads(request.POST['data']) + query = parse_query(data, request.user) + + #order + is_section_request = query['sort'] == [{u'operator': u'+', u'key': u'position'}] + def is_featured_condition(x): + return x['key'] == 'status' and \ + x['value'] == 'featured' and \ + x['operator'] in ('=', '==') + is_featured = len(filter(is_featured_condition, data['query'].get('conditions', []))) > 0 + + if is_section_request: + qs = query['qs'] + if not is_featured and not request.user.is_anonymous(): + qs = qs.filter(position__in=models.Position.objects.filter(user=request.user)) + qs = qs.order_by('position__position') + else: + qs = _order_query(query['qs'], query['sort']) + + response = json_response() + if 'keys' in data: + qs = qs[query['range'][0]:query['range'][1]] + + response['data']['items'] = [l.json(data['keys'], request.user) for l in qs] + elif 'position' in data: + #FIXME: actually implement position requests + response['data']['position'] = 0 + elif 'positions' in data: + ids = [i.get_id() for i in qs] + response['data']['positions'] = utils.get_positions(ids, query['positions']) + else: + response['data']['items'] = qs.count() + return render_to_json_response(response) +actions.register(findEdits) + +@login_required_json +def subscribeToEdit(request): + ''' + takes { + id: string, + } + returns {} + ''' + data = json.loads(request.POST['data']) + edit = get_edit_or_404_json(data['id']) + user = request.user + if edit.status == 'public' and \ + edit.subscribed_users.filter(username=user.username).count() == 0: + edit.subscribed_users.add(user) + pos, created = models.Position.objects.get_or_create(edit=edit, user=user, section='public') + if created: + qs = models.Position.objects.filter(user=user, section='public') + pos.position = qs.aggregate(Max('position'))['position__max'] + 1 + pos.save() + response = json_response() + return render_to_json_response(response) +actions.register(subscribeToEdit, cache=False) + + +@login_required_json +def unsubscribeFromEdit(request): + ''' + takes { + id: string, + user: username(only admins) + } + returns {} + ''' + data = json.loads(request.POST['data']) + edit = get_edit_or_404_json(data['id']) + user = request.user + edit.subscribed_users.remove(user) + models.Position.objects.filter(edit=edit, user=user, section='public').delete() + response = json_response() + return render_to_json_response(response) +actions.register(unsubscribeFromEdit, cache=False) + + +@login_required_json +def sortEdits(request): + ''' + takes { + section: 'personal', + ids: [1,2,4,3] + } + known sections: 'personal', 'public', 'featured' + featured can only be edited by admins + + returns {} + ''' + data = json.loads(request.POST['data']) + position = 0 + section = data['section'] + #ids = list(set(data['ids'])) + ids = data['ids'] + if section == 'featured' and not request.user.get_profile().capability('canEditFeaturedEdits'): + response = json_response(status=403, text='not allowed') + else: + user = request.user + if section == 'featured': + for i in ids: + l = get_edit_or_404_json(i) + qs = models.Position.objects.filter(section=section, edit=l) + if qs.count() > 0: + pos = qs[0] + else: + pos = models.Position(edit=l, user=user, section=section) + if pos.position != position: + pos.position = position + pos.save() + position += 1 + models.Position.objects.filter(section=section, edit=l).exclude(id=pos.id).delete() + else: + for i in ids: + l = get_edit_or_404_json(i) + pos, created = models.Position.objects.get_or_create(edit=l, + user=request.user, section=section) + if pos.position != position: + pos.position = position + pos.save() + position += 1 + + response = json_response() + return render_to_json_response(response) +actions.register(sortEdits, cache=False) + +def icon(request, id, size=16): + if not size: + size = 16 + + id = id.split(':') + username = id[0] + name = ":".join(id[1:]) + qs = models.Edit.objects.filter(user__username=username, name=name) + if qs.count() == 1 and qs[0].accessible(request.user): + edit = qs[0] + icon = edit.get_icon(int(size)) + else: + icon = os.path.join(settings.STATIC_ROOT, 'jpg/list256.jpg') + return HttpFileResponse(icon, content_type='image/jpeg') diff --git a/pandora/text/views.py b/pandora/text/views.py index 617ee98b..e2cd338b 100644 --- a/pandora/text/views.py +++ b/pandora/text/views.py @@ -21,8 +21,8 @@ import models def get_text_or_404_json(id): id = id.split(':') username = id[0] - textname = ":".join(id[1:]) - return get_object_or_404_json(models.Text, user__username=username, name=textname) + name = ":".join(id[1:]) + return get_object_or_404_json(models.Text, user__username=username, name=name) @login_required_json def addText(request): diff --git a/pandora/urls.py b/pandora/urls.py index 05d93b55..07c0db53 100644 --- a/pandora/urls.py +++ b/pandora/urls.py @@ -32,6 +32,7 @@ urlpatterns = patterns('', (r'^resetUI$', 'user.views.reset_ui'), (r'^documents/(?P.*?.pdf).jpg$', 'document.views.thumbnail'), (r'^documents/(?P.*?.)$', 'document.views.file'), + (r'^edit/(?P.*?)/icon(?P\d*).jpg$', 'edit.views.icon'), (r'^list/(?P.*?)/icon(?P\d*).jpg$', 'itemlist.views.icon'), (r'^text/(?P.*?)/icon(?P\d*).jpg$', 'text.views.icon'), (r'^texts/(?P.*?)/text.pdf$', 'text.views.pdf'), diff --git a/static/js/pandora/URL.js b/static/js/pandora/URL.js index 991ec7aa..89b0005e 100644 --- a/static/js/pandora/URL.js +++ b/static/js/pandora/URL.js @@ -317,6 +317,20 @@ pandora.URL = (function() { item: {} }; + // Edits + views['edits'] = { + list: [], + item: ['edit'] + }; + spanType['edits'] = { + list: [], + item: {edit: 'number'} + }; + sortKeys['edits'] = { + list: {}, + item: {} + }; + findKeys = [{id: 'list', type: 'string'}].concat(pandora.site.itemKeys); self.URL = Ox.URL({ diff --git a/static/js/pandora/editPanel.js b/static/js/pandora/editPanel.js new file mode 100644 index 00000000..a4a03bbc --- /dev/null +++ b/static/js/pandora/editPanel.js @@ -0,0 +1,200 @@ +'use strict'; + +pandora.ui.editPanel = function() { + + var that = Ox.SplitPanel({ + elements: [ + {element: Ox.Element(), size: 24}, + {element: Ox.Element()}, + {element: Ox.Element(), size: 16} + ], + orientation: 'vertical' + }); + + pandora.user.ui.edit && render(); + + function render() { + pandora.api.getEdit({id: pandora.user.ui.edit}, function(result) { + + var edit = result.data; + + var $toolbar = Ox.Bar({size: 24}), + + $editMenu, + + + $statusbar = Ox.Bar({size: 16}), + + $panel = Ox.SplitPanel({ + elements: [ + { + element: pandora.$ui.edit = pandora.ui.editList(edit) + }, + { + element: Ox.Element(), + size: 0, + resizable: false + } + ], + orientation: 'horizontal' + }); + + that.replaceElement(0, $toolbar); + that.replaceElement(1, $panel); + that.replaceElement(2, $statusbar); + }); + } + + that.reload = function() { + render(); + } + + return that; + +}; + +pandora.ui.editList = function(edit) { + + var height = getHeight(), + width = getWidth(), + + that = Ox.Element() + .css({ + 'overflow-y': 'auto' + }); + + self.$list = Ox.TableList({ + columns: [ + { + align: 'left', + id: 'index', + operator: '+', + title: Ox._('Index'), + visible: false, + width: 60 + }, + { + align: 'left', + id: 'id', + operator: '+', + title: Ox._('ID'), + visible: false, + width: 60 + }, + { + align: 'left', + id: 'item', + operator: '+', + title: Ox._(pandora.site.itemName.singular), + visible: true, + width: 360 + }, + { + editable: true, + id: 'in', + operator: '+', + title: Ox._('In'), + visible: true, + width: 60 + }, + { + editable: true, + id: 'out', + operator: '+', + title: Ox._('Out'), + visible: true, + width: 60 + }, + { + id: 'duration', + operator: '+', + title: Ox._('Duration'), + visible: true, + width: 60 + } + ], + columnsMovable: true, + columnsRemovable: true, + columnsResizable: true, + columnsVisible: true, + items: edit.clips, + scrollbarVisible: true, + sort: [{key: 'index', operator: '+'}], + sortable: true, + unique: 'id' + }) + .appendTo(that) + .bindEvent({ + add: function(data) { + if(pandora.user.ui.item) { + pandora.api.addClip({ + edit: pandora.user.ui.edit, + item: pandora.user.ui.item, + 'in': pandora.user.ui.videoPoints[pandora.user.ui.item]['in'], + out: pandora.user.ui.videoPoints[pandora.user.ui.item].out, + }, function(result) { + Ox.Request.clearCache(); + pandora.$ui.rightPanel.reload() + }); + } + }, + 'delete': function(data) { + if (data.ids.length > 0 && edit.editable) { + pandora.api.removeClip({ + ids: data.ids, + edit: pandora.user.ui.edit + }, function(result) { + Ox.Request.clearCache(); + pandora.$ui.rightPanel.reload() + }); + } + }, + move: function(data) { + Ox.Request.clearCache(); + pandora.api.sortClips({ + edit: pandora.user.ui.edit, + ids: data.ids + }) + }, + select: function(data) { + }, + submit: function(data) { + var value = self.$list.value(data.id, data.key); + if (data.value != value && !(data.value === '' && value === null)) { + self.$list.value(data.id, data.key, data.value || null); + var edit = { + id: data.id, + }; + edit[data.key] = parseFloat(data.value); + pandora.api.editClip(edit, function(result) { + self.$list.value(data.id, data.key, result.data[data.key]); + self.$list.value(data.id, 'duration', result.data.duration); + }); + } + } + }); + + function getHeight() { + // 24 menu + 24 toolbar + 16 statusbar + 32 title + 32 margins + // + 1px to ge trid of scrollbar + return window.innerHeight - 128 -1; + } + + function getWidth() { + return window.innerWidth + - pandora.user.ui.showSidebar * pandora.user.ui.sidebarSize - 1 + - pandora.user.ui.embedSize - 1 + - 32; + } + + that.update = function() { + $text.options({ + maxHeight: getHeight(), + width: getWidth() + }); + return that; + }; + + return that; + +}; diff --git a/static/js/pandora/mainPanel.js b/static/js/pandora/mainPanel.js index 9d3a9a81..17f43954 100644 --- a/static/js/pandora/mainPanel.js +++ b/static/js/pandora/mainPanel.js @@ -20,6 +20,9 @@ pandora.ui.mainPanel = function() { orientation: 'horizontal' }) .bindEvent({ + pandora_edit: function(data) { + that.replaceElement(1, pandora.$ui.rightPanel = pandora.ui.rightPanel()); + }, pandora_find: function() { var previousUI = pandora.UI.getPrevious(); Ox.Log('FIND', 'handled in mainPanel', previousUI.item, previousUI._list) diff --git a/static/js/pandora/rightPanel.js b/static/js/pandora/rightPanel.js index 338447a6..e7511a1e 100644 --- a/static/js/pandora/rightPanel.js +++ b/static/js/pandora/rightPanel.js @@ -59,6 +59,8 @@ pandora.ui.rightPanel = function() { }); } else if (pandora.user.ui.section == 'texts') { that = pandora.$ui.textPanel = pandora.ui.textPanel(); + } else if (pandora.user.ui.section == 'edits') { + that = pandora.$ui.editPanel = pandora.ui.editPanel(); } return that; }; diff --git a/static/js/pandora/sectionButtons.js b/static/js/pandora/sectionButtons.js index 470bc7b6..33910ff1 100644 --- a/static/js/pandora/sectionButtons.js +++ b/static/js/pandora/sectionButtons.js @@ -3,8 +3,8 @@ pandora.ui.sectionButtons = function() { var that = Ox.ButtonGroup({ buttons: [ - {id: 'items', title: pandora.site.itemName.plural}, - {id: 'edits', title: Ox._('Edits'), disabled: true}, + {id: 'items', title: Ox._(pandora.site.itemName.plural)}, + {id: 'edits', title: Ox._('Edits'), disabled: pandora.user.level != 'admin'}, {id: 'texts', title: Ox._('Texts'), disabled: pandora.user.level != 'admin'} ], id: 'sectionButtons', diff --git a/static/js/pandora/sectionSelect.js b/static/js/pandora/sectionSelect.js index 42ced846..b9bbe729 100644 --- a/static/js/pandora/sectionSelect.js +++ b/static/js/pandora/sectionSelect.js @@ -5,9 +5,9 @@ pandora.ui.sectionSelect = function() { var that = Ox.Select({ id: 'sectionSelect', items: [ - {id: 'items', title: pandora.site.itemName.plural}, - {id: 'edits', title: Ox._('Edits'), disabled: true}, - {id: 'texts', title: Ox._('Texts'), disabled: true} + {id: 'items', title: Ox._(pandora.site.itemName.plural)}, + {id: 'edits', title: Ox._('Edits'), disabled: pandora.user.level != 'admin'} + {id: 'texts', title: Ox._('Texts'), disabled: pandora.user.level != 'admin'} ], value: pandora.user.ui.section }).css({ diff --git a/static/js/pandora/utils.js b/static/js/pandora/utils.js index a707535b..e8cde7ad 100644 --- a/static/js/pandora/utils.js +++ b/static/js/pandora/utils.js @@ -785,6 +785,16 @@ pandora.getItem = function(state, str, callback) { callback(); } }); + } else if (state.type == 'edits') { + pandora.api.getEdit({id: str}, function(result) { + if (result.status.code == 200) { + state.item = result.data.id; + callback(); + } else { + state.item = ''; + callback(); + } + }); } else { callback(); } @@ -1444,9 +1454,10 @@ pandora.selectList = function() { }); } } else { - var id = pandora.user.ui[pandora.user.ui.section.slice(0,-1)]; + var id = pandora.user.ui[pandora.user.ui.section.slice(0,-1)], + section = Ox.toTitleCase(pandora.user.ui.section.slice(0, -1)); if (id) { - pandora.api.getText({id: id}, function(result) { + pandora.api['edit' + section]({id: id}, function(result) { var folder; if (result.data.id) { folder = result.data.status == 'featured' ? 'featured' : (