Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@sugarlabs.org>2014-02-03 10:08:50 (GMT)
committer Aleksey Lim <alsroot@sugarlabs.org>2014-02-03 10:08:50 (GMT)
commitb4b008c1f302059221a1a43ed237e6d562ec7f97 (patch)
tree1c8b4dd71d6c165c461bbefe0847154ea333b91b
parent726ac1d66321ee7ac6b4cc7eff17001b5aeb6c5e (diff)
Keep Post comments in aggregated property
-rw-r--r--sugar_network/db/__init__.py2
-rw-r--r--sugar_network/db/directory.py29
-rw-r--r--sugar_network/db/index.py15
-rw-r--r--sugar_network/db/metadata.py8
-rw-r--r--sugar_network/db/routes.py50
-rw-r--r--sugar_network/model/post.py9
-rw-r--r--sugar_network/node/routes.py25
-rw-r--r--sugar_network/toolkit/router.py16
-rwxr-xr-xtests/units/db/resource.py194
-rwxr-xr-xtests/units/db/routes.py97
-rwxr-xr-xtests/units/model/post.py31
-rwxr-xr-xtests/units/node/node.py117
-rwxr-xr-xtests/units/node/volume.py5
13 files changed, 560 insertions, 38 deletions
diff --git a/sugar_network/db/__init__.py b/sugar_network/db/__init__.py
index b6bde81..2f22a36 100644
--- a/sugar_network/db/__init__.py
+++ b/sugar_network/db/__init__.py
@@ -351,7 +351,7 @@ Volume
from sugar_network.db.metadata import \
indexed_property, stored_property, blob_property, \
- Property, StoredProperty, BlobProperty, IndexedProperty
+ Property, StoredProperty, BlobProperty, IndexedProperty, AggregatedType
from sugar_network.db.index import index_flush_timeout, \
index_flush_threshold, index_write_queue
from sugar_network.db.resource import Resource
diff --git a/sugar_network/db/directory.py b/sugar_network/db/directory.py
index 96a2923..944f73a 100644
--- a/sugar_network/db/directory.py
+++ b/sugar_network/db/directory.py
@@ -24,6 +24,7 @@ from sugar_network.toolkit.router import ACL
from sugar_network.db.storage import Storage
from sugar_network.db.metadata import BlobProperty, Metadata, GUID_PREFIX
from sugar_network.db.metadata import IndexedProperty, StoredProperty
+from sugar_network.db.metadata import AggregatedType
from sugar_network.toolkit import http, exception, enforce
@@ -278,9 +279,16 @@ class Directory(object):
if isinstance(prop, BlobProperty):
del meta['seqno']
else:
- meta = {'mtime': meta['mtime'],
- 'value': meta.get('value'),
- }
+ value = meta.get('value')
+ if prop.typecast is AggregatedType:
+ value_ = {}
+ for key, agg in value.items():
+ aggseqno = agg.pop('seqno')
+ if aggseqno >= start and \
+ (not end or aggseqno <= end):
+ value_[key] = agg
+ value = value_
+ meta = {'mtime': meta['mtime'], 'value': value}
yield name, meta, seqno
yield doc.guid, patch()
@@ -303,6 +311,12 @@ class Directory(object):
else:
meta['seqno'] = (orig_meta or {}).get('seqno') or 0
meta.update(kwargs)
+ if self.metadata.get(prop).typecast is AggregatedType:
+ for agg in meta['value'].values():
+ agg['seqno'] = meta['seqno']
+ if orig_meta:
+ orig_meta['value'].update(meta['value'])
+ meta['value'] = orig_meta['value']
merge[prop] = meta
if op is not None:
patch[prop] = meta.get('value')
@@ -365,7 +379,14 @@ class Directory(object):
value = meta['value']
changes[name] = prop.default if value is None else value
else:
- if prop.localized:
+ if prop.typecast is AggregatedType:
+ for aggvalue in value.values():
+ aggvalue['seqno'] = seqno
+ if existed:
+ value_ = record.get(name)['value']
+ value_.update(value)
+ value = value_
+ elif prop.localized:
if not isinstance(value, dict):
value = {toolkit.default_lang(): value}
if existed and \
diff --git a/sugar_network/db/index.py b/sugar_network/db/index.py
index aa75f80..7ff43bb 100644
--- a/sugar_network/db/index.py
+++ b/sugar_network/db/index.py
@@ -349,9 +349,9 @@ class IndexWriter(IndexReader):
_logger.debug('Index %r object: %r', self.metadata.name, properties)
- document = xapian.Document()
+ doc = xapian.Document()
term_generator = xapian.TermGenerator()
- term_generator.set_document(document)
+ term_generator.set_document(doc)
for name, prop in self._props.items():
value = guid \
@@ -366,21 +366,20 @@ class IndexWriter(IndexReader):
slotted_value = toolkit.gettext(value) or ''
else:
slotted_value = next(_fmt_prop_value(prop, value))
- document.add_value(prop.slot, slotted_value)
+ doc.add_value(prop.slot, slotted_value)
if prop.prefix or prop.full_text:
for value_ in _fmt_prop_value(prop, value):
if prop.prefix:
if prop.boolean:
- document.add_boolean_term(
- _term(prop.prefix, value_))
+ doc.add_boolean_term(_term(prop.prefix, value_))
else:
- document.add_term(_term(prop.prefix, value_))
+ doc.add_term(_term(prop.prefix, value_))
if prop.full_text:
term_generator.index_text(value_, 1, prop.prefix or '')
term_generator.increase_termpos()
- self._db.replace_document(_term(GUID_PREFIX, guid), document)
+ self._db.replace_document(_term(GUID_PREFIX, guid), doc)
self._pending_updates += 1
if post_cb is not None:
@@ -475,7 +474,7 @@ def _fmt_prop_value(prop, value):
for i in value:
for j in fmt(i):
yield j
- else:
+ elif value is not None:
yield str(value)
return fmt(value if prop.fmt is None else prop.fmt(value))
diff --git a/sugar_network/db/metadata.py b/sugar_network/db/metadata.py
index 7cba5ce..55942a7 100644
--- a/sugar_network/db/metadata.py
+++ b/sugar_network/db/metadata.py
@@ -58,6 +58,10 @@ stored_property = lambda ** kwargs: indexed_property(StoredProperty, **kwargs)
blob_property = lambda ** kwargs: indexed_property(BlobProperty, **kwargs)
+class AggregatedType(dict):
+ pass
+
+
class Metadata(dict):
"""Structure to describe the document.
@@ -107,8 +111,8 @@ class Metadata(dict):
return self._name
def __getitem__(self, prop_name):
- enforce(prop_name in self, 'There is no %r property in %r',
- prop_name, self.name)
+ enforce(prop_name in self, http.NotFound,
+ 'There is no %r property in %r', prop_name, self.name)
return dict.__getitem__(self, prop_name)
diff --git a/sugar_network/db/routes.py b/sugar_network/db/routes.py
index 123e001..19ad26c 100644
--- a/sugar_network/db/routes.py
+++ b/sugar_network/db/routes.py
@@ -25,6 +25,7 @@ from contextlib import contextmanager
from os.path import exists
from sugar_network import toolkit
+from sugar_network.db.metadata import AggregatedType
from sugar_network.db.metadata import BlobProperty, StoredProperty, LIST_TYPES
from sugar_network.toolkit.router import Blob, ACL, route
from sugar_network.toolkit import http, enforce
@@ -114,6 +115,52 @@ class Routes(object):
def get_prop_meta(self, request, response):
self._prop_meta(request, response)
+ @route('POST', [None, None, None],
+ acl=ACL.AUTH, mime_type='application/json')
+ def insert_to_aggprop(self, request):
+ content = request.content or {}
+ enforce(isinstance(content, dict), http.BadRequest, 'Invalid value')
+
+ directory = self.volume[request.resource]
+ prop = directory.metadata[request.prop]
+
+ enforce(prop.typecast is AggregatedType, http.BadRequest,
+ 'Property is not aggregated')
+ prop.assert_access(ACL.INSERT)
+ self.on_aggprop_update(request, prop, None)
+
+ if request.principal:
+ authors = content['author'] = {}
+ self._useradd(authors, request.principal, ACL.ORIGINAL)
+ guid = content.pop('guid') if 'guid' in content else toolkit.uuid()
+ props = {request.prop: {guid: content}}
+ event = {}
+ self.on_update(request, props, event)
+ directory.update(request.guid, props, event)
+
+ return guid
+
+ @route('DELETE', [None, None, None, None],
+ acl=ACL.AUTH, mime_type='application/json')
+ def remove_from_aggprop(self, request):
+ directory = self.volume[request.resource]
+ doc = directory.get(request.guid)
+ prop = directory.metadata[request.prop]
+
+ enforce(prop.typecast is AggregatedType, http.BadRequest,
+ 'Property is not aggregated')
+ prop.assert_access(ACL.REMOVE)
+
+ guid = request.path[3]
+ enforce(guid in doc[request.prop], http.NotFound,
+ 'No such aggregated item')
+ self.on_aggprop_update(request, prop, doc[request.prop][guid])
+
+ props = {request.prop: {guid: {}}}
+ event = {}
+ self.on_update(request, props, event)
+ directory.update(request.guid, props, event)
+
@route('PUT', [None, None], cmd='useradd',
arguments={'role': 0}, acl=ACL.AUTH | ACL.AUTHOR)
def useradd(self, request, user, role):
@@ -145,6 +192,9 @@ class Routes(object):
def on_update(self, request, props, event):
props['mtime'] = int(time.time())
+ def on_aggprop_update(self, request, prop, value):
+ pass
+
def after_post(self, doc):
pass
diff --git a/sugar_network/model/post.py b/sugar_network/model/post.py
index 602ad02..fda366f 100644
--- a/sugar_network/model/post.py
+++ b/sugar_network/model/post.py
@@ -45,7 +45,7 @@ class Post(db.Resource):
def title(self, value):
return value
- @db.indexed_property(prefix='D', full_text=True, localized=True,
+ @db.indexed_property(prefix='M', full_text=True, localized=True,
acl=ACL.CREATE | ACL.READ)
def message(self, value):
return value
@@ -59,6 +59,13 @@ class Post(db.Resource):
def vote(self, value):
return value
+ @db.indexed_property(prefix='D', typecast=db.AggregatedType,
+ full_text=True, default=db.AggregatedType(),
+ fmt=lambda x: [i.get('message') for i in x.values()],
+ acl=ACL.READ | ACL.INSERT | ACL.REMOVE)
+ def comments(self, value):
+ return value
+
@db.blob_property(mime_type='image/png')
def preview(self, value):
if value:
diff --git a/sugar_network/node/routes.py b/sugar_network/node/routes.py
index 4bc97e4..eb48c70 100644
--- a/sugar_network/node/routes.py
+++ b/sugar_network/node/routes.py
@@ -271,13 +271,7 @@ class NodeRoutes(model.VolumeRoutes, model.FrontRoutes):
request.principal = request.authorization.login
if op.acl & ACL.AUTHOR and request.guid:
- if request.resource == 'user':
- allowed = (request.principal == request.guid)
- else:
- doc = self.volume[request.resource].get(request.guid)
- allowed = (request.principal in doc['author'])
- enforce(allowed or self.authorize(request.principal, 'root'),
- http.Forbidden, 'Operation is permitted only for authors')
+ self._enforce_authority(request)
if op.acl & ACL.SUPERUSER:
enforce(self.authorize(request.principal, 'root'), http.Forbidden,
@@ -300,6 +294,12 @@ class NodeRoutes(model.VolumeRoutes, model.FrontRoutes):
if 'deleted' in props.get('layer', []):
event['event'] = 'delete'
+ def on_aggprop_update(self, request, prop, value):
+ if prop.acl & ACL.AUTHOR:
+ self._enforce_authority(request)
+ elif value is not None:
+ self._enforce_authority(request, value.get('author'))
+
def find(self, request, reply):
limit = request.get('limit')
if limit is None or limit < 0:
@@ -402,6 +402,17 @@ class NodeRoutes(model.VolumeRoutes, model.FrontRoutes):
del data[key]
return result
+ def _enforce_authority(self, request, author=None):
+ if request.resource == 'user':
+ allowed = (request.principal == request.guid)
+ else:
+ if author is None:
+ doc = self.volume[request.resource].get(request.guid)
+ author = doc['author']
+ allowed = request.principal in author
+ enforce(allowed or self.authorize(request.principal, 'root'),
+ http.Forbidden, 'Operation is permitted only for authors')
+
def generate_node_stats(volume, path):
tmp_path = toolkit.mkdtemp()
diff --git a/sugar_network/toolkit/router.py b/sugar_network/toolkit/router.py
index 9a430b7..df57ff3 100644
--- a/sugar_network/toolkit/router.py
+++ b/sugar_network/toolkit/router.py
@@ -82,20 +82,24 @@ class ACL(object):
WRITE = 1 << 3
READ = 1 << 4
DELETE = 1 << 5
- PUBLIC = CREATE | WRITE | READ | DELETE
+ INSERT = 1 << 6
+ REMOVE = 1 << 7
+ PUBLIC = CREATE | WRITE | READ | DELETE | INSERT | REMOVE
- AUTH = 1 << 6
- AUTHOR = 1 << 7
- SUPERUSER = 1 << 8
+ AUTH = 1 << 8
+ AUTHOR = 1 << 9
+ SUPERUSER = 1 << 10
- LOCAL = 1 << 9
- CALC = 1 << 10
+ LOCAL = 1 << 11
+ CALC = 1 << 12
NAMES = {
CREATE: 'Create',
WRITE: 'Write',
READ: 'Read',
DELETE: 'Delete',
+ INSERT: 'Insert',
+ REMOVE: 'Remove',
}
diff --git a/tests/units/db/resource.py b/tests/units/db/resource.py
index ad4580f..d09010e 100755
--- a/tests/units/db/resource.py
+++ b/tests/units/db/resource.py
@@ -622,6 +622,144 @@ class ResourceTest(tests.Test):
[i for i in diff(directory, [[0, None]], out_seq, group_by='prop')])
self.assertEqual([[2, 2]], out_seq)
+ def test_diff_Aggprops(self):
+
+ class Document(db.Resource):
+
+ @db.stored_property(typecast=db.AggregatedType, default=db.AggregatedType())
+ def prop(self, value):
+ return value
+
+ directory = Directory(tests.tmpdir, Document, IndexWriter)
+
+ directory.create({'guid': '1', 'prop': {'1': {'prop': 1}}, 'ctime': 1, 'mtime': 1})
+ for i in os.listdir('1/1'):
+ os.utime('1/1/%s' % i, (1, 1))
+
+ directory.create({'guid': '2', 'prop': {'2': {'prop': 2}}, 'ctime': 2, 'mtime': 2})
+ for i in os.listdir('2/2'):
+ os.utime('2/2/%s' % i, (2, 2))
+
+ out_seq = Sequence()
+ self.assertEqual([
+ {'guid': '1', 'diff': {
+ 'guid': {'value': '1', 'mtime': 1},
+ 'ctime': {'value': 1, 'mtime': 1},
+ 'mtime': {'value': 1, 'mtime': 1},
+ 'prop': {'value': {'1': {'prop': 1}}, 'mtime': 1},
+ }},
+ {'guid': '2', 'diff': {
+ 'guid': {'value': '2', 'mtime': 2},
+ 'ctime': {'value': 2, 'mtime': 2},
+ 'mtime': {'value': 2, 'mtime': 2},
+ 'prop': {'value': {'2': {'prop': 2}}, 'mtime': 2},
+ }},
+ ],
+ [i for i in diff(directory, [[0, None]], out_seq)])
+ self.assertEqual([[1, 2]], out_seq)
+
+ out_seq = Sequence()
+ self.assertEqual([
+ {'guid': '1', 'diff': {
+ 'guid': {'value': '1', 'mtime': 1},
+ 'ctime': {'value': 1, 'mtime': 1},
+ 'mtime': {'value': 1, 'mtime': 1},
+ 'prop': {'value': {'1': {'prop': 1}}, 'mtime': 1},
+ }},
+ ],
+ [i for i in diff(directory, [[1, 1]], out_seq)])
+ self.assertEqual([[1, 1]], out_seq)
+
+ out_seq = Sequence()
+ self.assertEqual([
+ {'guid': '2', 'diff': {
+ 'guid': {'value': '2', 'mtime': 2},
+ 'ctime': {'value': 2, 'mtime': 2},
+ 'mtime': {'value': 2, 'mtime': 2},
+ 'prop': {'value': {'2': {'prop': 2}}, 'mtime': 2},
+ }},
+ ],
+ [i for i in diff(directory, [[2, 2]], out_seq)])
+ self.assertEqual([[2, 2]], out_seq)
+
+ out_seq = Sequence()
+ self.assertEqual([
+ ],
+ [i for i in diff(directory, [[3, None]], out_seq)])
+ self.assertEqual([], out_seq)
+
+ self.assertEqual({
+ '1': {'seqno': 1, 'prop': 1},
+ },
+ directory.get('1')['prop'])
+ self.assertEqual({
+ '2': {'seqno': 2, 'prop': 2},
+ },
+ directory.get('2')['prop'])
+
+ out_seq = Sequence()
+ directory.update('2', {'prop': {'2': {}, '3': {'prop': 3}}})
+ self.assertEqual([
+ {'guid': '2', 'diff': {
+ 'prop': {'value': {'2': {}, '3': {'prop': 3}}, 'mtime': int(os.stat('2/2/prop').st_mtime)},
+ }},
+ ],
+ [i for i in diff(directory, [[3, None]], out_seq)])
+ self.assertEqual([[3, 3]], out_seq)
+
+ self.assertEqual({
+ '2': {'seqno': 3},
+ '3': {'seqno': 3, 'prop': 3},
+ },
+ directory.get('2')['prop'])
+
+ out_seq = Sequence()
+ directory.update('1', {'prop': {'1': {'foo': 'bar'}}})
+ self.assertEqual([
+ {'guid': '1', 'diff': {
+ 'prop': {'value': {'1': {'foo': 'bar'}}, 'mtime': int(os.stat('1/1/prop').st_mtime)},
+ }},
+ ],
+ [i for i in diff(directory, [[4, None]], out_seq)])
+ self.assertEqual([[4, 4]], out_seq)
+
+ self.assertEqual({
+ '1': {'seqno': 4, 'foo': 'bar'},
+ },
+ directory.get('1')['prop'])
+
+ out_seq = Sequence()
+ directory.update('2', {'prop': {'2': {'restore': True}}})
+ self.assertEqual([
+ {'guid': '2', 'diff': {
+ 'prop': {'value': {'2': {'restore': True}}, 'mtime': int(os.stat('2/2/prop').st_mtime)},
+ }},
+ ],
+ [i for i in diff(directory, [[5, None]], out_seq)])
+ self.assertEqual([[5, 5]], out_seq)
+
+ self.assertEqual({
+ '2': {'seqno': 5, 'restore': True},
+ '3': {'seqno': 3, 'prop': 3},
+ },
+ directory.get('2')['prop'])
+
+ out_seq = Sequence()
+ directory.update('2', {'ctime': 0})
+ self.assertEqual([
+ {'guid': '2', 'diff': {
+ 'ctime': {'value': 0, 'mtime': int(os.stat('2/2/prop').st_mtime)},
+ }},
+ ],
+ [i for i in diff(directory, [[6, None]], out_seq)])
+ self.assertEqual([[6, 6]], out_seq)
+
+ self.assertEqual({
+ '2': {'seqno': 5, 'restore': True},
+ '3': {'seqno': 3, 'prop': 3},
+ },
+ directory.get('2')['prop'])
+
def test_merge_New(self):
class Document(db.Resource):
@@ -885,6 +1023,62 @@ class ResourceTest(tests.Test):
self.assertEqual(5, doc.meta('blob')['mtime'])
self.assertEqual('blob-2', file('document/1/1/blob.blob').read())
+ def test_merge_Aggprops(self):
+
+ class Document(db.Resource):
+
+ @db.stored_property(typecast=db.AggregatedType, default=db.AggregatedType())
+ def prop(self, value):
+ return value
+
+ directory = Directory('document', Document, IndexWriter)
+
+ directory.merge('1', {
+ 'guid': {'mtime': 1, 'value': '1'},
+ 'ctime': {'mtime': 1, 'value': 1},
+ 'mtime': {'mtime': 1, 'value': 1},
+ 'prop': {'mtime': 1, 'value': {'1': {}}},
+ })
+ self.assertEqual({
+ '1': {'seqno': 1},
+ },
+ directory.get('1')['prop'])
+
+ directory.merge('1', {
+ 'prop': {'mtime': 1, 'value': {'1': {'probe': False}}},
+ })
+ self.assertEqual({
+ '1': {'seqno': 1},
+ },
+ directory.get('1')['prop'])
+
+ directory.merge('1', {
+ 'prop': {'mtime': 2, 'value': {'1': {'probe': True}}},
+ })
+ self.assertEqual({
+ '1': {'seqno': 2, 'probe': True},
+ },
+ directory.get('1')['prop'])
+
+ directory.merge('1', {
+ 'prop': {'mtime': 3, 'value': {'2': {'foo': 'bar'}}},
+ })
+ self.assertEqual({
+ '1': {'seqno': 2, 'probe': True},
+ '2': {'seqno': 3, 'foo': 'bar'},
+ },
+ directory.get('1')['prop'])
+
+ directory.merge('1', {
+ 'prop': {'mtime': 4, 'value': {'2': {}, '3': {'foo': 'bar'}}},
+ })
+ self.assertEqual({
+ '1': {'seqno': 2, 'probe': True},
+ '2': {'seqno': 4},
+ '3': {'seqno': 4, 'foo': 'bar'},
+ },
+ directory.get('1')['prop'])
+
def test_wipe(self):
class Document(db.Resource):
diff --git a/tests/units/db/routes.py b/tests/units/db/routes.py
index 51cd892..5908d0f 100755
--- a/tests/units/db/routes.py
+++ b/tests/units/db/routes.py
@@ -1650,6 +1650,103 @@ class RoutesTest(tests.Test):
guid = self.call('POST', ['document'], content={'prop': None})
self.assertEqual('default', self.volume['document'].get(guid).meta('prop')['value'])
+ def test_InsertAggprops(self):
+
+ class Document(db.Resource):
+
+ @db.stored_property(default='')
+ def prop1(self, value):
+ return value
+
+ @db.stored_property(typecast=db.AggregatedType, default=db.AggregatedType(), acl=ACL.WRITE)
+ def prop2(self, value):
+ return value
+
+ @db.stored_property(typecast=db.AggregatedType, default=db.AggregatedType(), acl=ACL.INSERT)
+ def prop3(self, value):
+ return value
+
+ events = []
+ self.volume = db.Volume('db', [Document], lambda event: events.append(event))
+ guid = self.call('POST', ['document'], content={})
+
+ self.assertRaises(http.NotFound, self.call, 'POST', ['document', 'foo', 'bar'], content={})
+ self.assertRaises(http.NotFound, self.call, 'POST', ['document', guid, 'bar'], content={})
+ self.assertRaises(http.BadRequest, self.call, 'POST', ['document', guid, 'prop1'], content={})
+ self.assertRaises(http.Forbidden, self.call, 'POST', ['document', guid, 'prop2'], content={})
+
+ del events[:]
+ self.override(time, 'time', lambda: 0)
+ self.override(toolkit, 'uuid', lambda: '0')
+ self.assertEqual('0', self.call('POST', ['document', guid, 'prop3'], content={}))
+ self.assertEqual({
+ '0': {'seqno': 2},
+ },
+ self.volume['document'].get(guid)['prop3'])
+ self.assertEqual([
+ {'event': 'update', 'resource': 'document', 'guid': guid},
+ ],
+ events)
+
+ self.override(time, 'time', lambda: 1)
+ self.assertEqual('1', self.call('POST', ['document', guid, 'prop3'], content={'guid': '1', 'foo': 'bar'}))
+ self.assertEqual({
+ '0': {'seqno': 2},
+ '1': {'seqno': 3, 'foo': 'bar'},
+ },
+ self.volume['document'].get(guid)['prop3'])
+
+ self.override(time, 'time', lambda: 2)
+ self.override(toolkit, 'uuid', lambda: '2')
+ self.assertEqual('2', self.call('POST', ['document', guid, 'prop3'], content={'prop': 'more'}))
+ self.assertEqual({
+ '0': {'seqno': 2},
+ '1': {'seqno': 3, 'foo': 'bar'},
+ '2': {'seqno': 4, 'prop': 'more'},
+ },
+ self.volume['document'].get(guid)['prop3'])
+
+ def test_RemoveAggprops(self):
+
+ class Document(db.Resource):
+
+ @db.stored_property(typecast=db.AggregatedType, default=db.AggregatedType(), acl=ACL.INSERT)
+ def prop1(self, value):
+ return value
+
+ @db.stored_property(typecast=db.AggregatedType, default=db.AggregatedType(), acl=ACL.INSERT | ACL.REMOVE)
+ def prop2(self, value):
+ return value
+
+ events = []
+ self.volume = db.Volume('db', [Document], lambda event: events.append(event))
+ guid = self.call('POST', ['document'], content={})
+
+ agg_guid = self.call('POST', ['document', guid, 'prop1'], content={'probe': 'value'})
+ del events[:]
+ self.assertEqual(
+ {agg_guid: {'seqno': 2, 'probe': 'value'}},
+ self.volume['document'].get(guid)['prop1'])
+ self.assertRaises(http.Forbidden, self.call, 'DELETE', ['document', guid, 'prop1', agg_guid])
+ self.assertEqual(
+ {agg_guid: {'seqno': 2, 'probe': 'value'}},
+ self.volume['document'].get(guid)['prop1'])
+ self.assertEqual([], events)
+
+ agg_guid = self.call('POST', ['document', guid, 'prop2'], content={'probe': 'value'})
+ del events[:]
+ self.assertEqual(
+ {agg_guid: {'seqno': 3, 'probe': 'value'}},
+ self.volume['document'].get(guid)['prop2'])
+ self.call('DELETE', ['document', guid, 'prop2', agg_guid])
+ self.assertEqual(
+ {agg_guid: {'seqno': 4}},
+ self.volume['document'].get(guid)['prop2'])
+ self.assertEqual([
+ {'event': 'update', 'resource': 'document', 'guid': guid},
+ ],
+ events)
+
def call(self, method=None, path=None,
accept_language=None, content=None, content_stream=None, cmd=None,
content_type=None, host=None, request=None, routes=db.Routes, principal=None,
diff --git a/tests/units/model/post.py b/tests/units/model/post.py
index 931bd66..dc6f6f4 100755
--- a/tests/units/model/post.py
+++ b/tests/units/model/post.py
@@ -61,6 +61,37 @@ class PostTest(tests.Test):
['3', '5', '2', '4', '1'],
[i.guid for i in directory.find(order_by='-rating')[0]])
+ def test_FindComments(self):
+ directory = db.Volume('db', [Post])['post']
+
+ directory.create({'guid': '1', 'context': '', 'type': 'comment', 'title': '', 'message': '', 'comments': {
+ '1': {'message': 'foo'},
+ }})
+ directory.create({'guid': '2', 'context': '', 'type': 'comment', 'title': '', 'message': '', 'comments': {
+ '1': {'message': 'bar'},
+ }})
+ directory.create({'guid': '3', 'context': '', 'type': 'comment', 'title': '', 'message': '', 'comments': {
+ '1': {'message': 'bar'},
+ '2': {'message': 'foo'},
+ }})
+ directory.create({'guid': '4', 'context': '', 'type': 'comment', 'title': '', 'message': '', 'comments': {
+ '1': {'message': 'foo bar'},
+ }})
+
+ self.assertEqual(
+ ['1', '3', '4'],
+ [i.guid for i in directory.find(query='foo')[0]])
+ self.assertEqual(
+ ['2', '3', '4'],
+ [i.guid for i in directory.find(query='bar')[0]])
+ self.assertEqual(
+ ['1', '2', '3', '4'],
+ [i.guid for i in directory.find(query='foo bar')[0]])
+
+ self.assertEqual(
+ ['1', '3', '4'],
+ [i.guid for i in directory.find(query='comments:foo')[0]])
+
if __name__ == '__main__':
tests.main()
diff --git a/tests/units/node/node.py b/tests/units/node/node.py
index 388b2ed..9ad5726 100755
--- a/tests/units/node/node.py
+++ b/tests/units/node/node.py
@@ -1317,15 +1317,116 @@ class NodeTest(tests.Test):
'post.total',
], start=ts + 1, end=ts + 3))
+ def test_AggpropInsertAccess(self):
-def call(routes, method, document=None, guid=None, prop=None, principal=None, content=None, **kwargs):
- path = ['']
- if document:
- path.append(document)
- if guid:
- path.append(guid)
- if prop:
- path.append(prop)
+ class Document(db.Resource):
+
+ @db.stored_property(typecast=db.AggregatedType, default=db.AggregatedType(), acl=ACL.READ | ACL.INSERT)
+ def prop1(self, value):
+ return value
+
+ @db.stored_property(typecast=db.AggregatedType, default=db.AggregatedType(), acl=ACL.READ | ACL.INSERT | ACL.AUTHOR)
+ def prop2(self, value):
+ return value
+
+ volume = db.Volume('db', [Document, User])
+ volume['user'].create({'guid': tests.UID, 'name': 'user1', 'pubkey': {'blob': StringIO(tests.PUBKEY)}})
+ volume['user'].create({'guid': tests.UID2, 'name': 'user2', 'pubkey': {'blob': StringIO(tests.PUBKEY2)}})
+
+ cp = NodeRoutes('node', volume)
+ guid = call(cp, method='POST', document='document', principal=tests.UID, content={})
+ self.override(time, 'time', lambda: 0)
+
+ call(cp, method='POST', path=['document', guid, 'prop1'], principal=tests.UID, content={'guid': '1'})
+ call(cp, method='POST', path=['document', guid, 'prop1'], principal=tests.UID2, content={'guid': '2'})
+ self.assertEqual({
+ '1': {'seqno': 4, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}},
+ '2': {'seqno': 5, 'author': {tests.UID2: {'name': 'user2', 'order': 0, 'role': 3}}},
+ },
+ call(cp, method='GET', path=['document', guid, 'prop1']))
+
+ call(cp, method='POST', path=['document', guid, 'prop2'], principal=tests.UID, content={'guid': '1'})
+ self.assertRaises(http. Forbidden, call, cp, method='POST', path=['document', guid, 'prop2'], principal=tests.UID2, content={'guid': '2'})
+ self.assertEqual({
+ '1': {'seqno': 6, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}},
+ },
+ call(cp, method='GET', path=['document', guid, 'prop2']))
+
+ def test_AggpropRemoveAccess(self):
+
+ class Document(db.Resource):
+
+ @db.stored_property(typecast=db.AggregatedType, default=db.AggregatedType(), acl=ACL.READ | ACL.INSERT | ACL.REMOVE)
+ def prop1(self, value):
+ return value
+
+ @db.stored_property(typecast=db.AggregatedType, default=db.AggregatedType(), acl=ACL.READ | ACL.INSERT | ACL.REMOVE | ACL.AUTHOR)
+ def prop2(self, value):
+ return value
+
+ volume = db.Volume('db', [Document, User])
+ volume['user'].create({'guid': tests.UID, 'name': 'user1', 'pubkey': {'blob': StringIO(tests.PUBKEY)}})
+ volume['user'].create({'guid': tests.UID2, 'name': 'user2', 'pubkey': {'blob': StringIO(tests.PUBKEY2)}})
+
+ cp = NodeRoutes('node', volume)
+ guid = call(cp, method='POST', document='document', principal=tests.UID, content={})
+ self.override(time, 'time', lambda: 0)
+
+ call(cp, method='POST', path=['document', guid, 'prop1'], principal=tests.UID, content={'guid': '1', 'probe': True})
+ call(cp, method='POST', path=['document', guid, 'prop1'], principal=tests.UID2, content={'guid': '2', 'probe': True})
+ self.assertEqual({
+ '1': {'seqno': 4, 'probe': True, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}},
+ '2': {'seqno': 5, 'probe': True, 'author': {tests.UID2: {'name': 'user2', 'order': 0, 'role': 3}}},
+ },
+ call(cp, method='GET', path=['document', guid, 'prop1']))
+ self.assertRaises(http.Forbidden, call, cp, method='DELETE', path=['document', guid, 'prop1', '1'], principal=tests.UID2)
+ self.assertRaises(http.Forbidden, call, cp, method='DELETE', path=['document', guid, 'prop1', '2'], principal=tests.UID)
+ self.assertEqual({
+ '1': {'seqno': 4, 'probe': True, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}},
+ '2': {'seqno': 5, 'probe': True, 'author': {tests.UID2: {'name': 'user2', 'order': 0, 'role': 3}}},
+ },
+ call(cp, method='GET', path=['document', guid, 'prop1']))
+
+ call(cp, method='DELETE', path=['document', guid, 'prop1', '1'], principal=tests.UID)
+ self.assertEqual({
+ '1': {'seqno': 6},
+ '2': {'seqno': 5, 'probe': True, 'author': {tests.UID2: {'name': 'user2', 'order': 0, 'role': 3}}},
+ },
+ call(cp, method='GET', path=['document', guid, 'prop1']))
+ call(cp, method='DELETE', path=['document', guid, 'prop1', '2'], principal=tests.UID2)
+ self.assertEqual({
+ '1': {'seqno': 6},
+ '2': {'seqno': 7},
+ },
+ call(cp, method='GET', path=['document', guid, 'prop1']))
+
+ call(cp, method='POST', path=['document', guid, 'prop2'], principal=tests.UID, content={'guid': '1', 'probe': True})
+ self.assertEqual({
+ '1': {'seqno': 8, 'probe': True, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}},
+ },
+ call(cp, method='GET', path=['document', guid, 'prop2']))
+
+ self.assertRaises(http.Forbidden, call, cp, method='DELETE', path=['document', guid, 'prop2', '1'], principal=tests.UID2)
+ self.assertEqual({
+ '1': {'seqno': 8, 'probe': True, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}},
+ },
+ call(cp, method='GET', path=['document', guid, 'prop2']))
+ call(cp, method='DELETE', path=['document', guid, 'prop2', '1'], principal=tests.UID)
+ self.assertEqual({
+ '1': {'seqno': 9},
+ },
+ call(cp, method='GET', path=['document', guid, 'prop2']))
+
+
+def call(routes, method, document=None, guid=None, prop=None, principal=None, content=None, path=None, **kwargs):
+ if not path:
+ path = ['']
+ if document:
+ path.append(document)
+ if guid:
+ path.append(guid)
+ if prop:
+ path.append(prop)
env = {'REQUEST_METHOD': method,
'PATH_INFO': '/'.join(path),
'HTTP_HOST': '127.0.0.1',
diff --git a/tests/units/node/volume.py b/tests/units/node/volume.py
index 512b3e0..01e71a7 100755
--- a/tests/units/node/volume.py
+++ b/tests/units/node/volume.py
@@ -325,7 +325,10 @@ class VolumeTest(tests.Test):
def test_merge_MultipleCommits(self):
class Document(db.Resource):
- pass
+
+ @db.stored_property()
+ def prop(self, value):
+ return value
self.touch(('db/seqno', '100'))
volume = db.Volume('db', [Document])