merge branch master, + fix tests
This commit is contained in:
		
						commit
						f100a18f22
					
				
					 10 changed files with 414 additions and 70 deletions
				
			
		
							
								
								
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -7,6 +7,8 @@ | |||
| *.sublime-project | ||||
| *.sublime-workspace | ||||
| *.ropeproject | ||||
| .env | ||||
| .pytest.ini | ||||
| *#*# | ||||
| *.#* | ||||
| __pycache__ | ||||
|  |  | |||
							
								
								
									
										40
									
								
								scopes/interfaces.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								scopes/interfaces.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | |||
| # scopes.interfaces | ||||
| 
 | ||||
| from zope.interface import Interface | ||||
| 
 | ||||
| 
 | ||||
| class ITraversable(Interface): | ||||
| 
 | ||||
|     def get(key, default=None): | ||||
|         """Return the item addressed by `key`; return `default` if not found.""" | ||||
| 
 | ||||
| 
 | ||||
| class IContainer(ITraversable): | ||||
| 
 | ||||
|     def values(): | ||||
|         """Return a sequence of child objects.""" | ||||
| 
 | ||||
|     def __getitem__(key): | ||||
|         """Return the item addressed by `key`; raise KeyError if not found.""" | ||||
| 
 | ||||
|     def __setitem__(key, value): | ||||
|         """Store the `value` under the `key`.  | ||||
| 
 | ||||
|         May modify `value` so that the attributes referencing this object | ||||
|         and the value object (e.g. `parent` and `name`) are stored correctly.""" | ||||
| 
 | ||||
| 
 | ||||
| class IReference(Interface): | ||||
| 
 | ||||
|     def getTarget(): | ||||
|         """Return item referenced by this object.""" | ||||
| 
 | ||||
|     def setTarget(target): | ||||
|         """Store reference to target item.""" | ||||
| 
 | ||||
| 
 | ||||
| class IView(Interface): | ||||
| 
 | ||||
|     def __call__(): | ||||
|         """Render the view data as HTML or JSON.""" | ||||
| 
 | ||||
|  | @ -2,43 +2,49 @@ | |||
| 
 | ||||
| from zope.publisher.base import DefaultPublication | ||||
| from zope.publisher.browser import BrowserRequest | ||||
| from zope.publisher.interfaces import NotFound | ||||
| from zope.publisher.publish import publish | ||||
| from zope.traversing.publicationtraverse import PublicationTraverser | ||||
| 
 | ||||
| from scopes.interfaces import ITraversable, IView | ||||
| from scopes.server.browser import getView | ||||
| import scopes.storage.concept # register container classes | ||||
| from scopes.storage.folder import Root | ||||
| 
 | ||||
| 
 | ||||
| def demo_app(environ, start_response): | ||||
|     print(f'*** environ {environ}.') | ||||
|     status = '200 OK' | ||||
|     headers = [("Content-type", "text/plain; charset=utf-8")] | ||||
|     start_response(status, headers) | ||||
|     return ['Hello World'.encode()] | ||||
| def zope_app_factory(config): | ||||
|     storageFactory = config.StorageFactory(config) | ||||
|     def zope_app(environ, start_response): | ||||
|         storage = storageFactory(config.dbschema) | ||||
|         appRoot = Root(storage) | ||||
|         request = BrowserRequest(environ['wsgi.input'], environ) | ||||
|         request.setPublication(Publication(appRoot)) | ||||
|         request = publish(request, True) | ||||
|         response = request.response | ||||
|         start_response(response.getStatusString(), response.getHeaders()) | ||||
|         return response.consumeBodyIter() | ||||
|     return zope_app | ||||
| 
 | ||||
| 
 | ||||
| def zope_app(environ, start_response): | ||||
|     request = BrowserRequest(environ['wsgi.input'], environ) | ||||
|     request.setPublication(DefaultPublication(AppRoot())) | ||||
|     request = publish(request, False) | ||||
|     response = request.response | ||||
|     start_response(response.getStatusString(), response.getHeaders()) | ||||
|     return response.consumeBodyIter() | ||||
| class Publication(DefaultPublication): | ||||
| 
 | ||||
|     def traverseName(self, request, ob, name): | ||||
|         next = getView(request, ob, name) | ||||
|         if next is not None: | ||||
|             return next | ||||
|         if ITraversable.providedBy(ob): | ||||
|             next = ob.get(name) | ||||
|         if next is None: | ||||
|             raise NotFound(ob, name, request) | ||||
|         return next | ||||
| 
 | ||||
| class AppRoot: | ||||
|     """Zope Demo AppRoot""" | ||||
| 
 | ||||
|     def __call__(self): | ||||
|         """calling AppRoot""" | ||||
|         return 'At root' | ||||
| 
 | ||||
|     def __getitem__(self, key): | ||||
|         def child(): | ||||
|             """get child""" | ||||
|             print(f'--- getitem {key}') | ||||
|             return 'getitem' | ||||
|         return child | ||||
| 
 | ||||
|     def hello(self):  | ||||
|         """method hello""" | ||||
|         return 'Hello AppRoot' | ||||
|     def getDefaultTraversal(self, request, ob): | ||||
|         if IView.providedBy(ob): | ||||
|             return ob, () | ||||
|         return ob, ('index.html',) | ||||
| 
 | ||||
|     def handleException(self, ob, request, exc_info, retry_allowed=True): | ||||
|         if exc_info[0] != NotFound: | ||||
|             raise | ||||
|         request.response.reset() | ||||
|         request.response.handleException(exc_info) | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										63
									
								
								scopes/server/browser.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								scopes/server/browser.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| # scopes.server.browser | ||||
| 
 | ||||
| import json | ||||
| from zope.interface import implementer | ||||
| from scopes.interfaces import IContainer, IReference, IView | ||||
| 
 | ||||
| views = {} # registry for all views: {name: {prefix: viewClass, ...}, ...} | ||||
| 
 | ||||
| def register(name, *contextClasses): | ||||
|     """Use as decorator: `@register(name, class, ...).  | ||||
|        class `None` means default view for all classes.""" | ||||
|     def doRegister(factory): | ||||
|         implementer(IView)(factory) | ||||
|         nameEntry = views.setdefault(name, {}) | ||||
|         for cl in contextClasses: | ||||
|             nameEntry[cl.prefix] = factory | ||||
|         else: | ||||
|             nameEntry[''] = factory | ||||
|         return factory | ||||
|     return doRegister | ||||
| 
 | ||||
| def getView(request, ob, name): | ||||
|     nameEntry = views.get(name) | ||||
|     if nameEntry is None: | ||||
|         return None | ||||
|     factory = nameEntry.get(ob.prefix) | ||||
|     if factory is None: | ||||
|         factory = nameEntry.get('') | ||||
|     if factory is None: | ||||
|         return None | ||||
|     return factory(ob, request) | ||||
| 
 | ||||
| 
 | ||||
| @register('index.html') | ||||
| @register('index.json') | ||||
| class DefaultView: | ||||
| 
 | ||||
|     def __init__(self, context, request): | ||||
|         self.context = context | ||||
|         self.request = request | ||||
| 
 | ||||
|     def __call__(self): | ||||
|         result = self.prepareResult() | ||||
|         return self.render(result) | ||||
| 
 | ||||
|     def prepareResult(self): | ||||
|         ob = self.context | ||||
|         result = ob.asDict() | ||||
|         if IContainer.providedBy(ob): | ||||
|             result['items'] = [v.asDict() for v in ob.values()] | ||||
|         if IReference.providedBy(ob): | ||||
|             target = ob.getTarget() | ||||
|             if target: | ||||
|                 result['target'] = target.asDict() | ||||
|                 if IContainer.providedBy(target): | ||||
|                     result['target']['items'] = [v.asDict() for v in target.values()] | ||||
|         return result | ||||
| 
 | ||||
|     def render(self, result): | ||||
|         self.request.response.setHeader('Content-type', 'application/json; charset=utf-8') | ||||
|         return json.dumps(result).encode('UTF-8') | ||||
| 
 | ||||
| 
 | ||||
|  | @ -33,13 +33,17 @@ class Storage(object): | |||
|     def add(self, container): | ||||
|         self.containers[container.itemFactory.prefix] = container | ||||
| 
 | ||||
|     def getItem(self, uid): | ||||
|         prefix, id = uid.split('-') | ||||
|         id = int(id) | ||||
|     def getContainer(self, itemClass): | ||||
|         prefix = itemClass.prefix | ||||
|         container = self.containers.get(prefix) | ||||
|         if container is None: | ||||
|             container = self.create(registry[prefix]) | ||||
|         return container.get(id) | ||||
|             return self.create(registry[prefix]) | ||||
|         return container | ||||
| 
 | ||||
|     def getItem(self, uid): | ||||
|         prefix, id = uid.split('-') | ||||
|         cls = registry[prefix].itemFactory | ||||
|         return self.getContainer(cls).get(int(id)) | ||||
| 
 | ||||
|     def getExistingTable(self, tableName): | ||||
|         metadata = self.metadata | ||||
|  |  | |||
|  | @ -1,12 +1,145 @@ | |||
| # scopes.storage.concept | ||||
| 
 | ||||
| """Abstract base classes for concept map application classes.""" | ||||
| """Core classes for concept map structure.""" | ||||
| 
 | ||||
| from scopes.storage.common import registerContainerClass | ||||
| from zope.interface import implementer | ||||
| from scopes.interfaces import IContainer | ||||
| from scopes.storage.common import registerContainerClass, registry | ||||
| from scopes.storage.tracking import Container, Track | ||||
| 
 | ||||
| defaultPredicate = 'standard' | ||||
| 
 | ||||
| 
 | ||||
| class Concept(Track): | ||||
| 
 | ||||
|     headFields = ['name'] | ||||
| 
 | ||||
|     def parents(self, predicate=None): | ||||
|         return (r.getFirst() for r in self.parentRels(predicate)) | ||||
| 
 | ||||
|     def parentRels(self, predicate=None): | ||||
|         return self.container.queryRels(second=self, predicate=predicate) | ||||
| 
 | ||||
|     def children(self, predicate=None): | ||||
|         return (r.getSecond() for r in self.childRels(predicate)) | ||||
| 
 | ||||
|     def childRels(self, predicate=None): | ||||
|         return self.container.queryRels(first=self, predicate=predicate) | ||||
| 
 | ||||
|     def values(self): | ||||
|         return self.children(defaultPredicate) | ||||
| 
 | ||||
|     def addChild(self, child, predicate=defaultPredicate): | ||||
|         rels = self.container.storage.getContainer(Triple) | ||||
|         rels.save(Triple(self.uid, child.uid, predicate)) | ||||
| 
 | ||||
| 
 | ||||
| class Concepts(Container): | ||||
| 
 | ||||
|     insertOnChange = False | ||||
|     indexes = None | ||||
| 
 | ||||
|     def queryRels(self, **crit): | ||||
|         #pred = crit.get(predicate) | ||||
|         #if pred is not None and isinstance(pred, ('string', 'bytes')): | ||||
|         #    crit['predicate'] = self.storage.getContainer(Predicate).queryLast(name=pred) | ||||
|         for k, v in crit.items(): | ||||
|             if isinstance(v, Track): | ||||
|                 crit[k] = v.uid | ||||
|         rels = self.storage.getContainer(Triple) | ||||
|         return rels.query(**crit) | ||||
|   | ||||
| 
 | ||||
|  # implementation of relationships between concepts using RDF-like triples | ||||
| 
 | ||||
| class Predicate(Concept): | ||||
| 
 | ||||
|     prefix = 'pred' | ||||
| 
 | ||||
| 
 | ||||
| @registerContainerClass | ||||
| class Predicates(Concepts): | ||||
| 
 | ||||
|     itemFactory = Predicate | ||||
|     tableName = 'preds' | ||||
| 
 | ||||
| 
 | ||||
| def storePredicate(storage, name): | ||||
|     preds = storage.getContainer(Predicate) | ||||
|     preds.save(Predicate(name)) | ||||
| 
 | ||||
| 
 | ||||
| class Triple(Track): | ||||
| 
 | ||||
|     headFields = ['first', 'second', 'predicate'] | ||||
|     prefix = 'rel' | ||||
| 
 | ||||
|     def getFirst(self): | ||||
|         return self.container.storage.getItem(self.first) | ||||
| 
 | ||||
|     def getSecond(self): | ||||
|         return self.container.storage.getItem(self.second) | ||||
| 
 | ||||
|     def getPredicate(self): | ||||
|         return self.container.storage.getItem(self.second) | ||||
| 
 | ||||
| 
 | ||||
| @registerContainerClass | ||||
| class Rels(Container): | ||||
| 
 | ||||
|     itemFactory = Triple | ||||
|     indexes = [('first', 'predicate', 'second'), | ||||
|                ('first', 'second'), ('predicate', 'second')] | ||||
|     tableName = 'rels' | ||||
|     insertOnChange = False | ||||
| 
 | ||||
| 
 | ||||
| # types stuff | ||||
| 
 | ||||
| @implementer(IContainer) | ||||
| class Type(Concept): | ||||
| 
 | ||||
|     headFields = ['name', 'tprefix'] | ||||
|     prefix = 'type' | ||||
| 
 | ||||
|     @property | ||||
|     def typeClass(self): | ||||
|         return registry[self.tprefix].itemFactory | ||||
| 
 | ||||
|     def values(self): | ||||
|         cont = self.container.storage.getContainer(self.typeClass) | ||||
|         return cont.query() | ||||
| 
 | ||||
|     def get(self, key, default=None): | ||||
|         cont = self.container.storage.getContainer(self.typeClass) | ||||
|         return cont.queryLast(name=key) or default | ||||
| 
 | ||||
|     def __getitem__(self, key): | ||||
|         value = self.get(key) | ||||
|         if value is None: | ||||
|             raise KeyError(key) | ||||
|         return value | ||||
| 
 | ||||
|     def __setitem__(self, key, value): | ||||
|         cont = self.container.storage.getContainer(self.typeClass) | ||||
|         value.name = key | ||||
|         cont.save(value) | ||||
| 
 | ||||
| 
 | ||||
| @registerContainerClass | ||||
| class Types(Concepts): | ||||
| 
 | ||||
|     itemFactory = Type | ||||
|     indexes = [('name',), ('tprefix',)] | ||||
|     tableName = 'types' | ||||
| 
 | ||||
| 
 | ||||
| def storeType(storage, cls, name): | ||||
|     types = storage.getContainer(Type) | ||||
|     types.save(Type(name, cls.prefix)) | ||||
| 
 | ||||
| def setupCoreTypes(storage): | ||||
|     for c in registry.values(): | ||||
|         cls = c.itemFactory | ||||
|         storeType(storage, cls, cls.__name__.lower()) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,17 +1,26 @@ | |||
| # scopes.storage.folder | ||||
| 
 | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from scopes.interfaces import IContainer, IReference | ||||
| from scopes.storage.common import registerContainerClass | ||||
| from scopes.storage.tracking import Container, Track | ||||
| 
 | ||||
| 
 | ||||
| @implementer(IContainer, IReference) | ||||
| class Folder(Track): | ||||
| 
 | ||||
|     headFields = ['parent', 'name', 'ref'] | ||||
|     prefix = 'fldr' | ||||
| 
 | ||||
|     def values(self): | ||||
|         return self.container.query(parent=self.rid) | ||||
| 
 | ||||
|     def items(self): | ||||
|         return ((v.name, v) for v in self.values()) | ||||
| 
 | ||||
|     def keys(self): | ||||
|         for f in self.container.query(parent=self.rid): | ||||
|             yield f.name | ||||
|         return (v.name for v in self.values()) | ||||
| 
 | ||||
|     def get(self, key, default=None): | ||||
|         value = self.container.queryLast(parent=self.rid, name=key) | ||||
|  | @ -20,7 +29,7 @@ class Folder(Track): | |||
|         return value | ||||
| 
 | ||||
|     def __getitem__(self, key): | ||||
|         value = self.container.queryLast(parent=self.rid, name=key) | ||||
|         value = self.get(key) | ||||
|         if value is None: | ||||
|             raise KeyError(key) | ||||
|         return value | ||||
|  | @ -30,6 +39,19 @@ class Folder(Track): | |||
|         value.set('name', key) | ||||
|         self.container.save(value) | ||||
| 
 | ||||
|     def getTarget(self): | ||||
|         if self.ref == '': | ||||
|             return None | ||||
|         return self.container.storage.getItem(self.ref) | ||||
| 
 | ||||
|     def setTarget(self, target): | ||||
|         self.set('ref', target.uid) | ||||
|         self.container.update(self) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return '%s: %s; keys: %s' % (self.__class__.__name__, | ||||
|                 self.name, list(self.keys())) | ||||
| 
 | ||||
| 
 | ||||
| class Root(Folder): | ||||
|     """A dummy (virtual) root folder for creating real folders | ||||
|  |  | |||
							
								
								
									
										18
									
								
								scopes/storage/topic.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								scopes/storage/topic.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| # scopes.storage.topic | ||||
| 
 | ||||
| from scopes.storage.common import registerContainerClass | ||||
| from scopes.storage import concept | ||||
| 
 | ||||
| class Topic(concept.Concept): | ||||
| 
 | ||||
|     prefix = 'tpc' | ||||
| 
 | ||||
| 
 | ||||
| @registerContainerClass | ||||
| class Topics(concept.Concepts): | ||||
| 
 | ||||
|     itemFactory = Topic | ||||
|     tableName = 'topics' | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -22,6 +22,9 @@ class Track(object): | |||
| 
 | ||||
|     def __init__(self, *keys, **kw): | ||||
|         self.head = {} | ||||
|         for k, v in kw.items(): | ||||
|             if k in self.headFields: | ||||
|                 self.head[k] = kw.pop(k) | ||||
|         for ix, k in enumerate(keys): | ||||
|             self.head[self.headFields[ix]] = k | ||||
|         for k in self.headFields: | ||||
|  | @ -62,6 +65,13 @@ class Track(object): | |||
|             return '' | ||||
|         return str(self.trackId) | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return '%s: %s' % (self.__class__.__name__, self.asDict()) | ||||
| 
 | ||||
|     def asDict(self): | ||||
|         return dict(uid=self.uid, head=self.head, data=self.data,  | ||||
|                     timeStamp = str(self.timeStamp)[:19]) | ||||
| 
 | ||||
| 
 | ||||
| @registerContainerClass | ||||
| class Container(object): | ||||
|  | @ -91,8 +101,11 @@ class Container(object): | |||
|         return tr | ||||
| 
 | ||||
|     def query(self, **crit): | ||||
|         stmt = self.table.select().where( | ||||
|         if crit: | ||||
|             stmt = self.table.select().where( | ||||
|                 and_(*self.setupWhere(crit))).order_by(self.table.c.trackid) | ||||
|         else: | ||||
|             stmt = self.table.select().order_by(self.table.c.trackid) | ||||
|         for r in self.session.execute(stmt): | ||||
|             yield self.makeTrack(r) | ||||
| 
 | ||||
|  | @ -102,16 +115,23 @@ class Container(object): | |||
|         return self.makeTrack(self.session.execute(stmt).first()) | ||||
| 
 | ||||
|     def save(self, track): | ||||
|         track.container = self | ||||
|         crit = dict((hf, track.head[hf]) for hf in track.headFields) | ||||
|         found = self.queryLast(**crit) | ||||
|         if found is None: | ||||
|             return self.insert(track) | ||||
|         if self.insertOnChange and found.data != track.data: | ||||
|             return self.insert(track) | ||||
|         if found.data != track.data or found.timeStamp != track.timeStamp: | ||||
|         changed = False | ||||
|         if found.data != track.data: | ||||
|             found.update(track.data) | ||||
|             changed = True | ||||
|         if track.timeStamp is not None and found.timeStamp != track.timeStamp: | ||||
|             found.timeStamp = track.timeStamp | ||||
|             changed = True | ||||
|         if changed: | ||||
|             self.update(found) | ||||
|         track.trackId = found.trackId | ||||
|         return found.trackId | ||||
| 
 | ||||
|     def insert(self, track, withTrackId=False): | ||||
|  | @ -119,14 +139,15 @@ class Container(object): | |||
|         values = self.setupValues(track, withTrackId) | ||||
|         stmt = t.insert().values(**values).returning(t.c.trackid) | ||||
|         trackId = self.session.execute(stmt).first()[0] | ||||
|         track.trackId = trackId | ||||
|         self.storage.mark_changed() | ||||
|         return trackId | ||||
| 
 | ||||
|     def update(self, track): | ||||
|     def update(self, track, updateTimeStamp=False): | ||||
|         t = self.table | ||||
|         if updateTimeStamp or track.timeStamp is None: | ||||
|             track.timeStamp = datetime.now() | ||||
|         values = self.setupValues(track) | ||||
|         if track.timeStamp is None: | ||||
|             values['timestamp'] = datetime.now() | ||||
|         stmt = t.update().values(**values).where(t.c.trackid == track.trackId) | ||||
|         n = self.session.execute(stmt).rowcount | ||||
|         if n > 0: | ||||
|  | @ -158,7 +179,7 @@ class Container(object): | |||
|                 *r[1:-2], trackId=r[0],timeStamp=r[-2], data=r[-1], container=self) | ||||
| 
 | ||||
|     def setupWhere(self, crit): | ||||
|         return [self.table.c[k.lower()] == v for k, v in crit.items()] | ||||
|         return [self.table.c[k.lower()] == v for k, v in crit.items() if v is not None] | ||||
| 
 | ||||
|     def setupValues(self, track, withTrackId=False): | ||||
|         values = {} | ||||
|  |  | |||
|  | @ -1,9 +1,10 @@ | |||
| # scopes/tests.py | ||||
| 
 | ||||
| """The real test implementations""" | ||||
| 
 | ||||
| from datetime import datetime | ||||
| import unittest | ||||
| 
 | ||||
| from scopes.storage import folder, tracking | ||||
| from scopes.storage import concept, folder, topic, tracking | ||||
| 
 | ||||
| import config | ||||
| config.dbengine = 'postgresql' | ||||
|  | @ -83,11 +84,45 @@ class Test(unittest.TestCase): | |||
|      | ||||
|         storage.commit() | ||||
|      | ||||
|     def test_003_type(self): | ||||
|         storage.dropTable('types') | ||||
|         concept.setupCoreTypes(storage) | ||||
|      | ||||
| def suite(): | ||||
|     return unittest.TestSuite(( | ||||
|         unittest.TestLoader().loadTestsFromTestCase(Test), | ||||
|     )) | ||||
|         types = storage.getContainer(concept.Type) | ||||
|         tps = list(types.query()) | ||||
|         self.assertEqual(len(tps), 6) | ||||
|         self.assertEqual(tps[0].name, 'topic') | ||||
|      | ||||
|         tfolder = types.queryLast(name='folder') | ||||
|         fldrs = list(tfolder.values()) | ||||
|         self.assertEqual(len(fldrs), 2) | ||||
|         self.assertEqual(fldrs[0].name, 'top') | ||||
|      | ||||
|         storage.commit() | ||||
|      | ||||
|     def test_004_topic(self): | ||||
|         storage.dropTable('topics') | ||||
|         topics = storage.getContainer(topic.Topic) | ||||
|         types = storage.getContainer(concept.Type) | ||||
|         concept.storePredicate(storage, concept.defaultPredicate) | ||||
|         root = folder.Root(storage) | ||||
|         root['top']['topics'] = ftopics = folder.Folder() | ||||
|         ttopic = types.queryLast(name='topic') | ||||
|         self.assertEqual(ttopic.name, 'topic') | ||||
|         ftopics.setTarget(ttopic) | ||||
|         self.assertEqual(ftopics.ref, 'type-1') | ||||
|      | ||||
|         tp_itc = topic.Topic('itc', data=dict( | ||||
|             title='ITC', description='Information and Communication Technology')) | ||||
|         topics.save(tp_itc) | ||||
|         tp_proglang = topic.Topic('prog_lang', data=dict( | ||||
|             title='Programming Languages',  | ||||
|             description='Programming Languages')) | ||||
|         topics.save(tp_proglang) | ||||
|         tp_itc.addChild(tp_proglang) | ||||
|      | ||||
|         c = list(tp_itc.children()) | ||||
|         self.assertEqual(c[0].name, 'prog_lang') | ||||
|      | ||||
|         storage.commit() | ||||
|   | ||||
| if __name__ == '__main__': | ||||
|     unittest.main(defaultTest='suite') | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue