Compare commits
No commits in common. "06e89c264b8988edfcd24863120868063c92b70a" and "5a17f77f7a799d11ee7957a6210ee14a5858c80e" have entirely different histories.
06e89c264b
...
5a17f77f7a
6 changed files with 30 additions and 312 deletions
|
|
@ -292,13 +292,6 @@ class Annotation(models.Model):
|
|||
def _get_entity_json(self, user=None, entity_cache=None):
|
||||
"""When serializing many annotations pointing to the same entity, it is expensive to
|
||||
repeatedly look up and serialize the same entity.
|
||||
|
||||
TODO: if Entity were a (nullable) foreign key of Annotation, we could just:
|
||||
|
||||
prefetch_related('entity', 'entity__user', 'entity__documents')
|
||||
|
||||
before serializing the annotations, which would make self.entity.json(user=user) cheap and
|
||||
all this unnecessary.
|
||||
"""
|
||||
from entity.models import Entity
|
||||
|
||||
|
|
|
|||
|
|
@ -61,31 +61,6 @@ def buildCondition(k, op, v):
|
|||
key = str(key)
|
||||
if find_key:
|
||||
return Q(**{'find__key': find_key, key: v})
|
||||
q = Q(**{'find__key': find_key, key: v})
|
||||
|
||||
# TODO: support more elaborate predicates on link targets?
|
||||
# Straw man syntax:
|
||||
# // entities with 'ABC' in friends:
|
||||
# {key: 'link:friends', operator: '==', value: 'ABC'}
|
||||
# // entities whose friends have 'science' in their description
|
||||
# {key: 'link:friends', operator: '~', value: {
|
||||
# key: 'description',
|
||||
# operator: '=',
|
||||
# value: 'science'
|
||||
# }}
|
||||
# // entities listed in PQR's friends
|
||||
# {key: 'backlink:friends', operator: '==', value: 'PQR'}
|
||||
# It's a bit tricky without special syntax because at this point we
|
||||
# don't know what the matching entity type is.
|
||||
if op == '==':
|
||||
try:
|
||||
v_ = ox.fromAZ(v)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
q |= Q(links__key=find_key, links__target=v_)
|
||||
|
||||
return q
|
||||
else:
|
||||
return Q(**{key: v})
|
||||
|
||||
|
|
@ -143,7 +118,7 @@ class EntityManager(Manager):
|
|||
query.get('operator', '&'),
|
||||
user, item)
|
||||
if conditions:
|
||||
qs = qs.filter(conditions).distinct()
|
||||
qs = qs.filter(conditions)
|
||||
|
||||
return qs
|
||||
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-09-18 09:03
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import oxdjango.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('entity', '0005_jsonfield'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Link',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(max_length=200)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='link',
|
||||
name='source',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to='entity.Entity'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='link',
|
||||
name='target',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='backlinks', to='entity.Entity'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='link',
|
||||
unique_together=set([('source', 'key', 'target')]),
|
||||
),
|
||||
]
|
||||
|
|
@ -30,10 +30,6 @@ User = get_user_model()
|
|||
|
||||
@python_2_unicode_compatible
|
||||
class Entity(models.Model):
|
||||
class ValueError(ValueError):
|
||||
'''Raised if a field name or value is invalid (based on the "entities"
|
||||
key in config.jsonc)'''
|
||||
pass
|
||||
|
||||
class Meta:
|
||||
unique_together = ("type", "name")
|
||||
|
|
@ -58,7 +54,7 @@ class Entity(models.Model):
|
|||
documents = models.ManyToManyField(Document, through='DocumentProperties', related_name='entities')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
entity = self.get_entity(self.type)
|
||||
entity = get_by_id(settings.CONFIG['entities'], self.type)
|
||||
if entity.get('sortType') == 'person' and self.name:
|
||||
if isinstance(self.name, bytes):
|
||||
self.name = self.name.decode('utf-8')
|
||||
|
|
@ -79,13 +75,6 @@ class Entity(models.Model):
|
|||
def get(cls, id):
|
||||
return cls.objects.get(pk=ox.fromAZ(id))
|
||||
|
||||
@classmethod
|
||||
def get_entity(cls, type_):
|
||||
e = get_by_id(settings.CONFIG['entities'], type_)
|
||||
if e is None:
|
||||
raise cls.ValueError('Unknown entity type {!r}'.format(type_))
|
||||
return e
|
||||
|
||||
@classmethod
|
||||
def get_by_name(cls, name, type):
|
||||
return cls.objects.get(name_find__contains=u'|%s|' % name.lower(), type=type)
|
||||
|
|
@ -126,14 +115,7 @@ class Entity(models.Model):
|
|||
return False
|
||||
|
||||
def edit(self, data):
|
||||
if 'type' in data:
|
||||
entity = self.get_entity(data['type'])
|
||||
self.type = data['type']
|
||||
else:
|
||||
entity = self.get_entity(self.type)
|
||||
|
||||
config_keys = {k['id']: k for k in entity['keys']}
|
||||
for key, value in data.items():
|
||||
for key in data:
|
||||
if key == 'name':
|
||||
data['name'] = re.sub(' \[\d+\]$', '', data['name']).strip()
|
||||
if not data['name']:
|
||||
|
|
@ -145,7 +127,7 @@ class Entity(models.Model):
|
|||
name = data['name'] + ' [%d]' % n
|
||||
self.name = name
|
||||
elif key == 'type':
|
||||
pass
|
||||
self.type = data[key]
|
||||
elif key == 'alternativeNames':
|
||||
used_names = [self.name.lower()]
|
||||
names = []
|
||||
|
|
@ -161,34 +143,6 @@ class Entity(models.Model):
|
|||
names.append(name)
|
||||
used_names.append(name.lower())
|
||||
self.alternativeNames = tuple(ox.escape_html(n) for n in names)
|
||||
elif key not in config_keys:
|
||||
raise self.ValueError('Unknown key "{}" for entity type "{}"'.format(key, self.type))
|
||||
elif config_keys[key]['type'] == ["entity"]:
|
||||
n = config_keys[key].get('max')
|
||||
if n is not None:
|
||||
value = value[:n]
|
||||
|
||||
es = []
|
||||
for d in value:
|
||||
if not isinstance(d, dict):
|
||||
raise self.ValueError('"{}" should be [object]'.format(key))
|
||||
|
||||
try:
|
||||
if 'id' in d:
|
||||
es.append(Entity.get(d['id']))
|
||||
elif 'name' in d and 'type' in d:
|
||||
es.append(Entity.get_by_name(d['name'], d['type']))
|
||||
else:
|
||||
raise self.ValueError('"{}" elements should have either "id" or both "name" and "type"'.format(key))
|
||||
except Entity.DoesNotExist:
|
||||
pass # consistent with addDocument when "entity" is a list of IDs
|
||||
|
||||
for e in es:
|
||||
Link.objects.get_or_create(source=self, key=key, target=e)
|
||||
|
||||
Link.objects.filter(source=self, key=key) \
|
||||
.exclude(target__in=es) \
|
||||
.delete()
|
||||
else:
|
||||
#FIXME: more data validation
|
||||
if isinstance(data[key], string_types):
|
||||
|
|
@ -196,29 +150,7 @@ class Entity(models.Model):
|
|||
else:
|
||||
self.data[key] = data[key]
|
||||
|
||||
def _expand_links(self, keys, back=False):
|
||||
response = {}
|
||||
|
||||
# Not applying any filters here to allow .prefetch_related()
|
||||
# on sets of entities.
|
||||
qs = (self.backlinks if back else self.links).all()
|
||||
for link in qs:
|
||||
if link.key in keys:
|
||||
other = link.source if back else link.target
|
||||
j = other.json(keys=['id', 'type', 'name', 'sortName'])
|
||||
key = '-' + link.key if back else link.key
|
||||
response.setdefault(key, []).append(j)
|
||||
|
||||
for k in response:
|
||||
response[k].sort(key=lambda j: j['sortName'])
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def json(self, keys=None, user=None):
|
||||
entity = self.get_entity(self.type)
|
||||
config_keys = {k['id']: k for k in entity['keys']}
|
||||
|
||||
if not keys:
|
||||
keys = [
|
||||
'alternativeNames',
|
||||
|
|
@ -229,19 +161,8 @@ class Entity(models.Model):
|
|||
'type',
|
||||
'user',
|
||||
'documents',
|
||||
] + list(config_keys)
|
||||
|
||||
] + list(self.data)
|
||||
response = {}
|
||||
|
||||
link_keys = {
|
||||
id
|
||||
for id, k in config_keys.items()
|
||||
if id in keys and k['type'] in ('entity', ['entity'])
|
||||
}
|
||||
if link_keys:
|
||||
response.update(self._expand_links(link_keys))
|
||||
response.update(self._expand_links(link_keys, back=True))
|
||||
|
||||
for key in keys:
|
||||
if key == 'id':
|
||||
response[key] = self.get_id()
|
||||
|
|
@ -261,8 +182,6 @@ class Entity(models.Model):
|
|||
sort_key = 'document__created'
|
||||
response[key] = [ox.toAZ(id_)
|
||||
for id_, in self.documentproperties.order_by(sort_key).values_list('document_id')]
|
||||
elif key in link_keys:
|
||||
pass # expanded above
|
||||
elif key in self.data:
|
||||
response[key] = self.data[key]
|
||||
return response
|
||||
|
|
@ -286,7 +205,9 @@ class Entity(models.Model):
|
|||
else:
|
||||
Find.objects.filter(entity=self, key=key).delete()
|
||||
|
||||
entity = self.get_entity(self.type)
|
||||
entity = get_by_id(settings.CONFIG['entities'], self.type)
|
||||
if not entity:
|
||||
return
|
||||
with transaction.atomic():
|
||||
ids = ['name']
|
||||
for key in entity['keys']:
|
||||
|
|
@ -311,9 +232,6 @@ class Entity(models.Model):
|
|||
matches = annotation.models.Annotation.objects.filter(layer__in=entity_layers, value=self.get_id()).count()
|
||||
else:
|
||||
matches = 0
|
||||
|
||||
matches += Link.objects.filter(target=self).count()
|
||||
|
||||
for url in urls:
|
||||
matches += annotation.models.Annotation.objects.filter(value__contains=url).count()
|
||||
matches += item.models.Item.objects.filter(data__contains=url).count()
|
||||
|
|
@ -377,18 +295,3 @@ class Find(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
return u"%s=%s" % (self.key, self.value)
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Link(models.Model):
|
||||
'''Models entity fields of type "entity".'''
|
||||
|
||||
class Meta:
|
||||
unique_together = ("source", "key", "target")
|
||||
|
||||
source = models.ForeignKey(Entity, related_name='links')
|
||||
key = models.CharField(max_length=200)
|
||||
target = models.ForeignKey(Entity, related_name='backlinks')
|
||||
|
||||
def __str__(self):
|
||||
return u"%s-[%s]->%s" % (self.source, self.key, self.target)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,14 +29,6 @@ def get_entity_or_404_json(id):
|
|||
'text': 'Entity not found'}}
|
||||
raise HttpErrorJson(response)
|
||||
|
||||
def get_type_or_400_json(type_):
|
||||
try:
|
||||
return models.Entity.get_entity(type_)
|
||||
except models.Entity.ValueError as e:
|
||||
raise HttpErrorJson(json_response(
|
||||
status=400,
|
||||
text=e.message))
|
||||
|
||||
@login_required_json
|
||||
def addEntity(request, data):
|
||||
'''
|
||||
|
|
@ -53,7 +45,9 @@ def addEntity(request, data):
|
|||
existing_names = []
|
||||
exists = False
|
||||
|
||||
get_type_or_400_json(data.get('type'))
|
||||
if not utils.get_by_id(settings.CONFIG['entities'], data['type']):
|
||||
response = json_response(status=500, text='unknown entity type')
|
||||
return render_to_json_response(response)
|
||||
|
||||
if 'name' in data:
|
||||
names = [data['name']] + data.get('alternativeNames', [])
|
||||
|
|
@ -119,7 +113,7 @@ def autocompleteEntities(request, data):
|
|||
data['range'] = [0, 10]
|
||||
op = data.get('operator', '=')
|
||||
|
||||
entity = get_type_or_400_json(data['key'])
|
||||
entity = utils.get_by_id(settings.CONFIG['entities'], data['key'])
|
||||
order_by = entity.get('autocompleteSort', False)
|
||||
if order_by:
|
||||
for o in order_by:
|
||||
|
|
@ -163,17 +157,6 @@ def editEntity(request, data):
|
|||
id: string, // entity id
|
||||
name: string, // entity name
|
||||
description: string, // entity description
|
||||
friends: [ // key with type "entity"
|
||||
{
|
||||
// either:
|
||||
id: string, // related entity ID
|
||||
|
||||
// or:
|
||||
type: string, // related entity type, and
|
||||
name: string // related entity name
|
||||
}
|
||||
... // more entities
|
||||
]
|
||||
... // more properties, as defined in config
|
||||
}
|
||||
returns {
|
||||
|
|
@ -185,11 +168,8 @@ def editEntity(request, data):
|
|||
response = json_response()
|
||||
entity = get_entity_or_404_json(data['id'])
|
||||
if entity.editable(request.user):
|
||||
try:
|
||||
entity.edit(data)
|
||||
entity.save()
|
||||
except models.Entity.ValueError as e:
|
||||
raise HttpErrorJson(json_response(status=400, text=e.message))
|
||||
entity.edit(data)
|
||||
entity.save()
|
||||
response['data'] = entity.json(user=request.user)
|
||||
add_changelog(request, data)
|
||||
else:
|
||||
|
|
@ -248,8 +228,6 @@ def findEntities(request, data):
|
|||
response = json_response()
|
||||
if 'keys' in data:
|
||||
qs = qs[query['range'][0]:query['range'][1]]
|
||||
qs = qs.prefetch_related('links', 'links__target')
|
||||
qs = qs.prefetch_related('backlinks', 'backlinks__source')
|
||||
|
||||
response['data']['items'] = [l.json(data['keys'], request.user) for l in qs]
|
||||
elif 'position' in data:
|
||||
|
|
|
|||
|
|
@ -1,82 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
(function() {
|
||||
|
||||
// cribbed from documentsPanel.js, TODO: refactor
|
||||
var entitiesInput = function(width, defaultType, max) {
|
||||
var labelWidth = 80;
|
||||
|
||||
if (!defaultType) {
|
||||
defaultType = pandora.site.entities[0].id;
|
||||
}
|
||||
|
||||
return Ox.ArrayInput({
|
||||
input: {
|
||||
get: function(width) {
|
||||
var $input = Ox.FormElementGroup({
|
||||
elements: [
|
||||
Ox.Select({
|
||||
items: pandora.site.entities.map(function(entity) {
|
||||
return {
|
||||
id: entity.id,
|
||||
title: entity.title
|
||||
};
|
||||
}),
|
||||
overlap: 'right',
|
||||
width: labelWidth
|
||||
})
|
||||
.bindEvent({
|
||||
change: function() {
|
||||
var v = $input.value();
|
||||
$input.value({type: v.type, name: ''});
|
||||
$input.options('elements')[1].focusInput();
|
||||
}
|
||||
}),
|
||||
Ox.Input({
|
||||
autocomplete: function(value, callback) {
|
||||
pandora.api.autocompleteEntities({
|
||||
key: $input.value().type,
|
||||
operator: '=',
|
||||
range: [0, 10],
|
||||
value: value
|
||||
}, function(result) {
|
||||
callback(result.data.items);
|
||||
});
|
||||
},
|
||||
autocompleteReplace: true,
|
||||
autocompleteSelect: true,
|
||||
autocompleteSelectSubmit: true,
|
||||
width: width - labelWidth
|
||||
})
|
||||
],
|
||||
width: width,
|
||||
split: function(value) {
|
||||
return [value.type || defaultType, value.name || ''];
|
||||
},
|
||||
join: function(value) {
|
||||
return {type: value[0], name: value[1]};
|
||||
}
|
||||
});
|
||||
return $input;
|
||||
},
|
||||
getEmpty: function(value) {
|
||||
var type = (value && value[0]) || defaultType;
|
||||
return {type: type, name: ''};
|
||||
},
|
||||
isEmpty: function(value) {
|
||||
return !value.name;
|
||||
},
|
||||
setWidth: function($input, width) {
|
||||
$input.options('elements')[1].options({
|
||||
width: width - labelWidth
|
||||
});
|
||||
}
|
||||
},
|
||||
width: width,
|
||||
max: max || 0,
|
||||
});
|
||||
}
|
||||
|
||||
pandora.ui.entitiesDialog = function(options) {
|
||||
|
||||
var dialogHeight = Math.round((window.innerHeight - 48) * 0.9),
|
||||
|
|
@ -449,7 +372,6 @@ pandora.ui.entitiesDialog = function(options) {
|
|||
$form.empty()
|
||||
keys.forEach(function(key, index) {
|
||||
var defaultValue = void 0,
|
||||
value = result.data[key.id],
|
||||
$label = Ox.Label({
|
||||
title: Ox._(key.title),
|
||||
width: width
|
||||
|
|
@ -475,42 +397,29 @@ pandora.ui.entitiesDialog = function(options) {
|
|||
height: width,
|
||||
type: 'textarea'
|
||||
});
|
||||
} else if (key.type[0] === 'entity') {
|
||||
$input = entitiesInput(width, type, key.max || 0);
|
||||
defaultValue = [];
|
||||
|
||||
$input.bindEvent({
|
||||
submit: function(data) {
|
||||
$input.triggerEvent('change', data);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var change = function(data, eventName) {
|
||||
console.log(eventName, data);
|
||||
pandora.api.editEntity(Ox.extend({
|
||||
id: id
|
||||
}, key.id, data.value), function(result) {
|
||||
Ox.Request.clearCache('findEntities');
|
||||
Ox.Request.clearCache('getEntity');
|
||||
if (key.id == 'name') {
|
||||
$input.value(result.data.name);
|
||||
$list.reloadList(true);
|
||||
} else if (key.id == 'alternativeNames') {
|
||||
$input.value(result.data.alternativeNames);
|
||||
}
|
||||
renderEntity();
|
||||
});
|
||||
};
|
||||
|
||||
$input.options({
|
||||
disabled: key.id == 'id',
|
||||
value: value || defaultValue,
|
||||
value: result.data[key.id] || defaultValue,
|
||||
width: width
|
||||
})
|
||||
.css({margin: '4px'})
|
||||
.bindEvent({
|
||||
change: change,
|
||||
change: function(data) {
|
||||
pandora.api.editEntity(Ox.extend({
|
||||
id: id
|
||||
}, key.id, data.value), function(result) {
|
||||
Ox.Request.clearCache('findEntities');
|
||||
Ox.Request.clearCache('getEntity');
|
||||
if (key.id == 'name') {
|
||||
$input.value(result.data.name);
|
||||
$list.reloadList(true);
|
||||
} else if (key.id == 'alternativeNames') {
|
||||
$input.value(result.data.alternativeNames);
|
||||
}
|
||||
renderEntity();
|
||||
});
|
||||
}
|
||||
})
|
||||
.appendTo($form);
|
||||
$labels.push($label);
|
||||
|
|
@ -558,4 +467,3 @@ pandora.ui.entitiesDialog = function(options) {
|
|||
return that;
|
||||
|
||||
};
|
||||
}());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue