# HG changeset patch # User Filip de Waard # Date 1273466678 -7200 # Node ID b513a1fbc45f2084a685ec54e271c911dd3edf75 # Parent c4fea38cbeb3a3712d5ce007896afef72315e735 working on model.Entry diff -r c4fea38cbeb3a3712d5ce007896afef72315e735 -r b513a1fbc45f2084a685ec54e271c911dd3edf75 vix/model/__init__.py --- a/vix/model/__init__.py Fri Apr 30 02:50:39 2010 +0200 +++ b/vix/model/__init__.py Mon May 10 06:44:38 2010 +0200 @@ -256,7 +256,7 @@ subtitle = mapping.TextField() published = mapping.DateTimeField() updated = mapping.DateTimeField() - + def create(self, db, authority, slug_suggestion=None): """ Creates the feed using the slug_suggestion value for the Atom ID. @@ -291,3 +291,162 @@ self.updated = datetime.utcnow() return super(Feed, self).store(db) + + +class Entry(mapping.Document): + """ + An Entry object describes a document in Vix, analogous to an Atom Entry. + + :param feed: TagURI's of feeds this document belongs to. + :type feed: list + :param title: Document title. + :param subtitle: Document subtitle. + :param authors: + List of dictionaries with 'name', 'uri' and 'email' as keys. + :param contributors: + List of dictionaries with 'name', 'uri' and 'email' as keys. + :param categories: (term, scheme, label) + List of dictionaries with 'term', 'scheme' and 'label' as keys. + The 'term' value is the category name (e.g. 'hacking'), + the 'scheme' value is an IRI that identifies the catergory scheme and + the 'label' value is the human readable label (e.g. 'Hacking'). + :param content: + Dictionary with 'type' and 'content' as keys containing the content + and content-type of the Entry Document. + :param summary: + Dictionary with 'type' and 'summary' as keys containing a summary + of the Entry Document. + :param published: Publish date of the Entry Document. + :type published: datetime + :param updated: Date of the last update to the Entry Document. + :type updated: datetime + :param draft: + Boolean that determines if the Entry Document is public or still + under construction. Draft Entry Documents aren't included in public + feeds and still private. + :param rights: License of the contents of the Entry Document. + :param in_reply_to: + Dictionary with 'ref', 'href', and 'type' as keys. The 'ref' value + corresponds to the TagURI of the resource this entry is replying to, + the href attribute contains a link to a representation of the + resource and the type attribute contains a hint about the media + type of the resource identified by the 'href' attritube. + + """ + + feeds = mapping.ListField(mapping.TextField()) + title = mapping.TextField() + subtitle = mapping.TextField() + summary = mapping.TextField() + published = mapping.DateTimeField() + updated = mapping.DateTimeField() + rights = mapping.TextField() + + content = mapping.DictField(mapping.Mapping.build( + content=mapping.TextField(), + type=mapping.TextField())) + + authors = mapping.ListField(mapping.DictField(mapping.Mapping.build( + name = mapping.TextField(), + uri = mapping.TextField(), + email = mapping.TextField()))) + + contributors = mapping.ListField(mapping.DictField(mapping.Mapping.build( + name = mapping.TextField(), + uri = mapping.TextField(), + email = mapping.TextField()))) + + categories = mapping.ListField(mapping.DictField(mapping.Mapping.build( + term = mapping.TextField(), + scheme = mapping.TextField(), + label = mapping.TextField()))) + + in_reply_to = mapping.ListField(mapping.DictField(mapping.Mapping.build( + ref = mapping.TextField(), + type = mapping.TextField(), + href = mapping.TextField()))) + + replies = mapping.ListField(mapping.DictField(mapping.Mapping.build( + title = mapping.TextField(), + ref = mapping.TextField(), + type = mapping.TextField(), + href = mapping.TextField()))) + + def create(self, db, authority, slug_suggestion=None): + """ + Creates the feed using the slug_suggestion value for the Atom ID. + If the particular ID is already in use the slug is incremented. + Also sets published DateTime stamp and slug value. + + :arg db: Database object. + :type db: couchdb.client.Database + :arg authority: TagURI authority for the active site (e.g. vix.io) + :arg slug_suggestion: Desired identifier of the resource. + + """ + + self.published = datetime.utcnow() + self.slug = u'/%04d/%02d/%02d/%s' % ( + self.published.year, self.published.month, self.published.day, + slug_suggestion or self.title) + + #prevent duplicate ID's + while len(db.view('by_slug/by_slug', key=self.slug)) > 0: + self.slug = util.increment_id(self.slug) + + self.id = unicode(util.TagURI( + authority_name=authority, + date=self.published, + specific=self.slug)) + + self.store(db) + + def store(self, db, silent=False): + """Stores the document with a fresh updated value. + + :arg silent: + determines if updated stamp should be refreshed + (default is False). + :type silent: Boolean + + """ + + #refresh self.updated stamp + if not silent: + self.updated = datetime.utcnow() + + #refresh etag/updated time on related feeds + for feed_id in self.feeds: + Feed().load(db, feed_id).store(db) + + #create replies backreferences + reply = {'title': self.title, + 'ref': self.id, + 'href': self.get_href(), + 'type': self.content['type']} + + for irt in self.in_reply_to: + #avoid self referential replies + if not irt['ref'] == self.id: + entry = Entry().load(db, irt['ref']) + + #don't do anything if the entry wasn't found + if entry: + #remove this entry to avoid duplication + entry.replies = [r for r in entry.replies + if not r['ref'] == self.id] + + #add a fresh reference to this entry + entry.replies.append(reply) + entry.store(db, silent=True) + + return super(Entry, self).store(db) + + def get_href(self): + """Returns a link to the entry.""" + + if self.id: + tag, auth_and_date, slug = self.id.split(':') + authority = auth_and_date[:auth_and_date.find(',')] + + return u'http://%s%s' % (authority, slug) diff -r c4fea38cbeb3a3712d5ce007896afef72315e735 -r b513a1fbc45f2084a685ec54e271c911dd3edf75 vix/tests/test_models.py --- a/vix/tests/test_models.py Fri Apr 30 02:50:39 2010 +0200 +++ b/vix/tests/test_models.py Mon May 10 06:44:38 2010 +0200 @@ -24,9 +24,9 @@ import time import datetime +import bcrypt import couchdb -import bcrypt - +from couchdb.mapping import DateTimeField from testfixtures import Replacer import vix.tests @@ -350,3 +350,265 @@ self.assertEquals(f3.slug, u'/feeds/blog-3') self.assertEquals(f3.id, u'tag:vix.io,2010-04-29:/feeds/blog-3') + + #TODO: assure that Feed.store throws exception w/o published + #TODO: assure that exception is raised w/o required elements + #TODO: create feed w/o slug suggestion + #TODO: assure slug_suggestion is normalized + #TODO: add following: + #categories (term, scheme, label) + #authors (name, uri, email) + #contributors (name, uri, email) + #icon + #logo + + def test_Entry(self): + """Test model.Entry object.""" + + #add by_slug CouchDB view + views.by_slug.sync(model.db) + + authors = [] + authors.append({ + 'name': 'Filip de Waard', + 'uri': 'http://vix.io', + 'email': 'fmw@vix.io'}) + authors.append({ + 'name': 'John Doe', + 'uri': 'http://www.example.com', + 'email': 'john.doe@example.com'}) + + contributors = [] + contributors.append({ + 'name': 'a', + 'uri': 'http://vix.io', + 'email': 'a@vix.io'}) + contributors.append({ + 'name': 'b', + 'uri': 'http://vix.io', + 'email': 'b@vix.io'}) + + categories = [] + categories.append({ + 'term': 'python', + 'label': 'Python Programming Language', + 'scheme': 'http://vix.io/schemes/python'}) + categories.append({ + 'term': 'pylons', + 'label': 'Pylons Framework', + 'scheme': None}) + categories.append({ + 'term': 'couchdb', + 'label': 'CouchDB', + 'scheme': None}) + + #thank you, Google Translate! + entry = model.Entry(feeds=[u'tag:vix.io,2010-05-04:/feeds/blog', + u'tag:vix.io,2010-05-04:/feeds/photo'], + title=u'Объявление Vix прототип!', + subtitle=u'Vix 0.1 άλφα απελευθερώνεται.', + content={ + 'type': 'text/plain', + 'content': u'今日はビクスのプロトタイプを公開しています。'}, + summary=u'لدينا حتى ملخص باللغة العربية!', + draft=False, + rights=u'Apache License, version 2', + authors=authors, + contributors=contributors, + categories=categories) + + blog_feed = model.Feed(title=u'Vix Weblog') + photo_feed = model.Feed(title=u'Vix Photos') + + with Replacer() as r: + r.replace('vix.model.datetime', _datetime( + datetime.datetime(2010, 5, 4, 7, 50, 51))) + + blog_feed.create(model.db, + slug_suggestion=u'blog', + authority=u'vix.io') + + photo_feed.create(model.db, + slug_suggestion=u'photo', + authority=u'vix.io') + + entry.create(model.db, + slug_suggestion=u'announcing-vix-prototype', + authority=u'vix.io') + + self.assertEquals(blog_feed.updated, + datetime.datetime(2010, 5, 4, 7, 50, 51)) + + self.assertEquals(photo_feed.updated, + datetime.datetime(2010, 5, 4, 7, 50, 51)) + r.replace('vix.model.datetime', datetime.datetime) + r.replace('vix.model.datetime', _datetime( + datetime.datetime(2010, 5, 4, 8, 38, 10))) + + #update entry to get new + #model.entry.updated + entry.store(model.db) + + e = model.db.get( + u'tag:vix.io,2010-05-04:/2010/05/04/announcing-vix-prototype') + + self.assertEquals(e['feeds'], [ + u'tag:vix.io,2010-05-04:/feeds/blog', + u'tag:vix.io,2010-05-04:/feeds/photo']) + + self.assertEquals(e['title'], u'Объявление Vix прототип!') + self.assertEquals(e['subtitle'], u'Vix 0.1 άλφα απελευθερώνεται.') + self.assertEquals(e['content'], {'type': 'text/plain', + 'content': u'今日はビクスのプロトタイプを公開しています。'}) + self.assertEquals(e['summary'], u'لدينا حتى ملخص باللغة العربية!') + self.assertEquals(e['rights'], 'Apache License, version 2') + + self.assertEquals(e['authors'], authors) + self.assertEquals(e['contributors'], contributors) + self.assertEquals(e['categories'], categories) + + self.assertEquals(DateTimeField()._to_python(e['published']), + datetime.datetime(2010, 5, 4, 7, 50, 51)) + self.assertEquals(DateTimeField()._to_python(e['updated']), + datetime.datetime(2010, 5, 4, 8, 38, 10)) + + #make sure the feed's updated stamp gets refreshed as well: + blog = model.db.get(u'tag:vix.io,2010-05-04:/feeds/blog') + photo = model.db.get(u'tag:vix.io,2010-05-04:/feeds/photo') + + self.assertEquals(DateTimeField()._to_python(blog['updated']), + datetime.datetime(2010, 5, 4, 8, 38, 10)) + self.assertEquals(DateTimeField()._to_python(photo['updated']), + datetime.datetime(2010, 5, 4, 8, 38, 10)) + + def test_Entry_in_reply_to(self): + """Test Entry in_reply_to and replies.""" + + #add by_slug CouchDB view + views.by_slug.sync(model.db) + + #add feed + feed = model.Feed(title=u'Vix Weblog') + + feed.create(model.db, + slug_suggestion=u'blog', + authority=u'vix.io') + + #add base entry + entry = model.Entry(feeds=[feed.id], + title=u'Hey!', + draft=False, + authors=[{'name': 'fmw'}]) + + with Replacer() as r: + r.replace('vix.model.datetime', _datetime( + datetime.datetime(2010, 5, 10, 4, 23, 52))) + + entry.create(model.db, authority='vix.io', slug_suggestion='hey') + + irt_entry = {'ref': entry.id, 'type': 'text/plain', + 'href': entry.get_href()} + + #add some replies + reply1 = model.Entry(feeds=[feed.id], + title=u'Hey, you!', + draft=False, + content={'type': 'text/plain', 'content': "What's up?"}, + authors=[{'name': 'fmw'}]) + reply1.in_reply_to = [irt_entry] + reply1.create(model.db, authority='vix.io', slug_suggestion='hey-you') + + irt_r1 = {'ref': reply1.id, 'href': reply1.get_href(), + 'type': 'text/plain'} + + reply2 = model.Entry(feeds=[feed.id], + title=u'Hi!', + draft=False, + authors=[{'name': 'fmw'}]) + reply2.in_reply_to = [irt_entry, irt_r1] + reply2.create(model.db, authority='vix.io', slug_suggestion='hi') + + #get entries + e = model.db.get(entry.id) + r1 = model.db.get(reply1.id) + r2 = model.db.get(reply2.id) + + self.assertEquals(r1['in_reply_to'], [irt_entry]) + self.assertEquals(r2['in_reply_to'], [irt_entry, irt_r1]) + + #check if replies backrefs are created (title, ref, href, type) + r1_r = {'title': reply1.title, 'ref': reply1.id, + 'href': reply1.get_href(), 'type': 'text/plain'} + r2_r = {'title': reply2.title, 'ref': reply2.id, + 'href': reply2.get_href(), 'type': None} + + self.assertEquals(e['replies'], [r2_r, r1_r]) + self.assertEquals(r1['replies'], [r2_r]) + + #make sure the updated stamp of the entry that is being replied to + #still remains as it was before the reply was added to the database. + self.assertEquals(DateTimeField()._to_python(e['updated']), + datetime.datetime(2010, 5, 10, 4, 23, 52)) + + #test what happens when replying to an entry that doesn't exist + does_not_exist = {'ref': reply1.id + u'does_not_exist', + 'href': reply1.get_href() + u'does_not_exist', + 'type': 'text/plain'} + reply3 = model.Entry(feeds=[feed.id], + title=u'Hey', + draft=False, + authors=[{'name': 'fmw'}]) + reply3.in_reply_to = [does_not_exist] + reply3.create(model.db, authority='vix.io', slug_suggestion='hello') + + #test what happens when adding an in_reply_to to an existing entry + irt_r2 = {'ref': reply2.id, 'href': reply2.get_href(), 'type': None} + + reply1_id = reply1.id + reply1 = model.Entry().load(model.db, reply1_id) + reply1.in_reply_to.append(irt_r2) + reply1.store(model.db) + + r1 = model.db.get(reply1.id) + r2 = model.db.get(reply2.id) + + self.assertEquals(r1.in_reply_to, [irt_entry, irt_r2]) + + def test_Entry_get_href(self): + """Test Entry().get_href() method.""" + + #add by_slug CouchDB view + views.by_slug.sync(model.db) + + #add feed + feed = model.Feed(title=u'Vix Weblog') + + feed.create(model.db, + slug_suggestion=u'blog', + authority=u'vix.io') + + entry = model.Entry(feeds=[feed.id], + title=u'Give me a link!', + draft=False, + authors=[{'name': 'fmw'}]) + + self.assertEquals(entry.get_href(), None) + + with Replacer() as r: + r.replace('vix.model.datetime', _datetime( + datetime.datetime(2010, 5, 10, 2, 55, 41))) + + entry.create(model.db, authority='vix.io', + slug_suggestion='give-me-a-link') + + self.assertEquals(entry.get_href(), + u'http://vix.io/2010/05/10/give-me-a-link') + + def test_Entry_validate(self): + pass #TODO: implement required fields and validation + + def test_Entry_comments(self): + pass #TODO + + def test_Entry_attachments(self): + pass #TODO