I'm using Django Rest Framework to make a read-only API from an existing Django website. I'd like to require API URLs to include a format suffix (either .json, or .api for the browsable API) but I can't see how to do this when using ViewSets and Routers.
In my main site urls.py I have:
from django.urls import include
# ...
urlpatterns += [
path("api/v1/", include("api.urls", namespace="v1")),
]
And api/urls.py contains:
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from . import views
app_name = "api"
router = DefaultRouter(trailing_slash=False)
router.register(r"people", views.PersonViewSet)
urlpatterns = [
path("", include(router.urls)),
]
Two issues:
I can get a browsable API page by going to either /api/v1/people or /api/v1/people.api – I'd like to enforce the use of the latter.
The api-root view is visible at /api/v1/ or /api/v1/.api. That last looks ugly! But I can't see how to make it /api/v1.api, given I use a trailing slash on all URLs on the site, apart from the API.
Because I feel like I'm fighting DRF on this, I also feel I'm missing what the common best practice is.
At Andrew Backer's suggestion I tried using my own version of DefaultRouter and have, I think, solved point (1).
If I copy DefaultRouter, give it a new name, and make a single change, it enforces the use of format extensions. The only change is in the get_urls() method, where I change this:
if self.include_format_suffixes:
urls = format_suffix_patterns(urls)
To this:
if self.include_format_suffixes:
urls = format_suffix_patterns(urls, suffix_required=True, allowed=["api", "json"])
The suffix_required argument is the trick. It's a shame there's no way to pass an argument into DefaultRouter in order to toggle this on or off.
I tried subclassing DefaultRouter and only overriding the get_urls() method (rather than entirely copying and replacing the class) but that generated ImproperlyConfigured exceptions, "…is not a valid regular expression: redefinition of group name 'format' as group 2; was group 1 at position 42". I think that results in format_suffix_patterns() getting called twice, which is similar to an Issue here.
Related
I would like to do something like this:
from rest_framework import routers
router = routers.DefaultRouter(suffix_required=False)
When I look at the definition of DefaultRouter:
class DefaultRouter(SimpleRouter):
"""
The default router extends the SimpleRouter, but also adds in a default
API root view, and adds format suffix patterns to the URLs.
"""
include_root_view = True
include_format_suffixes = True
Notice include_format_suffixes is hardcoded to True.
I must be missing something, but how to I turn off format extensions preferabley for the whole project, or at least for a certain router? I always use JSON, and do not wish to have these .json extensions in the urls created from ViewSets.
I realize I could use Simple router, but I would the " default API root view, that returns a response containing hyperlinks to all the list views" that the DefaultRouter provides.
I want to import Mutation.updateUser() from "./generated/prisma.graphql", but I also want to add the #isAdmin directive. I can accomplish this by copying the query by hand:
type Mutation {
updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User #isAdmin
}
But that means I have to copy every query/mutation/type I want to add a directive to, is there any sintax like this?
# import Mutation.updateUser from "./generated/prisma.graphql" #isAdmin
Unfortunately this is not possible at the moment. GraphQL SDL by default has no way of importing types or fields of types (as you want to do) into other files.
Based on the # import ...-syntax you show, I believe you're trying to use graphql-import. With this library it's possible to import entire types but not individual fields of types. If you'd like to have that functionality in the graphql-import library, please open an issue.
I'm wondering if there is a clean way to retrieve an object from its URL with django rest framework. Surely there should be, as it seems to be what's happening when using HyperlinkedRelatedField.
For instance, I have this URL /api/comment/26 as a string. From my view, how can I get the comment instance with pk=26?
Of course I could redo the work and work on the string but it must be a better way?
Thanks a lot.
EDIT:
This is how I solved it at the end:
resolve('/api/comment/26/').func.cls.model will return my model Comment.
resolve('/api/category/1/').kwargs['pk'] will return the pk.
Which gives you:
from django.core.urlresolvers import resolve
resolved_func, unused_args, resolved_kwargs = resolve('/api/category/1/')
resolved_func.cls.model.objects.get(pk=resolved_kwargs['pk'])
I suspect your best bet would be to manually keep a map of models to url patterns — not too dissimilar from a URLConf.
If that doesn't rock your boat you could pass the path into resolve.
This will give you a ResolverMatch upon which you'll find the func that was returned by the as_view call when you set up your URLs.
The __name__ attribute of your view function will be that of your original view class. Do something like globals()[class_name] to get the class itself.
From there access the model attribute.
I hope that helps. As I say, you might just want to map models to URLs yourself.
With class-based views, something like the following seems needed:
resolve(url).func.cls.serializer_class.Meta.model.objects.get(
**resolve(url).kwargs)
Solution above did not work for me, the following did work though:
from django.core.urlresolvers import resolve
resolved_func, unused_args, resolved_kwargs = resolve('/api/category/1/')
resolved_func.cls().get_queryset().get(id=resolved_kwargs['pk'])
additionally this solution uses the built in queryset of your view, which might have annotations or important filters.
Using HyperlinkedModelSerializer I actually needed to do this with a full url. For this to work, you need to extract the path first, resulting in:
import urllib.parse
from django.core.urlresolvers import resolve
def obj_from_url(url):
path = urllib.parse.urlparse(url).path
resolved_func, unused_args, resolved_kwargs = resolve(path)
return resolved_func.cls().get_queryset().get(id=resolved_kwargs['pk'])
I know this doesn't exactly match the form of www.example.com/class/function/ID/, but what I want to display to the user would make more sense.
This is what I would like to do:
www.example.com/project/id/item/item_id/
So, an example would be:
www.example.com/project/5/item/198237/
And this would be the behavior:
www.example.com/project/ --> This would show a list of projects (current implementation)
www.example.com/project/5/ --> This would show a list of items on project 5 (current implementation)
www.example.com/project/5/item/ --> This wouldn't really mean anything different than the line above. (Is that bad?)
www.example.com/project/5/item/198237/ --> This would show details for item 198237.
So, each item is directly associated with one and only one project.
The only way I can think how to do this is to bloat the "project" controller and parse the various parameters and control ALL views from that "project" controller. I'd prefer not to do this, because the model and view for an "item" are truly separate from the model and view of a "project."
The only other solution (that I am currently implementing and don't prefer) is to have the following:
www.example.com/project/5/
www.example.com/item/198237/
Is there any way to build a hierarchical URL as I showed at the beginning without bloating the "project" controller?
There are 3 options, sorted by how practical they can be:
Use URI Routing. Define a regular expression that will use a specific controller/method combination for each URL.
Something like that could help you, in routes.php:
$route['project/'] = 'project/viewall';
$route['project/(.+)'] = 'project/view/$1';
$route['project/(.+)/item/'] = 'project/view/$1';
$route['project/(.+)/item/(.+)'] = 'item/view/$2';
That is, considering your controllers are item and project respectively. Also note that $n in the value corresponds to the part matched in the n-th parenthesis.
Use the same controller with (or without) redirection. I guess you already considered this.
Use redirection at a lower level, such as ModRewrite on Apache servers. You could come up with a rule similar to the one in routes.php. If you are already using such a method, it wouldn't be a bad idea to use that, but only if you are already using such a thing, and preferably, in the case of Apache, in the server configuration rather than an .htaccess file.
You can control all of these options using routes.php (found in the config folder). You can alternatively catch any of your URI segments using the URI class, as in $this->uri->segment(2). That is if you have the URL helper loaded. That you can load by default in the autoload.php file (also in the config folder).
I had to develop a CMS using Zend Framework and I used the default namespace defined in my boostrap for my backend:
autoloaderNamespaces[] = "Application_"
Now I want to develop the frontend, but I don't know how to do it, since I have access to my backend from the /public/ directory.
Then I would like to use a different layout for my frontend than the one I use for the backend access. So I found this post but I don't know if I have to change/add (and then how to change) the module of my backend, or if I have to create a second module that I will use for my frontend
my file tree is like this :
So if I create a frontend module, shall I create a frontend directory next to the applicationdirectory ?
EDIT : I created 2directories pub and frontend next to the application directory. In pub/index.php I instanciated the bootstrap with the application/configs/application.ini file with a different APPLICATION_FRONT_ENV :
[frontprod : production]
bootstrap.path = APPLICATION_FRONT_PATH "/bootstrap.php"
bootstrap.class = "Bootstrap"
resources.frontController.controllerDirectory = APPLICATION_FRONT_PATH "/controllers"
autoloaderNamespaces[] = "Frontend_"
resources.layout.layout = "layout"
resources.layout.layoutPath = APPLICATION_FRONT_PATH "/layouts/scripts"
[frontdev: frontprod]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1
resources.frontController.params.displayExceptions = 1
and in the frontend/bootstrap.php I loaded models from applicationdirectory :
public function _initAutoloader(){
$baseAutoload = new Zend_Loader_Autoloader_Resource(array(
'namespace' => 'Application',
'basePath' => realpath(dirname(__FILE__).'/../application')
)
);
}
And it seems to be working fine =)
thank you !
In Zend Framework you can organise your app in modules, wich suits very well to your needs. Unfortunately the doc doesn't emphasize enough the importance of this concept, and how you should implement it from day one.
Modules allows you to regroup under a same module folder everything that is related to this module only, and this way to isolate "parts" of your app in logical groups.
In your case it would be "back" and "front", but you could also have a "forum" module or let's say a "shop" module.
In the urls point of view, the default routing of a modular structure is example.com/module/controller/action, but using hostname routes you can also have www.example.com pointing to your "front" module and admin.example.com pointing to your backend.
Have a look at the poor documentation section about modules, and don't panic, you won't have to rename everything if you move your current controllers, views and models in the "default" module.
There is an other alternative that could suit well for a backend/frontend logic, but not if you want to split your code in more logical parts (forum, blog, shop,...). You just create a second application folder (you would name 'frontend') next to the 'application' folder, and a second public directory (where you can symlink your assets folder if you use the sames), and a different namespace.
To be able to autoload your 'Application_' classes in your frontend code, just add and configure a Module Autoloader in your frontend bootstrap. The code is quite simple :
//in your frontend/Bootstrap.php
public function _initAutoloader(){
new Zend_Loader_Autoloader_Resource( array(
'namespace' => 'Application_',
'path' => realpath(dirname(__FILE__).'/../application'
)
);
}
For the application.ini config file i would recommend, instead of duplicating it, that you create a section [frontprod : production] section where you override your backend settings (and a matching [frontdev: frontprod] for your local settings).
I hope this helped. There is so much to say about all the topics introduced here that you should first have a look at this, then comment this answer with more specific questions about the problems you may encounter, and i'll extend the answer.