diff --git a/masonite/__version__.py b/masonite/__version__.py index bc5170ba4..9a15c16b8 100644 --- a/masonite/__version__.py +++ b/masonite/__version__.py @@ -2,7 +2,7 @@ __title__ = 'masonite' __description__ = 'The core for the Masonite framework' __url__ = 'https://github.com/MasoniteFramework/masonite' -__version__ = '2.2.19' +__version__ = '2.2.20' __author__ = 'Joseph Mancuso' __author_email__ = 'joe@masoniteproject.com' __licence__ = 'MIT' diff --git a/masonite/helpers/view_helpers.py b/masonite/helpers/view_helpers.py index 6a6e07f79..982f5fdef 100644 --- a/masonite/helpers/view_helpers.py +++ b/masonite/helpers/view_helpers.py @@ -33,3 +33,21 @@ def back(location=None): def hidden(value, name='hidden-input'): return Markup("".format(name, value)) + + +def old(session_key, default=''): + """Return the old value submitted by forms validated with Valitators. + + Arguments: + session_key {string} -- The key flashed to session. + + Returns: + string -- An input string. + """ + + from wsgi import container + session_container = container.make('Session') + + if session_container.has(session_key): + return session_container.get(session_key) + return default diff --git a/masonite/providers/HelpersProvider.py b/masonite/providers/HelpersProvider.py index ca1bf336a..6c74df535 100644 --- a/masonite/providers/HelpersProvider.py +++ b/masonite/providers/HelpersProvider.py @@ -4,7 +4,7 @@ import os from masonite.exception_handler import DD -from masonite.helpers.view_helpers import back, set_request_method, hidden +from masonite.helpers.view_helpers import back, set_request_method, hidden, old from masonite.helpers.sign import sign, unsign, decrypt, encrypt from masonite.helpers import config, optional from masonite.provider import ServiceProvider @@ -50,6 +50,7 @@ def boot(self, view: View, request: Request): 'hidden': hidden, 'exists': view.exists, 'cookie': request.get_cookie, - 'url': lambda name, params={}: request.route(name, params, full=True) + 'url': lambda name, params={}: request.route(name, params, full=True), + 'old': old } ) diff --git a/masonite/request.py b/masonite/request.py index 975d38f0e..40284f238 100644 --- a/masonite/request.py +++ b/masonite/request.py @@ -645,6 +645,10 @@ def redirect(self, route=None, params={}, name=None, controller=None, status=302 self.status(status) return self + def with_input(self): + self.flash_inputs_to_session() + return self + def redirect_to(self, route_name, params={}, status=302): """Redirect to a named route. @@ -785,6 +789,8 @@ def back(self, default=None): Returns: self """ + self.with_input() + redirect_url = self.input('__back') if not redirect_url and default: return self.redirect(default) @@ -793,6 +799,13 @@ def back(self, default=None): return self.redirect(redirect_url) + def flash_inputs_to_session(self): + if not hasattr(self, 'session'): + return + + for key, value in self.all().items(): + self.session.flash(key, value) + def is_named_route(self, name, params={}): """Check if the current URI is a specific named route. diff --git a/masonite/response.py b/masonite/response.py index 0578ce1f1..ea6d16160 100644 --- a/masonite/response.py +++ b/masonite/response.py @@ -8,6 +8,7 @@ from orator.support.collection import Collection from orator import Model +from orator import Paginator, LengthAwarePaginator from masonite.app import App @@ -37,6 +38,85 @@ def json(self, payload, status=200): return self.data() + def paginated_json(self, paginator, status=200): + """Determine type of paginated instance and return JSON response. + + Arguments: + paginator {Paginator|LengthAwarePaginator} -- + Either an Orator Paginator or LengthAwarePaginator object + + Returns: + string -- Returns a string representation of the data + """ + # configured param types + page_size_parameter = 'page_size' + page_parameter = 'page' + + # try to capture request input for page_size and/or page + page_size_input = self.request.input(page_size_parameter) + page_input = self.request.input(page_parameter) + # use try/except here, as int(bool) will return 0 for False above + try: + page_size = ( + int(page_size_input) + if page_size_input and int(page_size_input) > 0 + else paginator.per_page + ) + except Exception: + page_size = paginator.per_page + try: + page = ( + int(page_input) + if page_input and int(page_input) > 0 + else paginator.current_page + ) + except Exception: + page = paginator.current_page + + # don't waste time instantiating new paginator if no change + if ( + page_size != paginator.per_page + or page != paginator.current_page + ): + try: + # try to get class of model + next(type(x) for x in paginator.items) + if isinstance(paginator, Paginator): + paginator = model_class.simple_paginate( + page_size, + page + ) + elif isinstance(paginator, LengthAwarePaginator): + paginator = model_class.paginate(page_size, page) + except Exception: + paginator = paginator + + payload = { + 'total': ( + paginator.total + if isinstance(paginator, LengthAwarePaginator) + else None + ), + 'count': paginator.count(), + 'per_page': page_size, + 'current_page': page, + 'last_page': ( + paginator.last_page + if isinstance(paginator, LengthAwarePaginator) + else None + ), + 'from': (page_size * (page - 1)) + 1, + 'to': page_size * page, + 'data': paginator.serialize() + } + + # remove fields not relevant to Paginator instance + if isinstance(paginator, Paginator): + del payload['total'] + del payload['last_page'] + + return self.json(payload, status) + def make_headers(self, content_type="text/html; charset=utf-8"): """Make the appropriate headers based on changes made in controllers or middleware. @@ -99,6 +179,8 @@ def view(self, view, status=200): view = view.rendered_template elif isinstance(view, self.request.__class__): view = self.data() + if isinstance(view, (Paginator, LengthAwarePaginator)): + return self.paginated_json(view, status=self.request.get_status()) elif view is None: raise ResponseError('Responses cannot be of type: None.') diff --git a/masonite/testing/MockRoute.py b/masonite/testing/MockRoute.py index 7c5cadd96..4ee24f581 100644 --- a/masonite/testing/MockRoute.py +++ b/masonite/testing/MockRoute.py @@ -23,6 +23,13 @@ def hasController(self, controller): def contains(self, value): return value in self.container.make('Response') + def assertContains(self, value): + assert value in self.container.make('Response'), "Response does not contain {}".format(value) + return self + + def assertNotFound(self): + return self.assertIsStatus(404) + def ok(self): return '200 OK' in self.container.make('Request').get_status_code() @@ -43,9 +50,38 @@ def hasJson(self, key, value=''): return True return Dot().dot(key, response, False) + def assertHasJson(self, key, value): + response = json.loads(self.container.make('Response')) + if isinstance(key, dict): + for item_key, key_value in key.items(): + assert Dot().dot(item_key, response, False) == key_value + else: + assert Dot().dot(key, response, False) == value, "Key '{}' with the value of '{}' could not find a match in {}".format(key, value, response) + return self + + def assertJsonContains(self, key, value): + response = json.loads(self.container.make('Response')) + if not isinstance(response, list): + raise ValueError("This method can only be used if the response is a list of elements.") + + found = False + for element in response: + if Dot().dot(key, element, False): + assert Dot().dot(key, element, False) + found = True + + if not found: + raise AssertionError("Could not find a key of: {} that had the value of {}".format(key, value)) + return self + def count(self, amount): return len(json.loads(self.container.make('Response'))) == amount + def assertCount(self, amount): + response_amount = len(json.loads(self.container.make('Response'))) + assert response_amount == amount, 'Response has an count of {}. Asserted {}'.format(response_amount, amount) + return self + def amount(self, amount): return self.count(amount) @@ -63,6 +99,8 @@ def assertHasAmount(self, key, amount): except TypeError: raise TypeError("The json response key of: {} is not iterable but has the value of {}".format(key, response[key])) + return self + def assertNotHasAmount(self, key, amount): response = json.loads(self.container.make('Response')) try: @@ -70,6 +108,8 @@ def assertNotHasAmount(self, key, amount): except TypeError: raise TypeError("The json response key of: {} is not iterable but has the value of {}".format(key, response[key])) + return self + def user(self, obj): self._user = obj self.container.on_resolve(Request, self._bind_user_to_request) @@ -111,9 +151,21 @@ def assertParameterIs(self, key, value): def assertIsStatus(self, status): request = self.container.make('Request') - if not request.get_status_code() == status: + assert request.is_status(status), AssertionError("{} is not equal to {}".format(request.get_status_code(), status)) + if not request.is_status(status): raise AssertionError("{} is not equal to {}".format(request.get_status_code(), status)) + return self + + def assertHasHeader(self, key): + pass + + def assertHeaderIs(self, key, value): + request = self.container.make('Request') + assert str(request.header(key)) == str(value), AssertionError("{} is not equal to {}".format(request.header(key), value)) + + return self + def assertPathIs(self, url): path = self.container.make('Request').path assert path == url, "Asserting the path is '{}' but it is '{}'".format(url, path) diff --git a/tests/core/test_response.py b/tests/core/test_response.py index 4c0131e05..a56d8b132 100644 --- a/tests/core/test_response.py +++ b/tests/core/test_response.py @@ -1,8 +1,10 @@ import unittest +import json from orator import Model from orator.support.collection import Collection - +from orator import Paginator, LengthAwarePaginator +from app.User import User from app.http.controllers.TestController import \ TestController as ControllerTest from masonite.app import App @@ -10,9 +12,13 @@ from masonite.response import Response from masonite.testsuite import generate_wsgi from masonite.view import View +from masonite.testing import TestCase +from masonite.routes import Get +from config.factories import factory class MockUser(Model): + __table__ = 'users' def all(self): return Collection([ @@ -25,72 +31,195 @@ def find(self, _): self.email = 'user@email.com' return self +class MockController: -class TestResponse(unittest.TestCase): + def test_json(self, response: Response): + return response.json({'test': 'value'}) - def setUp(self): - self.app = App() - self.request = Request(generate_wsgi()).load_app(self.app) - self.app.bind('Request', self.request) - self.app.bind('StatusCode', None) - self.response = Response(self.app) - self.app.bind('Response', self.response) + def redirect(self, response: Response): + return response.redirect('/some/test') - def test_can_set_json(self): - self.response.json({'test': 'value'}) + def view(self, view: View): + return view.render('test', {'test': 'test'}) - self.assertTrue(self.request.is_status(200)) - self.assertEqual(self.request.header('Content-Length'), '17') - self.assertEqual(self.request.header('Content-Type'), 'application/json; charset=utf-8') + def response_int(self, response: Response): + return response.view(1) - def test_redirect(self): - self.response.redirect('/some/test') + def all_users(self): + return MockUser().all() - self.request.header('Location', '/some/test') - self.assertTrue(self.request.is_status(302)) - self.assertEqual(self.request.header('Location'), '/some/test') + def paginate(self): + return MockUser.paginate(10) - def test_response_does_not_override_header_from_controller(self): - self.response.view(self.app.resolve(ControllerTest().change_header)) + def single(self): + return User.find(1) - self.assertEqual(self.request.header('Content-Type'), 'application/xml') + def length_aware(self): + return LengthAwarePaginator(User.find(1), 1, 10) - def test_view(self): - view = View(self.app).render('test', {'test': 'test'}) + def paginator(self): + return Paginator(User.all(), 10) - self.response.view(view) + def single_paginator(self): + return Paginator(User.find(1), 10) - self.assertEqual(self.app.make('Response'), 'test') - self.assertTrue(self.request.is_status(200)) - self.response.view('foobar') +class TestResponse(TestCase): - self.assertEqual(self.app.make('Response'), 'foobar') - - def test_view_can_return_integer_as_string(self): - self.response.view(1) - - self.assertEqual(self.app.make('Response'), '1') - self.assertTrue(self.request.is_status(200)) + def setUp(self): + super().setUp() + self.routes(only=[ + Get('/json', MockController.test_json), + Get('/redirect', MockController.redirect), + Get('/change/header', ControllerTest.change_header), + Get('/view', MockController.view), + Get('/int', MockController.response_int), + Get('/404', ControllerTest.change_404), + Get('/change/status', ControllerTest.change_status), + Get('/users', MockController.all_users), + Get('/paginate', MockController.paginate), + Get('/paginator', MockController.paginator), + Get('/single_paginator', MockController.single_paginator), + Get('/single', MockController.single), + Get('/length_aware', MockController.length_aware), + ]) + + def setUpFactories(self): + factory(User, 50).create() - def test_view_can_set_own_status_code_to_404(self): - self.response.view(self.app.resolve(ControllerTest().change_404)) - self.assertTrue(self.request.is_status(404)) + def test_can_set_json(self): + ( + self.json('GET', '/json') + .assertIsStatus(200) + .assertHeaderIs('Content-Length', 17) + .assertHeaderIs('Content-Type', 'application/json; charset=utf-8') + ) - def test_view_can_set_own_status_code(self): + def test_redirect(self): + ( + self.get('/redirect') + .assertHeaderIs('Location', '/some/test') + .assertIsStatus(302) + ) - self.response.view(self.app.resolve(ControllerTest().change_status)) - self.assertTrue(self.request.is_status(203)) + def test_response_does_not_override_header_from_controller(self): + ( + self.get('/change/header') + .assertHeaderIs('Content-Type', 'application/xml') + ) - def test_view_should_return_a_json_response_when_retrieve_a_user_from_model(self): + def test_view(self): + ( + self.get('/view') + .assertContains('test') + .assertIsStatus(200) + ) - self.assertIsInstance(MockUser(), Model) - self.response.view(MockUser().all()) + def test_view_can_return_integer_as_string(self): + ( + self.get('/int') + .assertContains('1') + .assertIsStatus(200) + ) - self.assertIn('"name": "TestUser"', self.app.make('Response')) - self.assertIn('"email": "user@email.com"', self.app.make('Response')) + def test_view_can_set_own_status_code_to_404(self): + ( + self.get('/404') + .assertNotFound() + ) - self.response.view(MockUser().find(1)) + def test_view_can_set_own_status_code(self): + ( + self.get('/change/status') + .assertIsStatus(203) + ) - self.assertIn('"name": "TestUser"', self.app.make('Response')) - self.assertIn('"email": "user@email.com"', self.app.make('Response')) + def test_view_should_return_a_json_response_when_retrieve_a_user_from_model(self): + ( + self.json('GET', '/users') + .assertCount(2) + .assertJsonContains('name', 'TestUser') + .assertJsonContains('email', 'user@email.com') + ) + + + def test_view_should_return_a_json_response_when_returning_length_aware_paginator_instance(self): + + users = User.all() + + # Page 1 + ( + self.get('/paginate') + .assertHasJson('total', len(users)) + .assertHasJson('count', 10) + .assertHasJson('per_page', 10) + .assertHasJson('current_page', 1) + .assertHasJson('from', 1) + .assertHasJson('to', 10) + ) + + # Page 2 + ( + self.get('/paginate', {'page': 2}) + .assertHasJson('total', len(users)) + .assertHasJson('count', 10) + .assertHasJson('per_page', 10) + .assertHasJson('current_page', 2) + .assertHasJson('from', 11) + .assertHasJson('to', 20) + ) + + factory(User).create() + ( + self.get('/length_aware') + .assertHasJson('total', 1) + .assertHasJson('count', 1) + ) + + + def test_view_should_return_a_json_response_when_returning_paginator_instance(self): + + users = User.all() + ( + self.get('/paginator') + .assertHasJson('count', 10) + .assertHasJson('per_page', 10) + .assertHasJson('current_page', 1) + .assertHasJson('from', 1) + .assertHasJson('to', 10) + ) + + + def test_can_correct_incorrect_pagination_page(self): + users = User.all() + ( + self.get('/paginator') + .assertHasJson('count', 10) + .assertHasJson('per_page', 10) + .assertHasJson('current_page', 1) + .assertHasJson('from', 1) + .assertHasJson('to', 10) + ) + + (self.get('/paginate', {'page': 'hey', 'page_size': 'hey'}) + .assertHasJson('total', len(users)) + .assertHasJson('count', 10) + .assertHasJson('per_page', 10) + .assertHasJson('current_page', 1) + .assertHasJson('from', 1) + .assertHasJson('to', 10)) + + (self.get('/length_aware', {'page': 'hey', 'page_size': 'hey'}) + # .assertHasJson('total', len(User.find(1))) + # .assertHasJson('count', len(User.find(1))) + .assertHasJson('per_page', 10) + .assertHasJson('current_page', 1) + .assertHasJson('from', 1) + .assertHasJson('to', 10)) + + (self.get('/single_paginator', {'page': 'hey', 'page_size': 'hey'}) + .assertHasJson('count', 1) + .assertHasJson('per_page', 10) + .assertHasJson('current_page', 1) + .assertHasJson('from', 1) + .assertHasJson('to', 10)) diff --git a/tests/core/test_session.py b/tests/core/test_session.py index 292bd2a25..0ccabd158 100644 --- a/tests/core/test_session.py +++ b/tests/core/test_session.py @@ -122,3 +122,15 @@ def test_delete_session(self): self.assertTrue(session.delete('test1')) self.assertFalse(session.has('test1')) self.assertFalse(session.delete('test1')) + + def test_can_redirect_with_inputs(self): + for driver in ('memory', 'cookie'): + request = self.app.make('Request') + request.request_variables = { + 'key1': 'val1', + 'key2': 'val2', + } + request.with_input() + session = self.app.make('SessionManager').driver(driver) + self.assertFalse(session.has('key1')) + self.assertFalse(session.has('key2'))