From 43a4d2e66f281a8a2906253f596b7e1789dd5efe Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Sun, 26 May 2013 18:49:20 +0200 Subject: [PATCH] add support for If-Unmodified-Since conditional PUT requests --- tests/base.py | 5 ++++ tests/test_rest_json.py | 50 ++++++++++++++++++++++++++++++++++++++-- tgext/crud/controller.py | 21 ++++++++++++++++- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/tests/base.py b/tests/base.py index 4bde0f6..c739486 100644 --- a/tests/base.py +++ b/tests/base.py @@ -7,6 +7,7 @@ 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()) @@ -14,6 +15,7 @@ DeclarativeBase = declarative_base() metadata = DeclarativeBase.metadata +MODIFICATION_DATE = datetime(2010, 1, 1, 12, 0, tzinfo=UTC) class Genre(DeclarativeBase): __tablename__ = "genres" @@ -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" diff --git a/tests/test_rest_json.py b/tests/test_rest_json.py index f8ab5f7..fdcb1ba 100644 --- a/tests/test_rest_json.py +++ b/tests/test_rest_json.py @@ -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 @@ -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 @@ -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'] diff --git a/tgext/crud/controller.py b/tgext/crud/controller.py index 78b1ef4..2778f54 100644 --- a/tgext/crud/controller.py +++ b/tgext/crud/controller.py @@ -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 { @@ -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)) @@ -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)) @@ -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))