From b8e5764f3d0e3eb40c33a692a95df1c0a2f7b235 Mon Sep 17 00:00:00 2001 From: j <0x006A@0x2620.org> Date: Fri, 26 Nov 2010 16:07:24 +0100 Subject: [PATCH] split models into apps, new api interface --- README | 9 +- pandora/api/views.py | 49 +- pandora/date/__init__.py | 0 pandora/date/admin.py | 12 + pandora/date/managers.py | 19 + pandora/date/models.py | 44 ++ pandora/date/tests.py | 23 + pandora/date/views.py | 113 +++ pandora/fixtures/0xdb_properties.json | 970 ++++++++++++++++++++++++++ pandora/item/admin.py | 16 +- pandora/item/managers.py | 2 +- pandora/item/models.py | 346 ++------- pandora/item/views.py | 156 +---- pandora/layer/__init__.py | 0 pandora/layer/admin.py | 16 + pandora/layer/models.py | 80 +++ pandora/layer/tests.py | 23 + pandora/layer/views.py | 69 ++ pandora/person/__init__.py | 0 pandora/person/models.py | 70 ++ pandora/person/tests.py | 23 + pandora/person/views.py | 1 + pandora/place/__init__.py | 0 pandora/place/admin.py | 13 + pandora/place/managers.py | 40 ++ pandora/place/models.py | 66 ++ pandora/place/tests.py | 23 + pandora/place/views.py | 131 ++++ pandora/settings.py | 14 +- pandora/templates/api.html | 17 +- requirements.txt | 3 +- static/js/pandora.api.js | 173 +++++ 32 files changed, 2033 insertions(+), 488 deletions(-) create mode 100644 pandora/date/__init__.py create mode 100644 pandora/date/admin.py create mode 100644 pandora/date/managers.py create mode 100644 pandora/date/models.py create mode 100644 pandora/date/tests.py create mode 100644 pandora/date/views.py create mode 100644 pandora/fixtures/0xdb_properties.json create mode 100644 pandora/layer/__init__.py create mode 100644 pandora/layer/admin.py create mode 100644 pandora/layer/models.py create mode 100644 pandora/layer/tests.py create mode 100644 pandora/layer/views.py create mode 100644 pandora/person/__init__.py create mode 100644 pandora/person/models.py create mode 100644 pandora/person/tests.py create mode 100644 pandora/person/views.py create mode 100644 pandora/place/__init__.py create mode 100644 pandora/place/admin.py create mode 100644 pandora/place/managers.py create mode 100644 pandora/place/models.py create mode 100644 pandora/place/tests.py create mode 100644 pandora/place/views.py create mode 100755 static/js/pandora.api.js diff --git a/README b/README index a82e8937f..fb610f3d9 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 b9e68a90b..1f7ab164f 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 000000000..e69de29bb diff --git a/pandora/date/admin.py b/pandora/date/admin.py new file mode 100644 index 000000000..b1141f0a0 --- /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 000000000..9a7e4e99f --- /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 000000000..ac44ed098 --- /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 000000000..2247054b3 --- /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 000000000..378476f53 --- /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 000000000..d6b214c78 --- /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 bd8296b53..8bf97d83e 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 59d1b2308..b0a5ebe44 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 27b9c0347..277dbda7d 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 81bc44ab1..dc11f836b 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 000000000..e69de29bb diff --git a/pandora/layer/admin.py b/pandora/layer/admin.py new file mode 100644 index 000000000..0426ae383 --- /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 000000000..fdcef3c12 --- /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 000000000..2247054b3 --- /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 000000000..4cc290e75 --- /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 000000000..e69de29bb diff --git a/pandora/person/models.py b/pandora/person/models.py new file mode 100644 index 000000000..6b5f33c62 --- /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 000000000..2247054b3 --- /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 000000000..60f00ef0e --- /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 000000000..e69de29bb diff --git a/pandora/place/admin.py b/pandora/place/admin.py new file mode 100644 index 000000000..8044bab14 --- /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 000000000..2e2765371 --- /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 000000000..fa7f80166 --- /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 000000000..2247054b3 --- /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 000000000..82de0fe85 --- /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 429772ed2..0e4e52db9 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 83e1d97dc..fb67a3f9d 100644 --- a/pandora/templates/api.html +++ b/pandora/templates/api.html @@ -3,9 +3,22 @@ {{sitename}} API - + + + + + + + + + + + + + diff --git a/requirements.txt b/requirements.txt index 244eddcf2..221472620 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,11 @@ -e svn+http://code.djangoproject.com/svn/django/branches/releases/1.2.X#egg=django South -chardet -e bzr+http://code.0x2620.org/python-ox/#egg=python-ox -e bzr+http://code.0x2620.org/oxtimeline/#egg=oxtimeline simplejson #-e hg+https://django-ajax-filtered-fields.googlecode.com/hg/#egg=django-ajax-filtered-fields #rabbitmq interface --e git+git://github.com/ask/celery.git#egg=celery +celery django-celery django_extensions -e bzr+http://firefogg.org/dev/python-firefogg/#egg=python-firefogg diff --git a/static/js/pandora.api.js b/static/js/pandora.api.js new file mode 100755 index 000000000..7f914c2e8 --- /dev/null +++ b/static/js/pandora.api.js @@ -0,0 +1,173 @@ +/*** + Pandora API +***/ + +var app = new Ox.App({ + apiURL: '/api/', + config: '/site.json', + init: 'hello', +}).launch(function(data) { + Ox.print('data', data) + app.config = data.config; + app.user = data.user; + if (app.user.group == 'guest') { + app.user = data.config.user; + $.browser.safari && Ox.theme('modern'); + } + + app.$body = $('body'); + app.$document = $(document); + app.$window = $(window); + //app.$body.html(''); + + app.$ui = {}; + app.$ui.actionList = constructList(); + app.$ui.actionInfo = Ox.Container().css({padding: '8px'}); + + app.api.apidoc(function(results) { app.docs = results.data.actions; }); + + var $main = new Ox.SplitPanel({ + elements: [ + { + element: app.$ui.actionList, + size: 160 + }, + { + element: app.$ui.actionInfo, + } + ], + orientation: 'horizontal' + }); + + $main.appendTo(app.$body); +}); + +function constructList() { + return new Ox.TextList({ + columns: [ + { + align: "left", + id: "name", + operator: "+", + title: "Name", + unique: true, + visible: true, + width: 140 + }, + ], + columnsMovable: false, + columnsRemovable: false, + id: 'actionList', + request: function(data, callback) { + if(!data.keys) { + app.api.apidoc(function(results) { + var items = []; + $.each(results.data.actions, function(k) {items.push({'name': k})}); + var result = {'data': {'items': items.length}}; + callback(result); + }); + } else { + app.api.apidoc(function(results) { + var items = []; + $.each(results.data.actions, function(k) {items.push({'name': k})}); + var result = {'data': {'items': items}}; + callback(result); + }); + } + }, + sort: [ + { + key: "name", + operator: "+" + } + ] + }).bindEvent({ + select: function(event, data) { + var info = $('
'); + $.each(data.ids, function(v, k) { + console.log(k) + info.append($("

").html(k)); + info.append($('
').html(app.docs[k].replace('/\n/
\n/g'))); + }); + app.$ui.actionInfo.html(info); + } + }); +} + +/* + .bindEvent({ + load: function(event, data) { + app.$ui.total.html(app.constructStatus('total', data)); + data = []; + $.each(app.config.totals, function(i, v) { + data[v.id] = 0; + }); + app.$ui.selected.html(app.constructStatus('selected', data)); + }, + open: function(event, data) { + //alert(data.toSource()); + var $iframe = $('