From b4b008c1f302059221a1a43ed237e6d562ec7f97 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Mon, 03 Feb 2014 10:08:50 +0000 Subject: Keep Post comments in aggregated property --- 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]) -- cgit v0.9.1