diff --git a/README b/README
index a82e8937..fb610f3d 100644
--- a/README
+++ b/README
@@ -27,9 +27,16 @@ Install rabbitmq and carrot:
update BROKER_* settings in local_settings.py:
+get current oxjs
+ cd static
+ bzr branch http://code.0x2620.org/oxjs
+
+Database:
+ with postresql install python-psycopg2
+
Development:
we are using django, http://docs.djangoproject.com/en/dev/
-
+
Nginx setup:
sudo apt-get install nginx
diff --git a/pandora/api/views.py b/pandora/api/views.py
index b9e68a90..1f7ab164 100644
--- a/pandora/api/views.py
+++ b/pandora/api/views.py
@@ -17,11 +17,7 @@ from django.shortcuts import render_to_response, get_object_or_404, get_list_or_
from django.template import RequestContext
from django.conf import settings
-try:
- import simplejson as json
-except ImportError:
- from django.utils import simplejson as json
-
+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 ox.django.http import HttpFileResponse
@@ -31,15 +27,18 @@ import models
import utils
import tasks
-from user.models import getUserJSON
-from user.views import api_login, api_logout, api_register, api_contact, api_recover, api_preferences, api_findUser
+from pandora.user.models import getUserJSON
+from pandora.user.views import api_login, api_logout, api_register, api_contact, api_recover, api_preferences, api_findUser
-from archive.views import api_update, api_upload, api_editFile, api_encodingProfile
+from pandora.archive.views import api_update, api_upload, api_editFile, api_encodingProfile
-from archive.models import File
-from archive import extract
+from pandora.archive.models import File
+from pandora.archive import extract
-from item.views import *
+from pandora.item.views import *
+from pandora.itemlist.views import *
+from pandora.place.views import *
+from pandora.date.views import *
def api(request):
if request.META['REQUEST_METHOD'] == "OPTIONS":
@@ -73,6 +72,18 @@ def api_api(request):
actions.sort()
return render_to_json_response(json_response({'actions': actions}))
+def api_apidoc(request):
+ '''
+ returns array of actions with documentation
+ '''
+ actions = globals().keys()
+ actions = map(lambda a: a[4:], filter(lambda a: a.startswith('api_'), actions))
+ actions.sort()
+ docs = {}
+ for f in actions:
+ docs[f] = get_api_doc(f)
+ return render_to_json_response(json_response({'actions': docs}))
+
def api_hello(request):
'''
return {'status': {'code': int, 'text': string},
@@ -93,10 +104,9 @@ def api_error(request):
success = error_is_success
return render_to_json_response({})
-def apidoc(request):
- '''
- this is used for online documentation at http://127.0.0.1:8000/api/
- '''
+def get_api_doc(f):
+ f = 'api_' + f
+
import sys
def trim(docstring):
if not docstring:
@@ -123,12 +133,19 @@ def apidoc(request):
# Return a single string:
return '\n'.join(trimmed)
+ return trim(globals()[f].__doc__)
+
+def apidoc(request):
+ '''
+ this is used for online documentation at http://127.0.0.1:8000/api/
+ '''
+
functions = filter(lambda x: x.startswith('api_'), globals().keys())
api = []
for f in sorted(functions):
api.append({
'name': f[4:],
- 'doc': trim(globals()[f].__doc__).replace('\n', '
\n')
+ 'doc': get_api_doc(f[4:]).replace('\n', '
\n')
})
context = RequestContext(request, {'api': api,
'sitename': settings.SITENAME,})
diff --git a/pandora/date/__init__.py b/pandora/date/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/pandora/date/admin.py b/pandora/date/admin.py
new file mode 100644
index 00000000..b1141f0a
--- /dev/null
+++ b/pandora/date/admin.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+
+from django.contrib import admin
+
+import models
+
+
+class DateAdmin(admin.ModelAdmin):
+ search_fields = ['name']
+admin.site.register(models.Date, DateAdmin)
+
diff --git a/pandora/date/managers.py b/pandora/date/managers.py
new file mode 100644
index 00000000..9a7e4e99
--- /dev/null
+++ b/pandora/date/managers.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+import re
+
+from ox.utils import json
+from django.contrib.auth.models import User
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Q, Manager
+
+import models
+
+class DateManager(Manager):
+ def get_query_set(self):
+ return super(DateManager, self).get_query_set()
+
+ def find(self, q=''):
+ qs = self.get_query_set()
+ qs = qs.filter(Q(name_find__icontains=q))
+ return qs
diff --git a/pandora/date/models.py b/pandora/date/models.py
new file mode 100644
index 00000000..ac44ed09
--- /dev/null
+++ b/pandora/date/models.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+from __future__ import division, with_statement
+
+from django.db import models
+from django.db.models import Q
+from django.conf import settings
+
+from ox.django import fields
+
+import managers
+
+class Date(models.Model):
+ '''
+ Dates are dates in time that can be once or recurring,
+ From Mondays to Spring to 1989 to Roman Empire
+ '''
+ name = models.CharField(null=True, max_length=255, unique=True)
+ name_sort = models.CharField(null=True, max_length=255, unique=True)
+ name_find = models.TextField(default='', editable=True)
+ wikipediaId = models.CharField(max_length=1000, blank=True)
+
+ objects = managers.DateManager()
+
+ class Meta:
+ ordering = ('name_sort', )
+
+ #FIXME: how to deal with aliases
+ aliases = fields.TupleField(default=[])
+
+ #once|year|week|day
+ recurring = models.IntegerField(default=0)
+
+ #start yyyy-mm-dd|mm-dd|dow 00:00|00:00
+ #end yyyy-mm-dd|mm-dd|dow 00:00|00:01
+ start = models.CharField(null=True, max_length=255)
+ end = models.CharField(null=True, max_length=255)
+
+ def save(self, *args, **kwargs):
+ if not self.name_sort:
+ self.name_sort = self.name
+ self.name_find = self.name + '||'.join(self.aliases)
+ super(Date, self).save(*args, **kwargs)
+
diff --git a/pandora/date/tests.py b/pandora/date/tests.py
new file mode 100644
index 00000000..2247054b
--- /dev/null
+++ b/pandora/date/tests.py
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/pandora/date/views.py b/pandora/date/views.py
new file mode 100644
index 00000000..378476f5
--- /dev/null
+++ b/pandora/date/views.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+from __future__ import division
+import os.path
+import re
+from datetime import datetime
+from urllib2 import unquote
+import mimetypes
+
+from django import forms
+from django.core.paginator import Paginator
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.db.models import Q, Avg, Count, Sum
+from django.http import HttpResponse, Http404
+from django.shortcuts import render_to_response, get_object_or_404, get_list_or_404, redirect
+from django.template import RequestContext
+from django.conf import settings
+
+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 ox.django.http import HttpFileResponse
+import ox
+
+import models
+
+@login_required_json
+def api_addDate(request):
+ data = json.loads(request.POST['data'])
+ if models.Date.filter(name=data['name']).count() == 0:
+ place = models.Date(name = data['name'])
+ place.save()
+ response = json_response(status=200, text='created')
+ else:
+ response = json_response(status=403, text='place name exists')
+ return render_to_json_response(response)
+
+@login_required_json
+def api_editDate(request):
+ '''
+ param data
+ {
+ 'id': dateid,
+ 'date': dict
+ }
+ date contains key/value pairs with place propterties
+ '''
+ data = json.loads(request.POST['data'])
+ Date = get_object_or_404_json(models.Date, pk=data['id'])
+ if Date.editable(request.user):
+ conflict = False
+ names = [data['date']['name']] + data['date']['aliases']
+ for name in names: #FIXME: also check aliases!
+ if models.Date.filter(name=data['name']).exclude(id=Date.id).count() != 0:
+ conflict = True
+ if not conflict:
+ for key in data['date']:
+ setattr(Date, key, data['date'][key])
+ Date.save()
+ response = json_response(status=200, text='updated')
+ else:
+ response = json_response(status=403, text='Date name/alias conflict')
+ else:
+ response = json_response(status=403, text='permission denied')
+ return render_to_json_response(response)
+
+@login_required_json
+def api_removeDate(request):
+ response = json_response(status=501, text='not implemented')
+ return render_to_json_response(response)
+
+def api_findDate(request):
+ '''
+ param data
+ {'query': query, 'sort': array, 'range': array}
+
+ query: query object, more on query syntax at
+ https://wiki.0x2620.org/wiki/pandora/QuerySyntax
+ sort: array of key, operator dics
+ [
+ {
+ key: "year",
+ operator: "-"
+ },
+ {
+ key: "director",
+ operator: ""
+ }
+ ]
+ range: result range, array [from, to]
+
+ with keys, items is list of dicts with requested properties:
+ return {'status': {'code': int, 'text': string},
+ 'data': {items: array}}
+
+Positions
+ param data
+ {'query': query, 'ids': []}
+
+ query: query object, more on query syntax at
+ https://wiki.0x2620.org/wiki/pandora/QuerySyntax
+ ids: ids of dates for which positions are required
+ '''
+ data = json.loads(request.POST['data'])
+ response = json_response(status=200, text='ok')
+ response['data']['places'] = []
+ #FIXME: add coordinates to limit search
+ for p in Dates.objects.find(data['query']):
+ response['data']['dates'].append(p.json())
+ return render_to_json_response(response)
+
diff --git a/pandora/fixtures/0xdb_properties.json b/pandora/fixtures/0xdb_properties.json
new file mode 100644
index 00000000..d6b214c7
--- /dev/null
+++ b/pandora/fixtures/0xdb_properties.json
@@ -0,0 +1,970 @@
+[
+ {
+ "pk": 1,
+ "model": "item.property",
+ "fields": {
+ "sort": "title",
+ "admin": false,
+ "group": false,
+ "name": "title",
+ "title": "Title",
+ "default": true,
+ "align": "left",
+ "totals": false,
+ "autocomplete": true,
+ "width": 180,
+ "removable": true,
+ "operator": "",
+ "position": 0,
+ "array": false,
+ "type": "title",
+ "find": true
+ }
+ },
+ {
+ "pk": 10,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": true,
+ "name": "director",
+ "title": "Director",
+ "default": true,
+ "align": "left",
+ "totals": false,
+ "autocomplete": true,
+ "width": 180,
+ "removable": true,
+ "operator": "",
+ "position": 1,
+ "array": true,
+ "type": "person",
+ "find": true
+ }
+ },
+ {
+ "pk": 28,
+ "model": "item.property",
+ "fields": {
+ "sort": "sring",
+ "admin": false,
+ "group": true,
+ "name": "country",
+ "title": "Country",
+ "default": true,
+ "align": "left",
+ "totals": false,
+ "autocomplete": true,
+ "width": 120,
+ "removable": true,
+ "operator": "",
+ "position": 2,
+ "array": true,
+ "type": "string",
+ "find": true
+ }
+ },
+ {
+ "pk": 15,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": true,
+ "name": "year",
+ "title": "Year",
+ "default": true,
+ "align": "right",
+ "totals": false,
+ "autocomplete": true,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 3,
+ "array": false,
+ "type": "string",
+ "find": true
+ }
+ },
+ {
+ "pk": 26,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": true,
+ "name": "language",
+ "title": "Language",
+ "default": true,
+ "align": "left",
+ "totals": false,
+ "autocomplete": true,
+ "width": 120,
+ "removable": true,
+ "operator": "",
+ "position": 4,
+ "array": true,
+ "type": "string",
+ "find": true
+ }
+ },
+ {
+ "pk": 37,
+ "model": "item.property",
+ "fields": {
+ "sort": "integer",
+ "admin": false,
+ "group": false,
+ "name": "runtime",
+ "title": "Runtime",
+ "default": false,
+ "align": "right",
+ "totals": true,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 5,
+ "array": false,
+ "type": "integer",
+ "find": false
+ }
+ },
+ {
+ "pk": 29,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": false,
+ "name": "writer",
+ "title": "Writer",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": true,
+ "width": 180,
+ "removable": true,
+ "operator": "",
+ "position": 6,
+ "array": true,
+ "type": "person",
+ "find": true
+ }
+ },
+ {
+ "pk": 6,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": false,
+ "name": "producer",
+ "title": "Producer",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": true,
+ "width": 180,
+ "removable": true,
+ "operator": "",
+ "position": 7,
+ "array": true,
+ "type": "person",
+ "find": true
+ }
+ },
+ {
+ "pk": 13,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": false,
+ "name": "cinematographer",
+ "title": "Cinematographer",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": true,
+ "width": 180,
+ "removable": true,
+ "operator": "",
+ "position": 8,
+ "array": true,
+ "type": "person",
+ "find": true
+ }
+ },
+ {
+ "pk": 34,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": false,
+ "name": "editor",
+ "title": "Editor",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": true,
+ "width": 180,
+ "removable": true,
+ "operator": "",
+ "position": 9,
+ "array": true,
+ "type": "person",
+ "find": true
+ }
+ },
+ {
+ "pk": 33,
+ "model": "item.property",
+ "fields": {
+ "sort": "length",
+ "admin": false,
+ "group": false,
+ "name": "actors",
+ "title": "Actors",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": true,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 10,
+ "array": true,
+ "type": "role",
+ "find": true
+ }
+ },
+ {
+ "pk": 46,
+ "model": "item.property",
+ "fields": {
+ "sort": "",
+ "admin": false,
+ "group": false,
+ "name": "name",
+ "title": "Name",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": true,
+ "width": 180,
+ "removable": true,
+ "operator": "",
+ "position": 11,
+ "array": true,
+ "type": "name",
+ "find": true
+ }
+ },
+ {
+ "pk": 45,
+ "model": "item.property",
+ "fields": {
+ "sort": "",
+ "admin": false,
+ "group": false,
+ "name": "character",
+ "title": "Character",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": true,
+ "width": 180,
+ "removable": true,
+ "operator": "",
+ "position": 11,
+ "array": true,
+ "type": "character",
+ "find": true
+ }
+ },
+ {
+ "pk": 16,
+ "model": "item.property",
+ "fields": {
+ "sort": "length",
+ "admin": false,
+ "group": true,
+ "name": "genre",
+ "title": "Genre",
+ "default": true,
+ "align": "left",
+ "totals": false,
+ "autocomplete": true,
+ "width": 120,
+ "removable": true,
+ "operator": "",
+ "position": 11,
+ "array": true,
+ "type": "string",
+ "find": true
+ }
+ },
+ {
+ "pk": 11,
+ "model": "item.property",
+ "fields": {
+ "sort": "length",
+ "admin": false,
+ "group": false,
+ "name": "keywords",
+ "title": "Keywords",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 12,
+ "array": true,
+ "type": "string",
+ "find": false
+ }
+ },
+ {
+ "pk": 31,
+ "model": "item.property",
+ "fields": {
+ "sort": "length",
+ "admin": false,
+ "group": false,
+ "name": "summary",
+ "title": "Summary",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 13,
+ "array": false,
+ "type": "title",
+ "find": true
+ }
+ },
+ {
+ "pk": 25,
+ "model": "item.property",
+ "fields": {
+ "sort": "length",
+ "admin": false,
+ "group": false,
+ "name": "trivia",
+ "title": "Trivia",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 14,
+ "array": false,
+ "type": "title",
+ "find": false
+ }
+ },
+ {
+ "pk": 32,
+ "model": "item.property",
+ "fields": {
+ "sort": "date",
+ "admin": false,
+ "group": false,
+ "name": "releasedate",
+ "title": "Releasedate",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 15,
+ "array": false,
+ "type": "date",
+ "find": false
+ }
+ },
+ {
+ "pk": 30,
+ "model": "item.property",
+ "fields": {
+ "sort": "float",
+ "admin": false,
+ "group": false,
+ "name": "budget",
+ "title": "Budget",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 16,
+ "array": false,
+ "type": "float",
+ "find": false
+ }
+ },
+ {
+ "pk": 23,
+ "model": "item.property",
+ "fields": {
+ "sort": "float",
+ "admin": false,
+ "group": false,
+ "name": "gross",
+ "title": "Gross",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 17,
+ "array": false,
+ "type": "float",
+ "find": false
+ }
+ },
+ {
+ "pk": 27,
+ "model": "item.property",
+ "fields": {
+ "sort": "float",
+ "admin": false,
+ "group": false,
+ "name": "profit",
+ "title": "Profit",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 18,
+ "array": false,
+ "type": "float",
+ "find": false
+ }
+ },
+ {
+ "pk": 3,
+ "model": "item.property",
+ "fields": {
+ "sort": "integer",
+ "admin": false,
+ "group": false,
+ "name": "rating",
+ "title": "Rating",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 19,
+ "array": false,
+ "type": "integer",
+ "find": false
+ }
+ },
+ {
+ "pk": 24,
+ "model": "item.property",
+ "fields": {
+ "sort": "integer",
+ "admin": false,
+ "group": false,
+ "name": "votes",
+ "title": "Votes",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 20,
+ "array": false,
+ "type": "integer",
+ "find": false
+ }
+ },
+ {
+ "pk": 17,
+ "model": "item.property",
+ "fields": {
+ "sort": "float",
+ "admin": false,
+ "group": false,
+ "name": "aspectratio",
+ "title": "Aspectratio",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 21,
+ "array": false,
+ "type": "faction",
+ "find": false
+ }
+ },
+ {
+ "pk": 8,
+ "model": "item.property",
+ "fields": {
+ "sort": "float",
+ "admin": true,
+ "group": false,
+ "name": "duration",
+ "title": "Duration",
+ "default": false,
+ "align": "right",
+ "totals": true,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 22,
+ "array": false,
+ "type": "float",
+ "find": false
+ }
+ },
+ {
+ "pk": 7,
+ "model": "item.property",
+ "fields": {
+ "sort": "color",
+ "admin": false,
+ "group": false,
+ "name": "color",
+ "title": "Color",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "",
+ "position": 23,
+ "array": false,
+ "type": "color",
+ "find": false
+ }
+ },
+ {
+ "pk": 4,
+ "model": "item.property",
+ "fields": {
+ "sort": "integer",
+ "admin": false,
+ "group": false,
+ "name": "saturation",
+ "title": "Saturation",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 24,
+ "array": false,
+ "type": "integer",
+ "find": false
+ }
+ },
+ {
+ "pk": 12,
+ "model": "item.property",
+ "fields": {
+ "sort": "integer",
+ "admin": false,
+ "group": false,
+ "name": "brightness",
+ "title": "Brightness",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 25,
+ "array": false,
+ "type": "integer",
+ "find": false
+ }
+ },
+ {
+ "pk": 22,
+ "model": "item.property",
+ "fields": {
+ "sort": "integer",
+ "admin": false,
+ "group": false,
+ "name": "volume",
+ "title": "Volume",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 26,
+ "array": false,
+ "type": "integer",
+ "find": false
+ }
+ },
+ {
+ "pk": 41,
+ "model": "item.property",
+ "fields": {
+ "sort": null,
+ "admin": false,
+ "group": false,
+ "name": "clips",
+ "title": "Clips",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 27,
+ "array": false,
+ "type": null,
+ "find": false
+ }
+ },
+ {
+ "pk": 42,
+ "model": "item.property",
+ "fields": {
+ "sort": null,
+ "admin": false,
+ "group": false,
+ "name": "cuts",
+ "title": "Cuts",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 28,
+ "array": false,
+ "type": null,
+ "find": false
+ }
+ },
+ {
+ "pk": 39,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": false,
+ "name": "cutsperminute",
+ "title": "Cuts per minute",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 29,
+ "array": false,
+ "type": "integer",
+ "find": false
+ }
+ },
+ {
+ "pk": 14,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": false,
+ "name": "words",
+ "title": "Words",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 30,
+ "array": false,
+ "type": "title",
+ "find": false
+ }
+ },
+ {
+ "pk": 40,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": false,
+ "name": "wordsperminute",
+ "title": "Words per minute",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 31,
+ "array": false,
+ "type": "integer",
+ "find": false
+ }
+ },
+ {
+ "pk": 38,
+ "model": "item.property",
+ "fields": {
+ "sort": "integer",
+ "admin": false,
+ "group": false,
+ "name": "resolution",
+ "title": "Resolution",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 32,
+ "array": true,
+ "type": "integer",
+ "find": false
+ }
+ },
+ {
+ "pk": 20,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": false,
+ "name": "pixels",
+ "title": "Pixels",
+ "default": false,
+ "align": "right",
+ "totals": true,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 33,
+ "array": false,
+ "type": "integer",
+ "find": false
+ }
+ },
+ {
+ "pk": 21,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": true,
+ "group": false,
+ "name": "size",
+ "title": "Size",
+ "default": false,
+ "align": "right",
+ "totals": true,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 34,
+ "array": false,
+ "type": "title",
+ "find": false
+ }
+ },
+ {
+ "pk": 19,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": false,
+ "name": "bitrate",
+ "title": "Bitrate",
+ "default": false,
+ "align": "right",
+ "totals": false,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 35,
+ "array": false,
+ "type": "title",
+ "find": false
+ }
+ },
+ {
+ "pk": 2,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": true,
+ "group": false,
+ "name": "files",
+ "title": "Files",
+ "default": false,
+ "align": "right",
+ "totals": true,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 36,
+ "array": false,
+ "type": "title",
+ "find": false
+ }
+ },
+ {
+ "pk": 18,
+ "model": "item.property",
+ "fields": {
+ "sort": "string",
+ "admin": false,
+ "group": false,
+ "name": "filename",
+ "title": "Filename",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": false,
+ "width": 180,
+ "removable": true,
+ "operator": "",
+ "position": 37,
+ "array": false,
+ "type": "title",
+ "find": false
+ }
+ },
+ {
+ "pk": 36,
+ "model": "item.property",
+ "fields": {
+ "sort": "date",
+ "admin": false,
+ "group": false,
+ "name": "published",
+ "title": "Published",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 38,
+ "array": false,
+ "type": "date",
+ "find": false
+ }
+ },
+ {
+ "pk": 9,
+ "model": "item.property",
+ "fields": {
+ "sort": "date",
+ "admin": false,
+ "group": false,
+ "name": "modified",
+ "title": "Modified",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": false,
+ "width": 90,
+ "removable": true,
+ "operator": "-",
+ "position": 39,
+ "array": false,
+ "type": "date",
+ "find": false
+ }
+ },
+ {
+ "pk": 5,
+ "model": "item.property",
+ "fields": {
+ "sort": "date",
+ "admin": false,
+ "group": false,
+ "name": "popularity",
+ "title": "Popularity",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": false,
+ "width": 60,
+ "removable": true,
+ "operator": "-",
+ "position": 40,
+ "array": false,
+ "type": "date",
+ "find": false
+ }
+ },
+ {
+ "pk": 35,
+ "model": "item.property",
+ "fields": {
+ "sort": null,
+ "admin": false,
+ "group": false,
+ "name": "dialog",
+ "title": "Dialog",
+ "default": false,
+ "align": "left",
+ "totals": false,
+ "autocomplete": false,
+ "width": 180,
+ "removable": true,
+ "operator": "",
+ "position": 100,
+ "array": false,
+ "type": "title",
+ "find": true
+ }
+ }
+]
\ No newline at end of file
diff --git a/pandora/item/admin.py b/pandora/item/admin.py
index bd8296b5..8bf97d83 100644
--- a/pandora/item/admin.py
+++ b/pandora/item/admin.py
@@ -5,22 +5,12 @@ from django.contrib import admin
import models
-
-class BinAdmin(admin.ModelAdmin):
- search_fields = ['name', 'title']
-admin.site.register(models.Bin, BinAdmin)
+class ItemAdmin(admin.ModelAdmin):
+ search_fields = ['itemId', 'data', 'external_data']
+admin.site.register(models.Item, ItemAdmin)
class PropertyAdmin(admin.ModelAdmin):
search_fields = ['name', 'title']
admin.site.register(models.Property, PropertyAdmin)
-class PlaceAdmin(admin.ModelAdmin):
- search_fields = ['name']
-admin.site.register(models.Place, PlaceAdmin)
-
-
-class EventAdmin(admin.ModelAdmin):
- search_fields = ['name']
-admin.site.register(models.Event, EventAdmin)
-
diff --git a/pandora/item/managers.py b/pandora/item/managers.py
index 59d1b230..b0a5ebe4 100644
--- a/pandora/item/managers.py
+++ b/pandora/item/managers.py
@@ -3,7 +3,7 @@
import re
from datetime import datetime
from urllib2 import unquote
-import json
+from ox.utils import json
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
diff --git a/pandora/item/models.py b/pandora/item/models.py
index 27b9c034..277dbda7 100644
--- a/pandora/item/models.py
+++ b/pandora/item/models.py
@@ -29,99 +29,23 @@ import load
import utils
from archive import extract
+from layer.models import Layer
+from person.models import getPersonSort, Person
-class Bin(models.Model):
- class Meta:
- ordering = ('position', )
-
- name = models.CharField(null=True, max_length=255, unique=True)
- title = models.CharField(null=True, max_length=255)
- #text, string, string from list(fixme), event, place, person
- type = models.CharField(null=True, max_length=255)
- position = models.IntegerField(default=0)
-
- overlapping = models.BooleanField(default=True)
- enabled = models.BooleanField(default=True)
-
- enabled = models.BooleanField(default=True)
- public = models.BooleanField(default=True) #false=users only see there own bins
- subtitle = models.BooleanField(default=True) #bis can be displayed as subtitle, only one bin
-
- find = models.BooleanField(default=True)
- #words / item duration(wpm), total words, cuts per minute, cuts, number of layers, number of layers/duration
- sort = models.CharField(null=True, max_length=255)
-
- def properties(self):
- p = {}
- if self.find:
- p[self.name] = {'type': 'bin', 'find': True}
- if self.sort:
- print 'FIXME: need to add sort stuff'
- return p
-
-properties = {
- 'title': {'type': 'string', 'sort': 'title', 'find': True},
- 'director': {'type': 'person', 'array': True, 'sort': 'string', 'find': True, 'group': True},
- 'country': {'type': 'string', 'array': True, 'sort': 'sring', 'find': True, 'group': True},
- 'year': {'type': 'string', 'sort': 'string', 'find': True, 'group': True},
- 'language': {'type': 'string', 'array': True, 'sort': 'string', 'find': True, 'group': True},
- 'runtime': {'type': 'integer', 'sort': 'integer'},
- 'writer': {'type': 'person', 'array': True, 'sort': 'string', 'find': True},
- 'producer': {'type': 'person', 'array': True, 'sort': 'string', 'find': True},
- 'cinematographer': {'type': 'person', 'array': True, 'sort': 'string', 'find': True},
- 'editor': {'type': 'person', 'array': True, 'sort': 'string', 'find': True},
- 'actors': {'type': 'role', 'array': True, 'sort': 'length', 'find': True},
- 'genre': {'type': 'string', 'array': True, 'sort': 'length', 'find': True, 'group': True},
- 'keywords': {'type': 'string', 'array': True, 'sort': 'length', 'find': True},
- 'summary': {'type': 'title', 'sort': 'length', 'find': True},
- 'trivia': {'type': 'title', 'sort': 'length', 'find': True},
- 'releasedate': {'type': 'date', 'sort': 'date', 'find': True},
- 'runtime': {'type': 'integer', 'sort': 'integer', 'totals': True},
-
- 'budget': {'type': 'float', 'sort': 'float'},
- 'gross': {'type': 'float', 'sort': 'float'},
- 'profit': {'type': 'float', 'sort': 'float'},
-
- 'rating': {'type': 'integer', 'sort': 'integer'},
- 'votes': {'type': 'integer', 'sort': 'integer'},
- 'published': {'type': 'date', 'sort': 'date'},
- 'modified': {'type': 'date', 'sort': 'date'},
- 'popularity': {'type': 'date', 'sort': 'date'},
-
- #file properties // are those even configurable? think not
- 'aspectratio': {'type': 'faction', 'sort': 'float'},
- 'duration': {'type': 'float', 'sort': 'float', 'totals': True, "admin": True},
- 'color': {'type': 'color', 'sort': 'color'},
- 'saturation': {'type': 'integer', 'sort': 'integer'},
- 'brightness': {'type': 'integer', 'sort': 'integer'},
- 'volume': {'type': 'integer', 'sort': 'integer'},
- 'resolution': {'type': 'integer', 'array': True, 'sort': 'integer'}, #FIXME
- 'pixels': {'type': 'integer', 'sort': 'string', 'totals': True},
- 'size': {'type': 'title', 'sort': 'string', 'totals': True, 'admin': True},
- 'bitrate': {'type': 'title', 'sort': 'string'},
- 'files': {'type': 'title', 'sort': 'string', 'totals': True, 'admin': True},
- 'filename': {'type': 'title', 'sort': 'string'},
-
- #Layer properties // those need to be defined with bins
- 'dialog': {'type': 'title', 'find': True},
- #'clips': {'type': 'title', 'sort': 'string'},
- #'cuts': {'type': 'title', 'sort': 'string'},
- 'cutsperminute': {'type': 'integer', 'title': 'Cuts per minute', 'sort': 'string'},
- 'words': {'type': 'title', 'sort': 'string'},
- 'wordsperminute': {'type': 'integer','title': 'Words per minute', 'sort': 'string'},
-}
def siteJson():
r = {}
r['findKeys'] = [{"id": "all", "title": "All"}]
- for k in properties:
- i = properties[k]
- if i.get('find', False):
- f = {"id": k, "title": i.get('title', k.capitalize())}
- if i.get('autocomplete', False):
- f['autocomplete'] = True
+ for p in Property.objects.all():
+ if p.find:
+ title = p.title
+ if not title:
+ title = p.name.capitalize()
+ f = {"id": p.name, "title": title}
+ f['autocomplete'] = p.autocomplete
r['findKeys'].append(f)
- r['groups'] = filter(lambda k: properties[k].get('group', False), properties.keys())
+
+ r['groups'] = [p.name for p in Property.objects.filter(group=True)]
r['itemViews'] = [
{"id": "info", "title": "Info"},
{"id": "statistics", "title": "Statistics"},
@@ -155,35 +79,34 @@ def siteJson():
{"id": "featured", "title": "Featured Lists"}
]
r['sortKeys'] = []
- for k in properties:
- i = properties[k]
- if 'sort' in i:
- f = {
- "id": k,
- "title": i.get('title', k.capitalize()),
- "operator": i.get('operator', ''),
- "align": i.get('align', 'left'),
- "width": i.get('width', 180),
- }
- if not i.get('removable', True):
- f['removable'] = False
- r['sortKeys'].append(f)
+ for p in Property.objects.exclude(sort=''):
+ title = p.title
+ if not title:
+ title = p.name.capitalize()
+
+ f = {
+ "id": p.name,
+ "title": title,
+ "operator": p.operator,
+ "align": p.align,
+ "width": p.width,
+ }
+ if not p.removable:
+ f['removable'] = False
+ r['sortKeys'].append(f)
+ r['sortKeys'].append([{"id": "id", "title": "ID", "operator": "", "align": "left", "width": 90}])
r['totals'] = [{"id": "items"}]
- for k in properties:
- i = properties[k]
- if i.get('totals', False):
- f = {"id": k}
- if i.get('admin', False):
- f['admin'] = True
- r['totals'].append(f)
+ for p in Property.objects.filter(totals=True):
+ f = {'id': p.name, 'admin': p.admin}
+ r['totals'].append(f)
#FIXME: defaults should also be populated from properties
r["user"] = {
"group": "guest",
"preferences": {},
"ui": {
- "columns": ["id", "title", "director", "country", "year", "language", "genre"],
+ "columns": ["id"] + [p.name for p in Property.objects.filter(default=True)],
"findQuery": {"conditions": [], "operator": ""},
"groupsQuery": {"conditions": [], "operator": "|"},
"groupsSize": 128,
@@ -196,10 +119,8 @@ def siteJson():
"showInfo": True,
"showLists": True,
"showMovies": True,
- "sort": [
- {"key": "director", "operator": ""}
- ],
- "theme": "classic"
+ "sort": settings.DEFAULT_SORT,
+ "theme": settings.DEFAULT_THEME
},
"username": ""
}
@@ -252,16 +173,25 @@ def getItem(info):
class Property(models.Model):
class Meta:
ordering = ('position', )
-
+ verbose_name_plural = "Properties"
+
name = models.CharField(null=True, max_length=255, unique=True)
- title = models.CharField(null=True, max_length=255)
+ title = models.CharField(null=True, max_length=255, blank=True)
#text, string, string from list(fixme), event, place, person
type = models.CharField(null=True, max_length=255)
array = models.BooleanField(default=False)
position = models.IntegerField(default=0)
+ width = models.IntegerField(default=180)
+ align = models.CharField(null=True, max_length=255, default='left')
+ operator = models.CharField(null=True, max_length=5, default='', blank=True)
+ default = models.BooleanField('Enabled by default', default=False)
+ removable = models.BooleanField(default=True)
#sort values: title, string, integer, float, date
- sort = models.CharField(null=True, max_length=255)
+ sort = models.CharField(null=True, max_length=255, blank=True)
+ find = models.BooleanField(default=False)
+ autocomplete = models.BooleanField(default=False)
+ group = models.BooleanField(default=False)
totals = models.BooleanField(default=False)
admin = models.BooleanField(default=False)
@@ -279,6 +209,11 @@ class Property(models.Model):
j[key] = value
return j
+ def save(self, *args, **kwargs):
+ if not self.title:
+ self.title = self.name.capitalize()
+ super(Property, self).save(*args, **kwargs)
+
class Item(models.Model):
person_keys = ('director', 'writer', 'producer', 'editor', 'cinematographer', 'actor', 'character')
facet_keys = person_keys + ('country', 'language', 'genre', 'keyword')
@@ -905,191 +840,10 @@ class Facet(models.Model):
self.value_sort = self.value
super(Facet, self).save(*args, **kwargs)
-def getPersonSort(name):
- person, created = Person.objects.get_or_create(name=name)
- name_sort = unicodedata.normalize('NFKD', person.name_sort)
- return name_sort
-
-class Person(models.Model):
- name = models.CharField(max_length=200)
- name_sort = models.CharField(max_length=200)
-
- #FIXME: how to deal with aliases
- aliases = fields.TupleField(default=[])
-
- imdbId = models.CharField(max_length=7, blank=True)
- wikipediaId = models.CharField(max_length=1000, blank=True)
-
- class Meta:
- ordering = ('name_sort', )
-
- def __unicode__(self):
- return self.name
-
- def save(self, *args, **kwargs):
- if not self.name_sort:
- self.name_sort = ox.normalize.canonicalName(self.name)
- super(Person, self).save(*args, **kwargs)
-
- def get_or_create(model, name, imdbId=None):
- if imdbId:
- q = model.objects.filter(name=name, imdbId=imdbId)
- else:
- q = model.objects.all().filter(name=name)
- if q.count() > 0:
- o = q[0]
- else:
- o = model.objects.create(name=name)
- if imdbId:
- o.imdbId = imdbId
- o.save()
- return o
- get_or_create = classmethod(get_or_create)
-
- def json(self):
- return self.name
-
-class Place(models.Model):
- '''
- Places are named locations, they should have geographical information attached to them.
- '''
-
- name = models.CharField(max_length=200, unique=True)
- name_sort = models.CharField(max_length=200)
- manual = models.BooleanField(default=False)
- items = models.ManyToManyField(Item, related_name='places')
- wikipediaId = models.CharField(max_length=1000, blank=True)
-
- #FIXME: how to deal with aliases
- aliases = fields.TupleField(default=[])
-
- #FIXME: geo data, is this good enough?
- lat_sw = models.FloatField(default=0)
- lng_sw = models.FloatField(default=0)
- lat_ne = models.FloatField(default=0)
- lng_ne = models.FloatField(default=0)
- lat_center = models.FloatField(default=0)
- lng_center = models.FloatField(default=0)
- area = models.FloatField(default=-1)
-
- class Meta:
- ordering = ('name_sort', )
-
- def __unicode__(self):
- return self.name
-
- def json(self):
- return self.name
-
- def save(self, *args, **kwargs):
- if not self.name_sort:
- self.name_sort = self.name
-
- #update center
- self.lat_center = ox.location.center(self.lat_sw, self.lat_ne)
- self.lng_center = ox.location.center(self.lng_sw, self.lng_ne)
-
- #update area
- self.area = location.area(self.lat_sw, self.lng_sw, self.lat_ne, self.lng_ne)
-
- super(Place, self).save(*args, **kwargs)
-
-class Event(models.Model):
- '''
- Events are events in time that can be once or recurring,
- From Mondays to Spring to 1989 to Roman Empire
- '''
- name = models.CharField(null=True, max_length=255, unique=True)
- name_sort = models.CharField(null=True, max_length=255, unique=True)
- wikipediaId = models.CharField(max_length=1000, blank=True)
-
- class Meta:
- ordering = ('name_sort', )
-
- #FIXME: how to deal with aliases
- aliases = fields.TupleField(default=[])
-
- #once|year|week|day
- recurring = models.IntegerField(default=0)
-
- #start yyyy-mm-dd|mm-dd|dow 00:00|00:00
- #end yyyy-mm-dd|mm-dd|dow 00:00|00:01
- start = models.CharField(null=True, max_length=255)
- end = models.CharField(null=True, max_length=255)
-
- def save(self, *args, **kwargs):
- if not self.name_sort:
- self.name_sort = self.name
- super(Event, self).save(*args, **kwargs)
-
class ReviewWhitelist(models.Model):
name = models.CharField(max_length=255, unique=True)
url = models.CharField(max_length=255, unique=True)
-class List(models.Model):
- class Meta:
- unique_together = ("user", "name")
-
- 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)
- items = models.ManyToManyField(Item, related_name='lists', through='ListItem')
-
- def add(self, item):
- q = self.items.filter(id=item.id)
- if q.count() == 0:
- l = ListItem()
- l.list = self
- l.item = item
- l.save()
-
- def remove(self, item):
- self.ListItem.objects.all().filter(item=item, list=self).delete()
-
- def __unicode__(self):
- return u'%s (%s)' % (self.title, unicode(self.user))
-
- def editable(self, user):
- #FIXME: make permissions work
- if self.user == user or user.has_perm('Ox.admin'):
- return True
- return False
-
-class ListItem(models.Model):
- created = models.DateTimeField(auto_now_add=True)
- modified = models.DateTimeField(auto_now=True)
- list = models.ForeignKey(List)
- item = models.ForeignKey(Item)
-
- def __unicode__(self):
- return u'%s in %s' % (unicode(self.item), unicode(self.list))
-
-class Layer(models.Model):
- #FIXME: here having a item,start index would be good
- created = models.DateTimeField(auto_now_add=True)
- modified = models.DateTimeField(auto_now=True)
- user = models.ForeignKey(User)
- item = models.ForeignKey(Item)
-
- #seconds
- start = models.FloatField(default=-1)
- stop = models.FloatField(default=-1)
-
- type = models.CharField(blank=True, max_length=255)
- value = models.TextField()
-
- #FIXME: relational layers, Locations, clips etc
- #location = models.ForeignKey('Location', default=None)
-
- def editable(self, user):
- if user.is_authenticated():
- if obj.user == user.id or user.has_perm('0x.admin'):
- return True
- if user.groups.filter(id__in=obj.groups.all()).count() > 0:
- return True
- return False
class Collection(models.Model):
created = models.DateTimeField(auto_now_add=True)
diff --git a/pandora/item/views.py b/pandora/item/views.py
index 81bc44ab..dc11f836 100644
--- a/pandora/item/views.py
+++ b/pandora/item/views.py
@@ -17,10 +17,7 @@ from django.shortcuts import render_to_response, get_object_or_404, get_list_or_
from django.template import RequestContext
from django.conf import settings
-try:
- import simplejson as json
-except ImportError:
- from django.utils import simplejson as json
+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
@@ -263,156 +260,6 @@ def api_removeItem(request):
response = json_response(status=403, text='permission denied')
return render_to_json_response(response)
-@login_required_json
-def api_addLayer(request):
- '''
- param data
- {key: value}
- return {'status': {'code': int, 'text': string},
- 'data': {}}
- '''
- response = {'status': {'code': 501, 'text': 'not implemented'}}
- return render_to_json_response(response)
-
-@login_required_json
-def api_removeLayer(request):
- '''
- param data
- {key: value}
- return {'status': {'code': int, 'text': string},
- 'data': {}}
- '''
- response = {'status': {'code': 501, 'text': 'not implemented'}}
- return render_to_json_response(response)
-
-@login_required_json
-def api_editLayer(request):
- '''
- param data
- {key: value}
- return {'status': {'code': int, 'text': string},
- 'data': {}}
- '''
- response = json_response({})
- data = json.loads(request.POST['data'])
- layer = get_object_or_404_json(models.Layer, pk=data['id'])
- if layer.editable(request.user):
- response = json_response(status=501, text='not implemented')
- else:
- response = json_response(status=403, text='permission denied')
- return render_to_json_response(response)
-
- response = json_response(status=501, text='not implemented')
- return render_to_json_response(response)
-
-'''
- List API
-'''
-@login_required_json
-def api_addListItem(request):
- '''
- param data
- {list: listId,
- item: itemId,
- quert: ...
- }
- return {'status': {'code': int, 'text': string},
- 'data': {}}
- '''
- data = json.loads(request.POST['data'])
- list = get_object_or_404_json(models.List, 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 removed')
- else:
- response = json_response(status=403, text='not allowed')
- elif 'query' in data:
- response = json_response(status=501, text='not implemented')
-
- else:
- response = json_response(status=501, text='not implemented')
- return render_to_json_response(response)
-
-@login_required_json
-def api_removeListItem(request):
- '''
- param data
- {list: listId,
- item: itemId,
- quert: ...
- }
- return {'status': {'code': int, 'text': string},
- 'data': {}}
- '''
- data = json.loads(request.POST['data'])
- list = get_object_or_404_json(models.List, 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')
-
- else:
- response = json_response(status=501, text='not implemented')
- return render_to_json_response(response)
-
-@login_required_json
-def api_addList(request):
- '''
- param data
- {name: value}
- return {'status': {'code': int, 'text': string},
- 'data': {}}
- '''
- data = json.loads(request.POST['data'])
- if models.List.filter(name=data['name'], user=request.user).count() == 0:
- list = models.List(name = data['name'], user=request.user)
- list.save()
- response = json_response(status=200, text='created')
- else:
- response = json_response(status=403, text='list name exists')
- return render_to_json_response(response)
-
-@login_required_json
-def api_editList(request):
- '''
- param data
- {key: value}
- keys: name, public
- return {'status': {'code': int, 'text': string},
- 'data': {}}
- '''
- data = json.loads(request.POST['data'])
- list = get_object_or_404_json(models.List, pk=data['list'])
- if list.editable(request.user):
- for key in data:
- if key in ('name', 'public'):
- setattr(list, key, data['key'])
- else:
- response = json_response(status=403, text='not allowed')
- return render_to_json_response(response)
-
-def api_removeList(request):
- '''
- param data
- {key: value}
- return {'status': {'code': int, 'text': string},
- 'data': {}}
- '''
- data = json.loads(request.POST['data'])
- list = get_object_or_404_json(models.List, pk=data['list'])
- if list.editable(request.user):
- list.delete()
- else:
- response = json_response(status=403, text='not allowed')
- return render_to_json_response(response)
-
'''
Poster API
'''
@@ -485,7 +332,6 @@ def api_getImdbId(request):
response = json_response(status=404, text='not found')
return render_to_json_response(response)
-
'''
media delivery
'''
diff --git a/pandora/layer/__init__.py b/pandora/layer/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/pandora/layer/admin.py b/pandora/layer/admin.py
new file mode 100644
index 00000000..0426ae38
--- /dev/null
+++ b/pandora/layer/admin.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+
+from django.contrib import admin
+
+import models
+
+
+class BinAdmin(admin.ModelAdmin):
+ search_fields = ['name', 'title']
+admin.site.register(models.Bin, BinAdmin)
+
+class LayerAdmin(admin.ModelAdmin):
+ search_fields = ['name', 'title']
+admin.site.register(models.Layer, LayerAdmin)
+
diff --git a/pandora/layer/models.py b/pandora/layer/models.py
new file mode 100644
index 00000000..fdcef3c1
--- /dev/null
+++ b/pandora/layer/models.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+from __future__ import division, with_statement
+
+from datetime import datetime
+import os.path
+import math
+import random
+import re
+import subprocess
+import unicodedata
+from glob import glob
+
+from django.db import models
+from django.db.models import Q
+from django.contrib.auth.models import User
+from django.core.files.base import ContentFile
+from django.utils import simplejson as json
+from django.conf import settings
+
+from ox.django import fields
+import ox
+from ox import stripTags
+from ox.normalize import canonicalTitle, canonicalName
+
+
+class Bin(models.Model):
+ class Meta:
+ ordering = ('position', )
+
+ name = models.CharField(null=True, max_length=255, unique=True)
+ title = models.CharField(null=True, max_length=255)
+ #text, string, string from list(fixme), event, place, person
+ type = models.CharField(null=True, max_length=255)
+ position = models.IntegerField(default=0)
+
+ overlapping = models.BooleanField(default=True)
+ enabled = models.BooleanField(default=True)
+
+ enabled = models.BooleanField(default=True)
+ public = models.BooleanField(default=True) #false=users only see there own bins
+ subtitle = models.BooleanField(default=True) #bis can be displayed as subtitle, only one bin
+
+ find = models.BooleanField(default=True)
+ #words / item duration(wpm), total words, cuts per minute, cuts, number of layers, number of layers/duration
+ sort = models.CharField(null=True, max_length=255)
+
+ def properties(self):
+ p = {}
+ if self.find:
+ p[self.name] = {'type': 'bin', 'find': True}
+ if self.sort:
+ print 'FIXME: need to add sort stuff'
+ return p
+
+class Layer(models.Model):
+ #FIXME: here having a item,start index would be good
+ created = models.DateTimeField(auto_now_add=True)
+ modified = models.DateTimeField(auto_now=True)
+ user = models.ForeignKey(User)
+ item = models.ForeignKey('item.Item')
+
+ #seconds
+ start = models.FloatField(default=-1)
+ stop = models.FloatField(default=-1)
+
+ type = models.CharField(blank=True, max_length=255)
+ value = models.TextField()
+
+ #FIXME: relational layers, Locations, clips etc
+ #location = models.ForeignKey('Location', default=None)
+
+ def editable(self, user):
+ if user.is_authenticated():
+ if obj.user == user.id or user.has_perm('0x.admin'):
+ return True
+ if user.groups.filter(id__in=obj.groups.all()).count() > 0:
+ return True
+ return False
+
diff --git a/pandora/layer/tests.py b/pandora/layer/tests.py
new file mode 100644
index 00000000..2247054b
--- /dev/null
+++ b/pandora/layer/tests.py
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/pandora/layer/views.py b/pandora/layer/views.py
new file mode 100644
index 00000000..4cc290e7
--- /dev/null
+++ b/pandora/layer/views.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+from __future__ import division
+import os.path
+import re
+from datetime import datetime
+from urllib2 import unquote
+import mimetypes
+
+from django import forms
+from django.core.paginator import Paginator
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.db.models import Q, Avg, Count, Sum
+from django.http import HttpResponse, Http404
+from django.shortcuts import render_to_response, get_object_or_404, get_list_or_404, redirect
+from django.template import RequestContext
+from django.conf import settings
+
+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 ox.django.http import HttpFileResponse
+import ox
+
+import models
+
+@login_required_json
+def api_addLayer(request):
+ '''
+ param data
+ {key: value}
+ return {'status': {'code': int, 'text': string},
+ 'data': {}}
+ '''
+ response = {'status': {'code': 501, 'text': 'not implemented'}}
+ return render_to_json_response(response)
+
+@login_required_json
+def api_removeLayer(request):
+ '''
+ param data
+ {key: value}
+ return {'status': {'code': int, 'text': string},
+ 'data': {}}
+ '''
+ response = {'status': {'code': 501, 'text': 'not implemented'}}
+ return render_to_json_response(response)
+
+@login_required_json
+def api_editLayer(request):
+ '''
+ param data
+ {key: value}
+ return {'status': {'code': int, 'text': string},
+ 'data': {}}
+ '''
+ response = json_response({})
+ data = json.loads(request.POST['data'])
+ layer = get_object_or_404_json(models.Layer, pk=data['id'])
+ if layer.editable(request.user):
+ response = json_response(status=501, text='not implemented')
+ else:
+ response = json_response(status=403, text='permission denied')
+ return render_to_json_response(response)
+
+ response = json_response(status=501, text='not implemented')
+ return render_to_json_response(response)
+
diff --git a/pandora/person/__init__.py b/pandora/person/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/pandora/person/models.py b/pandora/person/models.py
new file mode 100644
index 00000000..6b5f33c6
--- /dev/null
+++ b/pandora/person/models.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+from __future__ import division, with_statement
+
+from datetime import datetime
+import os.path
+import math
+import random
+import re
+import subprocess
+import unicodedata
+from glob import glob
+
+from django.db import models
+from django.db.models import Q
+from django.contrib.auth.models import User
+from django.core.files.base import ContentFile
+from django.utils import simplejson as json
+from django.conf import settings
+
+from ox.django import fields
+import ox
+from ox import stripTags
+from ox.normalize import canonicalTitle, canonicalName
+
+
+def getPersonSort(name):
+ person, created = Person.objects.get_or_create(name=name)
+ name_sort = unicodedata.normalize('NFKD', person.name_sort)
+ return name_sort
+
+class Person(models.Model):
+ name = models.CharField(max_length=200)
+ name_sort = models.CharField(max_length=200)
+
+ #FIXME: how to deal with aliases
+ aliases = fields.TupleField(default=[])
+
+ imdbId = models.CharField(max_length=7, blank=True)
+ wikipediaId = models.CharField(max_length=1000, blank=True)
+
+ class Meta:
+ ordering = ('name_sort', )
+
+ def __unicode__(self):
+ return self.name
+
+ def save(self, *args, **kwargs):
+ if not self.name_sort:
+ self.name_sort = ox.normalize.canonicalName(self.name)
+ super(Person, self).save(*args, **kwargs)
+
+ def get_or_create(model, name, imdbId=None):
+ if imdbId:
+ q = model.objects.filter(name=name, imdbId=imdbId)
+ else:
+ q = model.objects.all().filter(name=name)
+ if q.count() > 0:
+ o = q[0]
+ else:
+ o = model.objects.create(name=name)
+ if imdbId:
+ o.imdbId = imdbId
+ o.save()
+ return o
+ get_or_create = classmethod(get_or_create)
+
+ def json(self):
+ return self.name
+
diff --git a/pandora/person/tests.py b/pandora/person/tests.py
new file mode 100644
index 00000000..2247054b
--- /dev/null
+++ b/pandora/person/tests.py
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/pandora/person/views.py b/pandora/person/views.py
new file mode 100644
index 00000000..60f00ef0
--- /dev/null
+++ b/pandora/person/views.py
@@ -0,0 +1 @@
+# Create your views here.
diff --git a/pandora/place/__init__.py b/pandora/place/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/pandora/place/admin.py b/pandora/place/admin.py
new file mode 100644
index 00000000..8044bab1
--- /dev/null
+++ b/pandora/place/admin.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+
+from django.contrib import admin
+
+import models
+
+
+class PlaceAdmin(admin.ModelAdmin):
+ search_fields = ['name']
+admin.site.register(models.Place, PlaceAdmin)
+
+
diff --git a/pandora/place/managers.py b/pandora/place/managers.py
new file mode 100644
index 00000000..2e276537
--- /dev/null
+++ b/pandora/place/managers.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+import re
+
+from ox.utils import json
+from django.contrib.auth.models import User
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Q, Manager
+
+import models
+
+class PlaceManager(Manager):
+ def get_query_set(self):
+ return super(PlaceManager, self).get_query_set()
+
+ def find(self, q='', f="globe", sw_lat=-180.0, sw_lng=-180.0, ne_lat=180.0, ne_lng=180.0):
+ qs = self.get_query_set()
+ qs = qs.filter(Q(
+ Q(Q(sw_lat__gt=sw_lat)|Q(sw_lat__lt=ne_lat)|Q(sw_lng__gt=sw_lng)|Q(sw_lng__lt=ne_lng)) &
+ Q(Q(sw_lat__gt=sw_lat)|Q(sw_lat__lt=ne_lat)|Q(sw_lng__lt=ne_lng)|Q(ne_lng__gt=ne_lng)) &
+ Q(Q(ne_lat__gt=sw_lat)|Q(ne_lat__lt=ne_lat)|Q(sw_lng__gt=sw_lng)|Q(sw_lng__lt=ne_lng)) &
+ Q(Q(ne_lat__gt=sw_lat)|Q(ne_lat__lt=ne_lat)|Q(ne_lng__gt=sw_lng)|Q(ne_lng__lt=ne_lng))
+ ))
+ if q:
+ qs = qs.filter(name_find__icontains, q)
+ return qs
+ '''
+ #only return locations that have layers of videos visible to current user
+ if not identity.current.anonymous:
+ user = identity.current.user
+ if not user.in_group('admin'):
+ query = AND(query,
+ id == Layer.q.locationID, Layer.q.videoID == Video.q.id,
+ OR(Video.q.public == True, Video.q.creatorID == user.id)
+ )
+ else:
+ query = AND(query,
+ id == Layer.q.locationID, Layer.q.videoID == Video.q.id,
+ Video.q.public == True)
+ '''
diff --git a/pandora/place/models.py b/pandora/place/models.py
new file mode 100644
index 00000000..fa7f8016
--- /dev/null
+++ b/pandora/place/models.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+from __future__ import division, with_statement
+
+from django.db import models
+from django.db.models import Q
+from django.conf import settings
+
+from ox.django import fields
+
+import managers
+
+class Place(models.Model):
+ '''
+ Places are named locations, they should have geographical information attached to them.
+ '''
+
+ name = models.CharField(max_length=200, unique=True)
+ name_sort = models.CharField(max_length=200)
+ name_find = models.TextField(default='', editable=False)
+
+ geoname = models.CharField(max_length=1024, unique=True)
+ geoname_reverse = models.CharField(max_length=1024, unique=True)
+
+ wikipediaId = models.CharField(max_length=1000, blank=True)
+ aliases = fields.TupleField(default=[])
+
+ sw_lat = models.FloatField(default=0)
+ sw_lng = models.FloatField(default=0)
+ ne_lat = models.FloatField(default=0)
+ ne_lng = models.FloatField(default=0)
+ center_lat = models.FloatField(default=0)
+ center_lng = models.FloatField(default=0)
+ area = models.FloatField(default=-1)
+
+ objects = managers.PlaceManager()
+
+ class Meta:
+ ordering = ('name_sort', )
+
+ def __unicode__(self):
+ return self.name
+
+ def json(self):
+ j = {}
+ for key in ('name', 'name_sort', 'aliases', 'geoname', 'geoname_reversed',
+ 'sw_lat', 'sw_lng', 'ne_lat', 'ne_lng',
+ 'center_lat', 'center_lng'):
+ j[key] = getattr(self, key)
+
+ def save(self, *args, **kwargs):
+ if not self.name_sort:
+ self.name_sort = self.name
+ self.geoname_reverse = ', '.join(reversed(self.geoname.split(', ')))
+
+ self.name_find = '|%s|'%'|'.join([self.name] + self.aliases)
+
+ #update center
+ self.lat_center = ox.location.center(self.lat_sw, self.lat_ne)
+ self.lng_center = ox.location.center(self.lng_sw, self.lng_ne)
+
+ #update area
+ self.area = location.area(self.lat_sw, self.lng_sw, self.lat_ne, self.lng_ne)
+
+ super(Place, self).save(*args, **kwargs)
+
diff --git a/pandora/place/tests.py b/pandora/place/tests.py
new file mode 100644
index 00000000..2247054b
--- /dev/null
+++ b/pandora/place/tests.py
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/pandora/place/views.py b/pandora/place/views.py
new file mode 100644
index 00000000..82de0fe8
--- /dev/null
+++ b/pandora/place/views.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+from __future__ import division
+import os.path
+import re
+from datetime import datetime
+from urllib2 import unquote
+import mimetypes
+
+from django import forms
+from django.core.paginator import Paginator
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.db.models import Q, Avg, Count, Sum
+from django.http import HttpResponse, Http404
+from django.shortcuts import render_to_response, get_object_or_404, get_list_or_404, redirect
+from django.template import RequestContext
+from django.conf import settings
+
+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 ox.django.http import HttpFileResponse
+import ox
+
+import models
+
+'''
+fixme, require admin
+'''
+@login_required_json
+def api_addPlace(request):
+ '''
+ param data
+ {
+ 'place': dict
+ }
+ place contains key/value pairs with place propterties
+ '''
+ data = json.loads(request.POST['data'])
+ exists = False
+ names = [data['place']['name']] + data['place']['aliases']
+ for name in names:
+ if models.Place.objects.filter(name_find__icontains=u'|%s|'%data['name']).count() != 0:
+ exists = True
+ if not exists:
+ place = models.Place()
+ for key in data['place']:
+ setattr(place, key, data['place'][key])
+ place.save()
+ response = json_response(status=200, text='created')
+ else:
+ response = json_response(status=403, text='place name exists')
+ return render_to_json_response(response)
+
+@login_required_json
+def api_editPlace(request):
+ '''
+ param data
+ {
+ 'id': placeid,
+ 'place': dict
+ }
+ place contains key/value pairs with place propterties
+ '''
+ data = json.loads(request.POST['data'])
+ place = get_object_or_404_json(models.Place, pk=data['id'])
+ if place.editable(request.user):
+ conflict = False
+ names = [data['place']['name']] + data['place']['aliases']
+ for name in names:
+ if models.Place.objects.filter(name_find__icontains=u'|%s|'%data['name']).exclude(id=place.id).count() != 0:
+ conflict = True
+ if not conflict:
+ for key in data['place']:
+ setattr(place, key, data['place'][key])
+ place.save()
+ response = json_response(status=200, text='updated')
+ else:
+ response = json_response(status=403, text='place name/alias conflict')
+ else:
+ response = json_response(status=403, text='permission denied')
+ return render_to_json_response(response)
+
+@login_required_json
+def api_removePlace(request):
+ response = json_response(status=501, text='not implemented')
+ return render_to_json_response(response)
+
+def api_findPlace(request):
+ '''
+ param data
+ {'query': query, 'sort': array, 'range': array, 'area': array}
+
+ query: query object, more on query syntax at
+ https://wiki.0x2620.org/wiki/pandora/QuerySyntax
+ sort: array of key, operator dics
+ [
+ {
+ key: "year",
+ operator: "-"
+ },
+ {
+ key: "director",
+ operator: ""
+ }
+ ]
+ range: result range, array [from, to]
+ area: [sw_lat, sw_lng, ne_lat, ne_lng] only return places in that square
+
+ with keys, items is list of dicts with requested properties:
+ return {'status': {'code': int, 'text': string},
+ 'data': {items: array}}
+
+Positions
+ param data
+ {'query': query, 'ids': []}
+
+ query: query object, more on query syntax at
+ https://wiki.0x2620.org/wiki/pandora/QuerySyntax
+ ids: ids of places for which positions are required
+ '''
+ data = json.loads(request.POST['data'])
+ response = json_response(status=200, text='ok')
+ response['data']['places'] = []
+ #FIXME: add coordinates to limit search
+ for p in Places.objects.find(data['query']):
+ response['data']['places'].append(p.json())
+ return render_to_json_response(response)
+
diff --git a/pandora/settings.py b/pandora/settings.py
index 429772ed..0e4e52db 100644
--- a/pandora/settings.py
+++ b/pandora/settings.py
@@ -114,16 +114,21 @@ INSTALLED_APPS = (
'django_extensions',
'devserver',
- 'south',
+# 'south',
'djcelery',
'app',
'api',
- 'item',
'archive',
- 'user',
+ 'date',
+ 'item',
+ 'itemlist',
+ 'layer',
+ 'person',
+ 'place',
'text',
'torrent',
+ 'user',
)
AUTH_PROFILE_MODULE = 'user.UserProfile'
@@ -131,6 +136,9 @@ AUTH_PROFILE_MODULE = 'user.UserProfile'
#Video encoding settings
#available profiles: 96p, 270p, 360p, 480p, 720p, 1080p
+DEFAULT_SORT = [{"key": "director", "operator": ""}]
+DEFAULT_THEME = "classic"
+
VIDEO_PROFILE = '96p'
VIDEO_DERIVATIVES = []
VIDEO_H264 = False
diff --git a/pandora/templates/api.html b/pandora/templates/api.html
index 83e1d97d..fb67a3f9 100644
--- a/pandora/templates/api.html
+++ b/pandora/templates/api.html
@@ -3,9 +3,22 @@