Skip to content
This repository has been archived by the owner on May 3, 2023. It is now read-only.

Commit

Permalink
Post versioning (Part of #52)
Browse files Browse the repository at this point in the history
  • Loading branch information
borntyping committed Feb 1, 2013
1 parent 10030c3 commit 695d925
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 54 deletions.
13 changes: 5 additions & 8 deletions tentd/blueprints/posts.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,17 @@ def post(self):
TODO: Separate between apps creating a new post and a notification
from a non-followed entity.
"""
new_post = Post()
new_post.entity = g.entity
new_post.schema = request.json['schema']
new_post.content = request.json['content']

new_post.save()
post = Post(entity=g.entity, schema=request.json.pop('schema'))
post.new_version(**request.json)
post.save()

# TODO: Do this asynchronously?
for to_notify in g.entity.followers:
notification_link = follow.get_notification_link(to_notify)
requests.post(notification_link, data=jsonify(new_post.to_json()))
requests.post(notification_link, data=jsonify(post.to_json()))
# TODO: Handle failed notifications somehow

return jsonify(new_post)
return jsonify(post)

@posts.route_class('/<string:post_id>', endpoint='post')
class PostsView(MethodView):
Expand Down
99 changes: 73 additions & 26 deletions tentd/documents/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,46 @@

__all__ = ['Post']

from datetime import datetime

from mongoengine import *

from tentd.documents import db, EntityMixin
from tentd.utils import time_to_string, json_attributes

class Version(db.EmbeddedDocument):
"""A specific version of a Post
TODO: 'mentions'
TODO: 'licenses'
TODO: 'attachments'
TODO: 'app'
TODO: 'views'
TODO: 'permissions'
"""
meta = {
'allow_inheritance': False,
}

#: The time the post was published
published_at = DateTimeField(default=datetime.now, required=True)

#: The time we received the post from the publishing server
received_at = DateTimeField(default=datetime.now, required=True)

#: The content of the post
content = DictField(required=True)

def __repr__(self):
return '<Version {}: {}'.format(self.version, self.content)

def to_json(self):
return {
'content': self.content,
'published_at': time_to_string(self.published_at),
'received_at': time_to_string(self.received_at),
}

class Post(EntityMixin, db.Document):
"""A post belonging to an entity.
Expand All @@ -19,36 +54,48 @@ class Post(EntityMixin, db.Document):
meta = {
'allow_inheritance': False,
'indexes': ['schema'],
'ordering': ['-published_at'],
'ordering': ['-received_at'],
}

#: The post type
schema = URLField(required=True)

#: The content of the post
content = DictField(required=True)

#: The time the post was published
published_at = DateTimeField()

#: The time we received the post from the publishing server
received_at = DateTimeField()
#: The versions of the post
versions = SortedListField(
EmbeddedDocumentField(Version),
ordering='published_at', reverse=True, required=True)

def __init__(self, **kwargs):
super(Post, self).__init__(**kwargs)

@classmethod
def new(cls, **k):
"""Constucts a Post and an initial version from the same args"""
# Pop those arguments that are a Version field from the kwargs
version = {a: k.pop(a) for a in k.keys() if a in Version._fields}
# Create a new Post with the remaining arguments
post = cls(**k)
# And a new Version from the arguments we popped
post.new_version(**version)
return post

def new_version(self, **kwargs):
"""Add a new version of the post"""
version = Version(**kwargs)
self.versions.append(version)
return version

@property
def latest(self):
return self.versions[0]

def to_json(self):
"""Returns the post as a python dictonary
TODO: 'mentions'
TODO: 'licenses'
TODO: 'attachments'
TODO: 'app'
TODO: 'views'
TODO: 'permissions'
"""
return json_attributes(self,
('id', str),
'content',
('published_at', time_to_string),
('received_at', time_to_string),
entity=self.entity.core.identity,
type=self.schema
)
"""Returns the post as a python dictonary"""
json = {
'id': self.id,
'type': self.schema,
'entity': self.entity.core.identity,
'version': len(self.versions),
}
json.update(self.latest.to_json())
return json
4 changes: 4 additions & 0 deletions tentd/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import flask

from flask import abort, g, url_for, json, current_app, request
from bson import ObjectId
from mongoengine.queryset import QuerySet
from werkzeug.utils import cached_property
from werkzeug.exceptions import NotFound
Expand Down Expand Up @@ -115,6 +116,9 @@ def default(self, obj):

if isinstance(obj, (list, QuerySet)):
return [self.default(o) for o in obj]

if isinstance(obj, ObjectId):
return str(obj)

return super(JSONEncoder, self).default(obj)

Expand Down
24 changes: 12 additions & 12 deletions tentd/tests/blueprints/test_posts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
from json import dumps

from tentd.documents.entity import Post
from tentd.flask import jsonify
from tentd.tests import EntityTentdTestCase

class PostTests(EntityTentdTestCase):
"""Tests relating to the post routes."""

def before(self):
"""Create a post in the DB."""
self.new_post = Post()
self.new_post.schema = 'https://tent.io/types/post/status/v0.1.0'
self.new_post.content = {'text': 'test', 'location': None}
self.new_post.entity = self.entity
self.new_post.save()
self.new_post = Post.new(
entity=self.entity,
schema='https://tent.io/types/post/status/v0.1.0',
content={'text': 'test', 'location': None}).save()

def test_entity_header_posts(self):
"""Test the entity header is returned from the posts route."""
Expand All @@ -26,23 +26,23 @@ def test_entity_get_posts(self):
self.assertStatus(resp, 200)

posts = [post.to_json() for post in self.entity.posts]
self.assertEquals(resp.json(), posts)
self.assertEquals(resp.data, jsonify(posts).data)

def test_entity_new_post(self):
"""Test that a new post can be added correctly."""
post_details = {
details = {
'schema': 'https://tent.io/types/post/status/v0.1.0',
'content': {'text': 'test', 'location': None}}

resp = self.client.post('/{}/posts'.format(self.name),
data=dumps(post_details))
data=dumps(details))

self.assertStatus(resp, 200)

created_post = self.entity.posts.get(id=resp.json()['id'])
self.assertIsNotNone(created_post)
self.assertEquals(created_post.schema, post_details['schema'])
self.assertEquals(created_post.content, post_details['content'])
self.assertEquals(created_post.schema, details['schema'])
self.assertEquals(created_post.latest.content, details['content'])

def test_entity_create_invalid_post(self):
"""Test that attempting to create an invalid post fails."""
Expand All @@ -56,7 +56,7 @@ def test_entity_get_single_post(self):
'/{}/posts/{}'.format(self.name, self.new_post.id))

self.assertStatus(resp, 200)
self.assertEquals(resp.json(), self.new_post.to_json())
self.assertEquals(resp.data, jsonify(self.new_post).data)

def test_entity_update_single_post(self):
"""Test a single post can be updated."""
Expand All @@ -69,7 +69,7 @@ def test_entity_update_single_post(self):

new_post = Post.objects.get(entity=self.entity)
self.assertStatus(resp, 200)
self.assertEquals(resp.json(), new_post.to_json())
self.assertEquals(resp.data, jsonify(new_post).data)

def test_entity_update_post_invalid(self):
"""Test that attempting to update an invalid post fails."""
Expand Down
5 changes: 3 additions & 2 deletions tentd/tests/documents/test_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ def test_create_entity(self):
assert self.entity == Entity.objects.get(name="testuser")

def test_create_identical_entity(self):
"""Check that properly inserting a document does not overwrite an existing one"""
"""Check that properly inserting a document does not overwrite an
existing one"""
with self.assertRaises(db.NotUniqueError):
entity = Entity(name="testuser")
entity.save()
Expand All @@ -28,7 +29,7 @@ class DeletionTest(TentdTestCase):

def test_post_delete(self):
self.entity = Entity(name="testuser").save()
self.post = Post(
self.post = Post.new(
entity=self.entity,
schema="http://example.com/thisisnotrelevant",
content={'a': 'b'}).save()
Expand Down
30 changes: 24 additions & 6 deletions tentd/tests/documents/test_posts.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,22 @@

class PostTest(EntityTentdTestCase):
def before(self):
"""Create a post with several versions"""
self.post = Post(
entity=self.entity,
schema='https://tent.io/types/post/status/v0.1.0',
content={'text': "Hello world"})
schema='https://tent.io/types/post/status/v0.1.0')
self.post.new_version(content={
'text': "Hello world",
'coordinates': [0, 0],
})
self.post.new_version(content={
'text': "How are you, world?",
'coordinates': [1, 1],
})
self.post.new_version(content={
'text': "Goodbye world",
'coordinates': [2, 2],
})
self.post.save()

def test_post_owner(self):
Expand All @@ -21,14 +33,20 @@ def test_post_owner(self):

def test_post_content(self):
post = Post.objects.get(entity=self.entity)
assert post.content['text'] == "Hello world"
assert post.latest.content['text'] == "Goodbye world"
assert post.latest.content['coordinates'] == [2, 2]

def test_post_json(self):
"""Test that posts can be exported to json"""
assert 'content' in self.post.to_json()

def test_post_versions(self):
"""Test that the versions are numbered correctly"""
assert self.post.to_json()['version'] == 3

class UnicodePostTest(EntityTentdTestCase):
def before(self):
self.essay = Post(
self.essay = Post.new(
entity=self.entity,
schema='https://tent.io/types/post/status/v0.1.0',
content={
Expand All @@ -44,5 +62,5 @@ def before(self):
self.essay.save()

def test_post_content(self):
assert "⛺" in self.essay.content['title']
assert "⛺" in self.essay.content['body']
assert "⛺" in self.essay.latest.content['title']
assert "⛺" in self.essay.latest.content['body']

0 comments on commit 695d925

Please sign in to comment.