Python Pyramid traversal - python

I've been trying out pyramid and this traversal thing is sending me nuts. I'm basically fiddling around to make a control panel for a shopping cart and this is the basic structure that I have in mind.
The login page
localhost:6543/admin_login
Upon successful login
localhost:6543/admin/home
To view all existing products
localhost:6543/admin/product
To edit product X
localhost:6543/admin/product/edit/1
So my folder structure is something like this ( Capitalize files are models )
mycart
resources.py
Admin.py
Product.py
static
templates
views
__init__.py
admin.py
root.py
My resources.py
from pyramid.security import Authenticated
from pyramid.security import Allow
from pyramid.response import Response
class Root(object):
__name__ = ''
__parent__ = None
def __init__(self, request):
pass
def __getitem__(self, key):
if key == 'admin_login':
return Admin()
elif key == 'admin':
return Admin()
raise KeyError
class Admin(object):
__name__ = ''
__parent__ = Root
__acl__ = [(Allow, Authenticated, 'admin')]
def __init__(self):
pass
In views/__init.py, it's simply a blank file.
As for root.py, it's simply a httpexceptions.HTTPNOTFOUND, 404 code
For views/admin.py
from pyramid.view import view_config, render_view
import mycart.resources
from pyramid.httpexceptions import HTTPNotFound, HTTPFound
from mycart.views.root import strip_tags
from pyramid_mailer import get_mailer
from pyramid_mailer.message import Message
from pyramid.security import remember , forget , authenticated_userid
from pyramid.events import subscriber , BeforeRender
from mycart.Admin import Admin
from mycart.Product import Product
#view_config(context='mycart:resources.Admin', request_method='POST', renderer='admin/login.jinja2')
def login_post(context, request):
if 'btnLogin' in request.params:
token = request.session.get_csrf_token()
login = request.params['txtLogin']
password = request.params['txtPassword']
admin = Admin(login, request)
if admin.validate_user( password):
record = admin.find_user_by_login( login )
request.session['bs_admin_id'] = str(record['_id'])
request.session['bs_admin_name'] = record['usr']['fname'] + ' ' + record['usr']['lname'];
request.session['bs_admin_type'] = record['usr']['type']
headers = remember(request, login )
return HTTPFound('/admin/home', headers=headers)
message = 'Failed login'
return {'message': message, 'url': '/admin_login', 'page_title': 'Failed Login'}
#view_config(context='mycart:resources.Admin', name="home", renderer='admin/home.jinja2', permission='admin')
def home(context, request):
logged_in = authenticated_userid(request)
url = request.path_info
admin = Admin( logged_in, request )
rec = admin.find_user_by_objectid( request.session['bs_admin_id'] ) ;
return { 'firstname': rec['usr']['fname'] }
#view_config(context='mycart:resources.Admin', name="product", renderer='admin/product_listing.jinja2', permission='admin')
def product_list(context, request):
print ('yes, showing product listing requested by ', request.session['bs_admin_id'] )
After logging in, I point the url to localhost:6543/admin/product, I notice that it still rendering the home page, instead of the product page.
I know I missed out something but I can't seem to find out why. Looking through http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/narr/traversal.html, I know that I on the right track as there might be arbitary segments.
I've tried modifying resources.py to be the following
.....
class Admin(object):
__name__ = ''
__parent__ = Root
__acl__ = [(Allow, Authenticated, 'admin')]
def __init__(self):
pass
def __getitem__(self, key):
if key == 'product':
print ("WOOT! Listing products")
## this is the part where I don't know what should I return or set or how should I hook it up with view_config
if key == 'home':
print ("yes, I'm home!")
## this is the part where I don't know what should I return or set or how should I hook it up with view_config
raise KeyError
For this part, I made some progress where it's definitely printing the respective message in the console. However , I have no inkling how should I hook it up with the view_configs and what should be parameters be for the view_configs if any changes need to be made.
I do not know if version affects anything but anyway, I am using python 3.3
Any help will be appreciated. Thanks!
This is my first time coding in python after years of java. So there might be some terms / concepts that I'm not familiar with respect to pyramid / python.
Ok, I think I kinda got my mind to wrap around this traversal thing. Reading through http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/narr/traversal.html, 2 things caught my attention.
For example, if the path info sequence is ['a', 'b', 'c']:
- Traversal starts by acquiring the root resource of the application by calling the root factory. The root factory can be configured to return whatever object is appropriate as the traversal root of your application.
- Next, the first element ('a') is popped from the path segment sequence and is used as a key to lookup the corresponding resource in the root. This invokes the root resource’s __getitem__ method using that value ('a') as an argument.
- If the root resource “contains” a resource with key 'a', its __getitem__ method will return it. The context temporarily becomes the “A” resource.
So based on localhost:6543/admin/products, the settings for view_config is like the following:
#view_config(context=Admin, name='products', .... )
So after making changes to resources.py
## class Root(object):
....
class ProductName(object):
def __init__(self, _key):
pass
class Products(object):
__name__ = ''
__parent__ = Root
def __init__(self):
pass
def __getitem__(self, key):
print ('products: ', key)
if key == 'add':
return ProductName(key)
print ('Approaching KeyError')
raise KeyError
class Admin(object):
__name__ = ''
__parent__ = Root
__acl__ = [(Allow, Authenticated, 'admin')]
def __init__(self):
pass
def __getitem__(self, key):
if key == 'products':
print ('admin: ', key)
return Products()
raise KeyError
And in views/admin.py
#view_config(context=Admin, name='products', renderer='admin/products.jinja2', permission = 'admin')
def product_add(context, request):
print 'hey products_add'
return { 'msg': ''}
Somehow or rather, it isn't rendering the product template, but the default 404.

You take a look at the doc about traversal, because you've haven't got it quite right. This tutorial is also quite useful in understanding traversal. I'll try to do a quick explanation in your context :
First of all, the path of the request is split intro segments. For example /admin/product is split into ['admin', 'product'].
Then, pyramid tries to determine the context for this request. For that, it recursively call __getitem__ (which is just another way to say it does object[segment]) for each segment from the root (it traverses). In the exemple, it does root['admin'], which returns an admin object, then does admin['product']. It stops when it encounters a KeyError.
Once we have a context, pyramid searches for a view with this context, and whose view name is the part that wasn't traversed. For example, if admin['product'] raise a KeyError, then pyramid looks for a view that configured with #view_config(context=Admin, name="product").
So, how do you make an app from that ? First, you determine what is your resource tree. In your case, it might looks like this :
Root
Admin
ProductContainer
Product
There is a view named home for the Admin context (/admin/home), a view with no name for the ProductContainer (/admin/product) and a view named edit for the product (/admin/product/1/edit).

While I do not know if the code below is elegant or any loopholes, it is definitely working for me now. I'll put it in , in case someone is facing the same problem like me.
resources.py
class ProductName(object):
__name__ = ''
__parent__ = Root
__acl__ = [(Allow, Authenticated, 'admin')]
def __init__(self, _key):
pass
class Products(object):
__name__ = ''
__parent__ = Root
__acl__ = [(Allow, Authenticated, 'admin')]
def __init__(self):
pass
def __getitem__(self, key):
print ('products: ' + key)
if key == 'add':
return ProductName(key)
print ('Approaching KeyError')
raise KeyError
views/admin.py
#view_config(context="**mycart:resources.ProductName**", name="", renderer='admin/product_add.jinja2', permission = 'admin')
def product_add(context, request):
print 'hey product add'
return { 'msg': ''}
#view_config(context="**mycart:resources.Products**", name='' , renderer='admin/product.jinja2', permission = 'admin')
def product(context, request):
print 'hey products listing'
return { 'msg': ''}

Related

Optional url segment pattern in a Pyramid route

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()

Django: redirect url if slug is wrong

I have a function that is run every view to correct slugs.
For example if the slug is /12-post-about-stuff and a user enters /12-post-abot_stof they will be redirected correctly. The problem is that the different views have different url patterns for example:
/posts/post_slug/
...
/posts/post_slug/comments/new
how to I write a function that redirects by fixing the slug name based on the current url?
Edit: I am applying a decorator to every view with a board_name and pk argument. What I don't know is how to dynamically return the new url because the url format is different for each view.
def correct_board_url_name(func):
def wrapper(request, board_slug):
try:
pk = int(board_slug.split('-')[0])
board = Board.objects.get(pk=pk)
if (board.slug != board_slug):
# This does not always work depending on what is entered
return redirect(request.get_full_path().replace(board_slug, board.slug, 1))
else:
return func(request, board_slug)
except:
raise Http404('')
return wrapper
A middleware is a good choice if you want to process requests in many different views.
class RedirectMiddleware(object):
def process_request(self, request):
if request.resolver_match.app_name == 'posts' \
and 'post_slug' in request.resolver_match.kwargs:
new_path = None
# your logic here
if new_path:
return redirect(new_path, permanent=True)
return
In settings:
MIDDLEWARE = [
# another middlewares here ...
'path.to.RedirectMiddleware',
]

Render Multiple Jinja Templates with One Handler

Hello Stackoverflow Guru's!
I'm a complete newb, and I've got a question that I can't seem to find the answer to (hopefully because it's so simple nobody has bothered to ask).
I'm designing a website that has a bunch of recipes using google app engine. I'd like to be able to render a bunch of the recipe pages using one handler, because I plan of having lots of recipes later and I don't want to have to make a new handler for each one. My code is below:
import urllib2
import webapp2
import jinja2
import os
JINJA_ENVIRONMENT = jinja2.Environment(
loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
extensions=['jinja2.ext.autoescape'],
autoescape=True)
class Handler(webapp2.RequestHandler):
def write(self, *a, **kw):
self.response.out.write(*a, **kw)
def render_str(self,template,**params):
t = JINJA_ENVIRONMENT.get_template(template)
return t.render(params)
def render(self,template,**kw):
self.write(self.render_str(template,**kw))
class MainHandler(Handler):
def get(self):
template = JINJA_ENVIRONMENT.get_template('main.html')
self.response.write(template.render())
class RecipeHandler(Handler, recipe):
def get(self, recipe):
recipe_pages = {
'carbonara' : 'carbonara.html'
'burger' : 'burger.html'
}
if recipe in recipe_pages:
template = JINJA_ENVIRONMENT.get_template(recipe_pages[recipe])
self.response.write(template.render())
else:
self.abort(404)
app = webapp2.WSGIApplication([
('/', MainHandler),
('/carbonara', RecipeHandler(carbonara)),
('/burger',RecipeHandler(burger)),
], debug=True)
I basically want to avoid writing out a "CarbonaraHander" and "BurgerHandler", and just use "RecipeHandler" to render both pages. I know this should be possible, but I have no idea how to do it.
Any help is appreciated!
Edit: I think I should be using something called regular expressions? But I don't really understand how they need to be used in this case.
AFAIK you can't pass args to the handler, you need to extract them from the request. This is what I'd do (pushed it a bit further to directly use the template name in the URl routing):
class RecipeHandler(Handler):
def extract_template_name_from_request(self):
return self.request.path_info[9:] # strip leading '/recipes/' (or whatever else you need)
def get(self):
template_name = self.extract_template_name_from_request()
try:
template = JINJA_ENVIRONMENT.get_template(template_name)
except Exception:
# can't locate a template matching the requested path
self.abort(404)
return
# prepare the template values as needed
values = {'recipe': {'name': template_name[:-5]}} # just an example
try:
self.response.write(template.render(values))
except Exception:
# failure rendering the template
self.abort(500)
app = webapp2.WSGIApplication([
('/recipes/.*.html', RecipeHandler), # see extract_template_name_from_request()
('/.*', MainHandler),
], debug=True)

Django Rest Framework - How to test ViewSet?

I'm having trouble testing a ViewSet:
class ViewSetTest(TestCase):
def test_view_set(self):
factory = APIRequestFactory()
view = CatViewSet.as_view()
cat = Cat(name="bob")
cat.save()
request = factory.get(reverse('cat-detail', args=(cat.pk,)))
response = view(request)
I'm trying to replicate the syntax here:
http://www.django-rest-framework.org/api-guide/testing#forcing-authentication
But I think their AccountDetail view is different from my ViewSet, so I'm getting this error from the last line:
AttributeError: 'NoneType' object has no attributes 'items'
Is there a correct syntax here or am I mixing up concepts? My APIClient tests work, but I'm using the factory here because I would eventually like to add "request.user = some_user". Thanks in advance!
Oh and the client test works fine:
def test_client_view(self):
response = APIClient().get(reverse('cat-detail', args=(cat.pk,)))
self.assertEqual(response.status_code, 200)
I think I found the correct syntax, but not sure if it is conventional (still new to Django):
def test_view_set(self):
request = APIRequestFactory().get("")
cat_detail = CatViewSet.as_view({'get': 'retrieve'})
cat = Cat.objects.create(name="bob")
response = cat_detail(request, pk=cat.pk)
self.assertEqual(response.status_code, 200)
So now this passes and I can assign request.user, which allows me to customize the retrieve method under CatViewSet to consider the user.
I had the same issue, and was able to find a solution.
Looking at the source code, it looks like the view expects there to be an argument 'actions' that has a method items ( so, a dict ).
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/viewsets.py#L69
This is where the error you're getting is coming from. You'll have to specify the argument actions with a dict containing the allowed actions for that viewset, and then you'll be able to test the viewset properly.
The general mapping goes:
{
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
}
http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers
In your case you'll want {'get': 'retrieve'}
Like so:
class ViewSetTest(TestCase):
def test_view_set(self):
factory = APIRequestFactory()
view = CatViewSet.as_view(actions={'get': 'retrieve'}) # <-- Changed line
cat = Cat(name="bob")
cat.save()
request = factory.get(reverse('cat-detail', args=(cat.pk,)))
response = view(request)
EDIT: You'll actually need to specify the required actions. Changed code and comments to reflect this.
I found a way to do this without needing to manually create the right viewset and give it an action mapping:
from django.core.urlresolvers import reverse, resolve
...
url = reverse('cat-list')
req = factory.get(url)
view = resolve(url).func
response = view(req)
response.render()
I think it's your last line. You need to call the CatViewSet as_view(). I would go with:
response = view(request)
given that you already defined view = CatViewSet.as_view()
EDIT:
Can you show your views.py? Specifically, what kind of ViewSet did you use? I'm digging through the DRF code and it looks like you may not have any actions mapped to your ViewSet, which is triggering the error.
I needed to get this working with force authentication, and finally got it, here is what my test case looks like:
from django.test import TestCase
from rest_framework.test import APIRequestFactory
from django.db.models.query import QuerySet
from rest_framework.test import force_authenticate
from django.contrib.auth.models import User
from config_app.models import Config
from config_app.apps import ConfigAppConfig
from config_app.views import ConfigViewSet
class ViewsTestCase(TestCase):
def setUp(self):
# Create a test instance
self.config = Config.objects.create(
ads='{"frequency": 1, "site_id": 1, "network_id": 1}',
keys={}, methods={}, sections=[], web_app='{"image": 1, "label": 1, "url": 1}',
subscriptions=[], name='test name', build='test build', version='1.0test', device='desktop',
platform='android', client_id=None)
# Create auth user for views using api request factory
self.username = 'config_tester'
self.password = 'goldenstandard'
self.user = User.objects.create_superuser(self.username, 'test#example.com', self.password)
def tearDown(self):
pass
#classmethod
def setup_class(cls):
"""setup_class() before any methods in this class"""
pass
#classmethod
def teardown_class(cls):
"""teardown_class() after any methods in this class"""
pass
def shortDescription(self):
return None
def test_view_set1(self):
"""
No auth example
"""
api_request = APIRequestFactory().get("")
detail_view = ConfigViewSet.as_view({'get': 'retrieve'})
response = detail_view(api_request, pk=self.config.pk)
self.assertEqual(response.status_code, 401)
def test_view_set2(self):
"""
Auth using force_authenticate
"""
factory = APIRequestFactory()
user = User.objects.get(username=self.username)
detail_view = ConfigViewSet.as_view({'get': 'retrieve'})
# Make an authenticated request to the view...
api_request = factory.get('')
force_authenticate(api_request, user=user)
response = detail_view(api_request, pk=self.config.pk)
self.assertEqual(response.status_code, 200)
I'm using this with the django-nose test runner and it seems to be working well. Hope it helps those that have auth enabled on their viewsets.

Layout bug with my templates

On all my template rendering for a particular app, the output ends with None:
...</html>None
This must be a bug and probably in my code and I've spent days trying to track it down. There's nothing special about my app and this bug appears on every page I use template rendering, whether I use a seperate template engine or not. There is nothing special about my code:
class Objectives(NewBaseHandler):
#user_required
def get(self):
user = auth_models.User.get_by_id(long(self.auth.get_user_by_session()['user_id']))
if user:
self.render_template('objectives.html', {'user': user})
else:
self.render_template('/', {})
class NewBaseHandler(BaseHandler):
"""
........BaseHandler for all requests
........Holds the auth and session properties so they are reachable for all requests
...."""
def dispatch(self):
"""
............Save the sessions for preservation across requests
........"""
# self.session_store = sessions.get_store(request=self.request)
# if self.request.host.find('localhost') > 0: # for a Swedish domain that uses Swedish
# or lang = os.environ.get("HTTP_ACCEPT_LANGUAGE")
i18n.get_i18n().set_locale('sv')
lang_code_get = self.request.get('hl', None)
if lang_code_get:
#self.session['i18n_language'] = lang_code_get
i18n.get_i18n().set_locale(lang_code_get)
try:
response = super(NewBaseHandler, self).dispatch()
self.response.write(response)
finally:
self.session_store.save_sessions(self.response)
#webapp2.cached_property
def auth(self):
return auth.get_auth()
#webapp2.cached_property
def session_store(self):
return sessions.get_store(request=self.request)
#webapp2.cached_property
def auth_config(self):
"""
............Dict to hold urls for login/logout
........"""
return {'login_url': self.uri_for('login'),
'logout_url': self.uri_for('logout')}
class BaseHandler(webapp2.RequestHandler):
#webapp2.cached_property
def jinja2(self):
return jinja2.get_jinja2(app=self.app)
def render_template(self, file, template_args):
path = os.path.join(os.path.dirname(__file__), 'templates',
file)
self.response.out.write(template.render(path, template_args))
def render_jinja(self, filename, **template_args):
self.response.write(self.jinja2.render_template(filename,
**template_args))
How can I check where the output None is coming from? It's probably not coming from the template and it doesn't seem to be coming from the handlers and there is no other code.
Thank you
In Objectives.get() you must return a value. Since you don't do this Python assumes the result is None. This value you get in NewBaseHandler.dispatch() when calling to base dispatch implementation and then write it to response.
If I get your app correctly returning empty string in get method will solve the problem.

Categories

Resources