split models into apps, new api interface
This commit is contained in:
parent
f833109c02
commit
b8e5764f3d
32 changed files with 2033 additions and 488 deletions
9
README
9
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
|
||||
|
||||
|
|
|
@ -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', '<br>\n')
|
||||
'doc': get_api_doc(f[4:]).replace('\n', '<br>\n')
|
||||
})
|
||||
context = RequestContext(request, {'api': api,
|
||||
'sitename': settings.SITENAME,})
|
||||
|
|
0
pandora/date/__init__.py
Normal file
0
pandora/date/__init__.py
Normal file
12
pandora/date/admin.py
Normal file
12
pandora/date/admin.py
Normal file
|
@ -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)
|
||||
|
19
pandora/date/managers.py
Normal file
19
pandora/date/managers.py
Normal file
|
@ -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
|
44
pandora/date/models.py
Normal file
44
pandora/date/models.py
Normal file
|
@ -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)
|
||||
|
23
pandora/date/tests.py
Normal file
23
pandora/date/tests.py
Normal file
|
@ -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
|
||||
"""}
|
||||
|
113
pandora/date/views.py
Normal file
113
pandora/date/views.py
Normal file
|
@ -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)
|
||||
|
970
pandora/fixtures/0xdb_properties.json
Normal file
970
pandora/fixtures/0xdb_properties.json
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
'''
|
||||
|
|
0
pandora/layer/__init__.py
Normal file
0
pandora/layer/__init__.py
Normal file
16
pandora/layer/admin.py
Normal file
16
pandora/layer/admin.py
Normal file
|
@ -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)
|
||||
|
80
pandora/layer/models.py
Normal file
80
pandora/layer/models.py
Normal file
|
@ -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
|
||||
|
23
pandora/layer/tests.py
Normal file
23
pandora/layer/tests.py
Normal file
|
@ -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
|
||||
"""}
|
||||
|
69
pandora/layer/views.py
Normal file
69
pandora/layer/views.py
Normal file
|
@ -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)
|
||||
|
0
pandora/person/__init__.py
Normal file
0
pandora/person/__init__.py
Normal file
70
pandora/person/models.py
Normal file
70
pandora/person/models.py
Normal file
|
@ -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
|
||||
|
23
pandora/person/tests.py
Normal file
23
pandora/person/tests.py
Normal file
|
@ -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
|
||||
"""}
|
||||
|
1
pandora/person/views.py
Normal file
1
pandora/person/views.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Create your views here.
|
0
pandora/place/__init__.py
Normal file
0
pandora/place/__init__.py
Normal file
13
pandora/place/admin.py
Normal file
13
pandora/place/admin.py
Normal file
|
@ -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)
|
||||
|
||||
|
40
pandora/place/managers.py
Normal file
40
pandora/place/managers.py
Normal file
|
@ -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)
|
||||
'''
|
66
pandora/place/models.py
Normal file
66
pandora/place/models.py
Normal file
|
@ -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)
|
||||
|
23
pandora/place/tests.py
Normal file
23
pandora/place/tests.py
Normal file
|
@ -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
|
||||
"""}
|
||||
|
131
pandora/place/views.py
Normal file
131
pandora/place/views.py
Normal file
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
@ -3,9 +3,22 @@
|
|||
<head>
|
||||
<title>{{sitename}} API</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<script type="text/javascript" src="/static/js/jquery/jquery.js"></script>
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="/static/png/icon.16.png"/>
|
||||
<link rel="stylesheet" type="text/css" href="/static/oxjs/build/css/ox.ui.css"/>
|
||||
|
||||
<script type="text/javascript" src="/static/oxjs/build/js/jquery-1.4.2.js"></script>
|
||||
<script type="text/javascript" src="/static/js/jquery/jquery.videosupport.js"></script>
|
||||
<script type="text/javascript" src="/static/oxjs/build/js/ox.load.js"></script>
|
||||
<script type="text/javascript" src="/static/oxjs/build/js/ox.js"></script>
|
||||
<script type="text/javascript" src="/static/oxjs/build/js/ox.ui.js"></script>
|
||||
<script type="text/javascript" src="/static/js/pandora.api.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
|
||||
<body>
|
||||
<!--
|
||||
<div id="static_apidocs">
|
||||
<h1>{{sitename}} Public API</h1>
|
||||
<a href="http://code.0xdb.org/pandora/annotate/head%3A/pandora/backend/views.py">look inside</a>
|
||||
<div>
|
||||
|
@ -18,5 +31,7 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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
|
||||
|
|
173
static/js/pandora.api.js
Executable file
173
static/js/pandora.api.js
Executable file
|
@ -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 = $('<div>');
|
||||
$.each(data.ids, function(v, k) {
|
||||
console.log(k)
|
||||
info.append($("<h2>").html(k));
|
||||
info.append($('<pre>').html(app.docs[k].replace('/\n/<br>\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 = $('<iframe frameborder="0">')
|
||||
.css({
|
||||
width:'100%',
|
||||
height: '99%',
|
||||
border: 0,
|
||||
margin: 0,
|
||||
padding: 0
|
||||
});
|
||||
|
||||
var $dialog = new Ox.Dialog({
|
||||
title: 'Downloading',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Close',
|
||||
click: function() {
|
||||
$dialog.close();
|
||||
}
|
||||
}
|
||||
],
|
||||
width: 800,
|
||||
height: 400
|
||||
})
|
||||
.append($iframe)
|
||||
.open();
|
||||
|
||||
app.api.find({
|
||||
query: {
|
||||
conditions: [{
|
||||
key: 'id',
|
||||
value: data.ids[0],
|
||||
operator: '='
|
||||
}],
|
||||
operator: ''
|
||||
},
|
||||
keys: ['links'],
|
||||
range: [0, 100]
|
||||
}, function(result) {
|
||||
var url = result.data.items[0].links[0];
|
||||
$iframe.attr('src', 'http://anonym.to/?' + url);
|
||||
|
||||
//var url = result.data.items[0].links[0];
|
||||
//document.location.href = url;
|
||||
//window.open(url, "download");
|
||||
|
||||
});
|
||||
},
|
||||
select: function(event, data) {
|
||||
app.api.find({
|
||||
query: {
|
||||
conditions: $.map(data.ids, function(id, i) {
|
||||
return {
|
||||
key: 'id',
|
||||
value: id,
|
||||
operator: '='
|
||||
}
|
||||
}),
|
||||
operator: '|'
|
||||
}
|
||||
}, function(result) {
|
||||
app.$ui.selected.html(app.constructStatus('selected', result.data));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
*/
|
Loading…
Reference in a new issue