Skip to content

Commit

Permalink
add support for If-Unmodified-Since conditional PUT requests
Browse files Browse the repository at this point in the history
  • Loading branch information
amol- committed May 26, 2013
1 parent 854c4e1 commit 43a4d2e
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 3 deletions.
5 changes: 5 additions & 0 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
from sqlalchemy.orm import scoped_session, sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
from webob import UTC

maker = sessionmaker(autoflush=True, autocommit=False,
extension=ZopeTransactionExtension())
DBSession = scoped_session(maker)
DeclarativeBase = declarative_base()
metadata = DeclarativeBase.metadata

MODIFICATION_DATE = datetime(2010, 1, 1, 12, 0, tzinfo=UTC)

class Genre(DeclarativeBase):
__tablename__ = "genres"
Expand All @@ -33,6 +35,9 @@ class Movie(DeclarativeBase):
genre_id = Column(Integer, ForeignKey(Genre.genre_id), nullable=True)
genre = relationship(Genre)

@property
def updated_at(self):
return MODIFICATION_DATE

class Actor(DeclarativeBase):
__tablename__ = "actors"
Expand Down
50 changes: 48 additions & 2 deletions tests/test_rest_json.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from tg import TGController
from tgext.crud import EasyCrudRestController
from .base import CrudTest, Movie, DBSession, metadata, Genre, Actor
from .base import CrudTest, Movie, DBSession, metadata, Genre, Actor, MODIFICATION_DATE
from webob import serialize_date

import transaction

Expand Down Expand Up @@ -131,7 +132,7 @@ def test_delete_nofilter(self):

class TestRestJsonRead(CrudTest):
"""Basic tests for GET requests with JSON responses"""

def controller_factory(self):
class MovieController(EasyCrudRestController):
model = Movie
Expand Down Expand Up @@ -377,3 +378,48 @@ def test_put_relationship(self):
assert len(result.json['value']['actors']) == 2, result

assert DBSession.query(Actor).filter_by(movie_id=movie['movie_id']).count() == 2

class TestRestJsonConditionalPut(CrudTest):
def controller_factory(self):
class MovieController(EasyCrudRestController):
model = Movie
conditional_update_field = 'updated_at'

class RestJsonController(TGController):
movies = MovieController(DBSession)

return RestJsonController()

def test_put_failed(self):
result = self.app.post_json('/movies.json', params={'title':'Movie Test'})
movie = result.json['value']
assert result.last_modified == MODIFICATION_DATE

previous_date = MODIFICATION_DATE.replace(year=result.last_modified.year-1)
result = self.app.put_json('/movies/%s.json' % movie['movie_id'], status=412,
headers=[('If-Unmodified-Since', serialize_date(previous_date))],
params={'title':'New Title'})
assert result.json['value']['title'] == 'Movie Test'

def test_put(self):
result = self.app.post_json('/movies.json', params={'title':'Movie Test'})
movie = result.json['value']
assert result.last_modified == MODIFICATION_DATE

previous_date = MODIFICATION_DATE.replace(year=result.last_modified.year+1)
result = self.app.put_json('/movies/%s.json' % movie['movie_id'], status=200,
headers=[('If-Unmodified-Since', serialize_date(previous_date))],
params={'title':'New Title'})
assert result.json['value']['title'] == 'New Title'

def test_get_one(self):
result = self.app.post_json('/movies.json', params={'title':'Movie Test'})
orig_movie = result.json['value']

result = self.app.get('/movies/%s.json' % orig_movie['movie_id'])
assert result.last_modified == MODIFICATION_DATE

movie = result.json
assert movie['model'] == 'Movie', result
assert movie['value']['title'] == orig_movie['title']
assert movie['value']['movie_id'] == orig_movie['movie_id']
21 changes: 20 additions & 1 deletion tgext/crud/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class CrudRestController(RestController):
substring_filters = []
search_fields = True # True for automagic
json_dictify = False # True is slower but provides relations
conditional_update_field = None
pagination = {'items_per_page': 7}
style = Markup('''
#menu_items {
Expand Down Expand Up @@ -306,6 +307,9 @@ def get_one(self, *args, **kw):
obj = self.provider.get_obj(self.model, kw)
if obj is None:
tg.response.status_code = 404
elif self.conditional_update_field is not None:
tg.response.last_modified = getattr(obj, self.conditional_update_field)

return dict(model=self.model.__name__,
value=self._dictify(obj))

Expand Down Expand Up @@ -344,6 +348,9 @@ def post(self, *args, **kw):
obj = self.provider.create(self.model, params=kw)

if tg.request.response_type == 'application/json':
if obj is not None and self.conditional_update_field is not None:
tg.response.last_modified = getattr(obj, self.conditional_update_field)

return dict(model=self.model.__name__,
value=self._dictify(obj))

Expand All @@ -368,12 +375,24 @@ def put(self, *args, **kw):

obj = self.provider.get_obj(self.model, kw)

if obj is not None:
#This should actually by done by provider.update to make it atomic
can_modify = True
if obj is not None and self.conditional_update_field is not None and \
tg.request.if_unmodified_since is not None and \
tg.request.if_unmodified_since < getattr(obj, self.conditional_update_field):
can_modify = False

if obj is not None and can_modify:
obj = self.provider.update(self.model, params=kw, omit_fields=omit_fields)

if tg.request.response_type == 'application/json':
if obj is None:
tg.response.status_code = 404
elif can_modify is False:
tg.response.status_code = 412
elif self.conditional_update_field is not None:
tg.response.last_modified = getattr(obj, self.conditional_update_field)

return dict(model=self.model.__name__,
value=self._dictify(obj))

Expand Down

0 comments on commit 43a4d2e

Please sign in to comment.