Optional url segment pattern in a Pyramid route - python

I'm trying to create a website with optional url sub-paths:
/user - Returns general information on users
/user/edit - Edits the user
I've tried setting:
config.add_route('user', '/user/{action}')
#view_defaults(route_name="user")
class UserViews():
# not sure what (if anything) to put in #view_config here...
def user_general(self):
return Response("General User Info"
#view_config(match_param="action=edit")
def edit(self):
return Response("Editing user")
However while this works for /user/edit, it returns a 404 for /user
It also fails in the same way if I set 2 explicit routes with a shared path - e.g.:
config.add_route('login', '/user')
config.add_route('edit_user', '/user/edit')
I've tried things like setting match_params="action=" but can't get it to work.
Any ideas on how this can be achieved?

user_general inherits the default route configuration of the class, which requires an {action} match param. When you do not supply that in the request, the route for that view will never match, returning a 404 not found response.
You need to add a decorator with the route_name argument to user_general to override the default route for the view.
#view_config(
route_name="user"
)
def user_general(self):
The following works for me as a complete example with some minor explicit naming conventions.
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.view import view_config, view_defaults
#view_defaults(route_name="user_action")
class UserViews():
def __init__(self, context, request):
self.request = request
self.context = context
#view_config(
route_name="user_get",
request_method="GET"
)
def get_user(request):
return Response("I got you, Babe!")
#view_config(
match_param="action=edit"
)
def edit(self):
return Response("Don't ever change, Babe!")
if __name__ == "__main__":
with Configurator() as config:
config.add_route("user_get", "/user")
config.add_route('user_action', '/user/{action}')
config.scan()
app = config.make_wsgi_app()
server = make_server("0.0.0.0", 6543, app)
server.serve_forever()

Related

Generate URLs for Flask test client with url_for function

I'm trying to write unit tests for a Flask app using pytest. I have an app factory:
def create_app():
from flask import Flask
app = Flask(__name__)
app.config.from_object('config')
import os
app.secret_key = os.urandom(24)
from models import db
db.init_app(app)
return app
And a test class:
class TestViews(object):
#classmethod
def setup_class(cls):
cls.app = create_app()
cls.app.testing = True
cls.client = cls.app.test_client()
#classmethod
def teardown_class(cls):
cls.app_context.pop()
def test_create_user(self):
"""
Tests the creation of a new user.
"""
view = TestViews.client.get(url_for('create_users')).status_code == 200
but when I run my tests I get the following error:
RuntimeError: Attempted to generate a URL without the application context being pushed. This has to be executed when application context is available.
Googling this tells me (I think) that using the test client should create an automatic application context. What am I missing?
Making requests with the test client does indeed push an app context (indirectly). However, you're confusing the fact that url_for is visually inside the test request call with the idea that it is actually called inside. The url_for call is evaluated first, the result is passed to client.get.
url_for is typically for generating URLs within the app, unit tests are external. Typically, you just write exactly the URL you're trying to test in the request instead of generating it.
self.client.get('/users/create')
If you really want to use url_for here, you must do it in an app context. Note that when you're in an app context but not a request context, you must set the SERVER_NAME config and also pass _external=False. But again, you should probably just write out the URL you're trying to test.
app.config['SERVER_NAME'] = 'localhost'
with self.app.app_context():
url = url_for(..., _external=False)
self.client.get(url, ...)
You can call url_for() in test request context that created with app.test_request_context() method. There are three methods to achieve this.
With setup and teardown
Since you have created the setup and teardown method, just like what I normally do with unittest, you can just push a test request context in setup method then pop it in teardown method:
class TestViews(object):
#classmethod
def setup_class(cls):
cls.app = create_app()
cls.app.testing = True
cls.client = cls.app.test_client()
cls.context = cls.app.test_request_context() # create the context object
cls.context.push() # push the context
#classmethod
def teardown_class(cls):
cls.context.pop() # pop the context
def test_create_user(self):
"""
Tests the creation of a new user.
"""
view = TestViews.client.get(url_for('create_users')).status_code == 200
With pytest-flask
Besides, you can also just use pytest-flask. With pytest-flask, you can access to context bound objects (url_for, request, session) without context managers:
def test_app(client):
assert client.get(url_for('myview')).status_code == 200
With autouse fixture
If you don't want to install the plugin, you can just use the following fixtures to do similar things (stolen from the source of pytest-flask):
#pytest.fixture
def app():
app = create_app('testing')
return app
#pytest.fixture(autouse=True)
def _push_request_context(request, app):
ctx = app.test_request_context() # create context
ctx.push() # push
def teardown():
ctx.pop() # pop
request.addfinalizer(teardown) # set teardown

How do I automatically redirect a heroku app URL to my custom domain with Django?

I have a heroku app using django at example.herokuapp.com.
I also have a custom domain that points to this heroku app at example.com
How can I make it so that any time someone goes to example.herokuapp.com, it automatically redirects to my custom domain at example.com?
I essentially only want users to see the url example.com even if they type in example.herokuapp.com
Keep in mind that this is a django app. I could redirect every route to my custom domain, but I am wondering if there is any easier/better way to do this.
The simple solution is to just add middleware to the django app with a process_request() function that will be called everytime a route is requested.
I got the code from here: https://github.com/etianen/django-herokuapp/blob/master/herokuapp/middleware.py
Here is a file middelware.py that can be added:
from django.shortcuts import redirect
from django.core.exceptions import MiddlewareNotUsed
from django.conf import settings
SITE_DOMAIN = "example.com"
class CanonicalDomainMiddleware(object):
"""Middleware that redirects to a canonical domain."""
def __init__(self):
if settings.DEBUG or not SITE_DOMAIN:
raise MiddlewareNotUsed
def process_request(self, request):
"""If the request domain is not the canonical domain, redirect."""
hostname = request.get_host().split(":", 1)[0]
# Don't perform redirection for testing or local development.
if hostname in ("testserver", "localhost", "127.0.0.1"):
return
# Check against the site domain.
canonical_hostname = SITE_DOMAIN.split(":", 1)[0]
if hostname != canonical_hostname:
if request.is_secure():
canonical_url = "https://"
else:
canonical_url = "http://"
canonical_url += SITE_DOMAIN + request.get_full_path()
return redirect(canonical_url, permanent=True)
Lastly, be sure to add this class to the MIDDLEWARE_CLASSES list in the settings.py file.
It has been long time after #rishubk 's answer and I thought it can be better if I mention corrected CanonicalDomainMiddleware for currently Django 3.x and 4.x versions (and maybe for future ones).
Here how I changed, you can find:
https://github.com/berkaymizrak/Resume-Django-Web-App/blob/main/resume/CanonicalDomainMiddleware.py
As I see the basic changes are init function takes 2 arguments and you must use return self.get_response... if you want to do nothing instead of returning None.
from django.shortcuts import redirect
from django.core.exceptions import MiddlewareNotUsed
from django.conf import settings
class CanonicalDomainMiddleware(object):
"""Middleware that redirects to a canonical domain."""
def __init__(self, get_response):
self.get_response = get_response
if settings.DEBUG or not settings.SITE_DOMAIN:
raise MiddlewareNotUsed
def __call__(self, request):
"""If the request domain is not the canonical domain, redirect."""
hostname = request.get_host().split(":", 1)[0]
# Don't perform redirection for testing or local development.
if hostname in ("testserver", "localhost", "127.0.0.1"):
return self.get_response(request)
# Check against the site domain.
canonical_hostname = settings.SITE_DOMAIN.split(":", 1)[0]
if hostname == canonical_hostname:
return self.get_response(request)
else:
if request.is_secure():
canonical_url = "https://"
else:
canonical_url = "http://"
canonical_url += settings.SITE_DOMAIN + request.get_full_path()
return redirect(canonical_url, permanent=True)

Pyramid decorator chaining

In my pyramid application I am trying to implement authorization by decorating the view function.
When I use the config.scan() function none of the views are added, however if I explicitly add them using config.add_view() everything works fine.
I have two file one which defines all the view functions (views.py)
from pyramid.view import view_config
from pyramid.response import Response
from functools import wraps
def authorized(func): #decorator difnition
#wraps(func)
def new_func(request):
if(request.cookies.get('user')): # authorization
return func(request)
else:
return Response('not authirised')
return new_func
#view_config(route_name='hello') # view function being decorated
#authorized
def privileged_action(request):
return Response('Hello %(name)s!' % request.matchdict)
And another file to create the server (serve.py) which imports views.py
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from views import privileged_action
if __name__ == '__main__':
config = Configurator()
config.add_route('hello', '/hello/{name}')
# config.add_view(privileged_action, route_name='hello') # This works
config.scan() # This doesn't work
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever()
This gives 404 not found error if I access using 'http://localhost:8080/hello/a'
Why does this not work?
Is there any way to make this work?
Your code with the decorators looks fine.
The documentation for Configurator.scan() states for its first argument:
The package argument should be a Python package or module object (or a dotted Python name which refers to such a package or module). If package is None, the package of the caller is used.
So make sure you are doingconfig.scan(views), to get your web app dynamically adding your views.

Allow ALL method types in flask route

How can I allow a route to accept all types of methods?
I don't just want to route the standard methods like HEAD, GET, POST, OPTIONS, DELETE & PUT.
I would like it to also accept the following methods: FOOBAR, WHYISTHISMETHODNAMESOLONG & every other possible method names.
You can change the url_map directly for this, by adding a Rule with no methods:
from flask import Flask, request
import unittest
from werkzeug.routing import Rule
app = Flask(__name__)
app.url_map.add(Rule('/', endpoint='index'))
#app.endpoint('index')
def index():
return request.method
class TestMethod(unittest.TestCase):
def setUp(self):
self.client = app.test_client()
def test_custom_method(self):
resp = self.client.open('/', method='BACON')
self.assertEqual('BACON', resp.data)
if __name__ == '__main__':
unittest.main()
methods
A sequence of http methods this rule applies to. If not specified, all methods are allowed.
To quickly enable all HTTP Request Methods for a route without manually adding rules to the Flask url_map, modify the route definition as follows:
from flask import request
HTTP_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']
#app.route('/', methods=HTTP_METHODS)
def index():
return request.method
See below, which is some code (that I've trimmed down) from the Flask app object. This code handles adding a url rule (which is also what is called by flask when you do app.route() on your view)....
#setupmethod
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
""" I remove a ton the documentation here.... """
if endpoint is None:
endpoint = _endpoint_from_view_func(view_func)
options['endpoint'] = endpoint
methods = options.pop('methods', None)
# if the methods are not given and the view_func object knows its
# methods we can use that instead. If neither exists, we go with
# a tuple of only `GET` as default.
if methods is None:
methods = getattr(view_func, 'methods', None) or ('GET',)
methods = set(methods)
# ... SNIP a bunch more code...
rule = self.url_rule_class(rule, methods=methods, **options)
rule.provide_automatic_options = provide_automatic_options
self.url_map.add(rule)
As you can see, Flask will do it's darndest to ensure that the methods are explicitely defined. Now, Flask is based on Werkzeug, and the line...
rule = self.url_rule_class(rule, methods=methods, **options)
...typically uses Werkzeug's Rule class. This class has the following documentation for the "methods" argument...
A sequence of http methods this rule applies to. If not specified, all
methods are allowed.
So, this tells me that you MIGHT be able to do something like the following...
from werkzeug.routing import Rule
app = Flask(__name__)
def my_rule_wrapper(rule, **kwargs):
kwargs['methods'] = None
return Rule(rule, **kwargs)
app.url_rule_class = my_rule_wrapper
I haven't tested this out, but hopefully that can get you on the right track.
Edit:
Or you could just use DazWorrall's answer, which seems better :P

Can I avoid permission_required decorator on Django testing?

I am testing a set of Django applications which make extensive use of the 'permission_required' decorator. This derives in a 302 HTTP response in most of the views that I have.
My question is: there is any way to avoid or deactivate the 'permission_required' in testing, so I can get a 200 response when I call my views, instead of the 302?
Thank you!
Just log in with a superuser in your test case setUp method
from django.test import TestCase
from django.contrib.auth.models import User
class TestThatNeedsLogin(TestCase):
def setUp(self):
User.objects.create_superuser(
'user1',
'user1#example.com',
'pswd',
)
self.client.login(username="user1", password="pswd")
def tearDown(self):
self.client.logout()
def test_something(self):
response = self.client.get("/")
self.assertEqual(200, response.status_code)
You could monkey patch it:
import django.contrib.auth.decorators
real_permission_required = decorators.permission_required
# return a function that returns the exact function that was decorated, ignoring arguments
decorators.permission_required = lambda *args, **kwargs: lambda func: func
You need to make sure this happens before it's used, which is at the definition time of the object it's decorating. (For example, when that module is included.)
It also has to happen before it's rebound to another scope. After import django.contrib.auth.decorators is fine, but before from django.contrib.auth.decorators import permission_required.
Well. The solution I have found is to create a superuser in the setUp method from the TestCase class. I did it in that way:
def setUp(self):
self.client = Client()
self.user = User.objects.create_superuser(
'testuser',
'test#example.com',
'easy_password',
)
Then, when I want to test a URL, I do this:
def test_search_customers(self):
url = reverse('customer_search')
response = self.client.get(url)
# Not logged user. Must return a 302 HTTP code.
self.assertEquals(response.status_code, 302)
self.assertEquals(response['Location'], 'http://testserver/unauthorized/?next=/search/customers/')
# HERE I LOG IN MY SUPERUSER
self.client.login(username='testuser', password='easy_password')
response = self.client.get(url, follow=True)
# Same URL requested with a logged user with permissions. Must return 200 HTTP code.
self.assertEquals(response.status_code, 200)
This is what it worked for me :)
Thank you all. Cheers,
Jose

Categories

Resources