Nested detail_route in django-rest-framework - python

Consider simple view:
class SomeView(viewsets.GenericViewSet,
viewsets.mixins.ListModelMixin,
viewsets.mixins.RetrieveModelMixin):
...
#decorators.detail_route(methods=ENDPOINT_PROPERTY_METHODS)
def some_property(self, request, *args, **kwargs):
view = SomeOtherView
return view.as_view(CRUD_ACTIONS)(request, *args, **kwargs)
I'm calling SomeOtherView to have ability to have an endpoint-property like /someresource/:id/myproperty, so this property will receive request and can do all CRUD actions.
But, I want to SomeOtherView to have the declared detail_route inside too to have something like /someresource/:id/myproperty/nestedproperty.
Since I'm calling SomeOtherView dynamically, urls can not be registered, so nested property can not be called.
How I can resolve such situation to have nested properties?

There is currently no native way in automatically creating nested routes in django-rest-framework but there are some ways to achieve your goal:
use drf-extentions, what your are searching for are nested routers: https://chibisov.github.io/drf-extensions/docs/#nested-routes
create the paths manually with the default routers, here you need to filter your queryset manually
Although you didn't explain in detail what you want to achive with this api structure I wouldn't recomment continuing this path because views are not intended to be used like that.

Related

How to modify Page model view in wagtail admin?

Background: I would like to enhance a page instance during an admin page view with some admin request related information (some pre-population in general). Basically I would need some function like "get_queryset", but not for list view, just for edit view.
In my older question related to a similar problem: Wagtail - how to preopulate fields in admin form? I was provided with instructions to use something called
CreatePageView
However, I cannot import it. Furthermore, I cannot even found any mention about that in google if I search:
Wagtail +CreatePageView
The closest thing I found is https://docs.wagtail.io/en/v2.1.1/reference/contrib/modeladmin/create_edit_delete_views.html but the page also states:
NOTE: modeladmin only provides ‘create’, ‘edit’ and ‘delete’
functionality for non page type models (i.e. models that do not extend
wagtailcore.models.Page). If your model is a ‘page type’ model,
customising any of the following will not have any effect
I am quite confused. What should I do if I need to customize the admin view for Page model extension?
I studied the wagtail source codes for Model.admin and Page and I have not found any way. Any ideas?
The related code simplified:
wagtail hooks:
class ItemAdmin(ModelAdmin):
pass
# some function override here maybe?
models:
class ItemPage(Page):
pass
# override for a function that gives data to the admin view maybe here?
Edit
As suggested in comments, it is possible to modify the admin page form during creation:
from wagtail.admin.forms import WagtailAdminPageForm
class ItemPageForm(WagtailAdminPageForm):
def __init__(self, data=None, files=None, parent_page=None, *args, **kwargs):
super().__init__(data, files, *args, **kwargs)
class ItemPage(Page):
base_form_class = ItemPageForm
however, acquiring the "request" in the WagtailAdminPageForm constructor does not seem possible.
This question is a bit ambiguous, so it is not super clear exactly what you need.
Interpreted question: When crediting (or editing) a page, I need access to the request to modify the initial values of some fields in the page form.
Potential Approach
Note: This may not be best practice and could be fragile depending on future changes to Wagtail.
First, we need a custom EditHandler, these are the way Wagtail builds up forms and even Panels within the editing interface. An EditHandler's job is to manage the form to return based on the model and even the current request.
As a first step, it would be good to get your page create form showing correctly by following the instructions on using a custom tabbed interface. From here, you can replace the TabbedInterface with your custom class (e.g. CustomTabbedInterface) and add some functionality to this which will allow for a dynamic form_class to be returned.
get_form_class should return the form_class, however, we can modify it to return a function that, when called, will instantiate the class with custom information based on the request.
There may be some issues with this approach below in edit views or scenarios not considered by this example so validate this fully before using.
Example Code
from wagtail.admin.edit_handlers import TabbedInterface, ObjectList
from wagtail.core.models import Page
class CustomTabbedInterface(TabbedInterface):
def get_form_class(self):
form_class = super().get_form_class()
request = self.request
if request and request.method != 'POST':
# check request is available to ensure this instance has been bound to it
user = self.request.user
def initiate_class(**kwargs):
# instead of returning the class, return a function that returns the instantiated class
# here we can inject a kwarg `initial` into the generated form
# important: this gets called for edit view also and initial will override the instance data
# kwarg['instance'] will be the `Page` instance and can be inspected as needed
kwargs['initial'] = {'introduction': user.first_name}
return form_class(**kwargs)
return initiate_class
return form_class
class StandardPage(Page):
# ... field etc
edit_handler = CustomTabbedInterface([
ObjectList(content_panels, heading='Content'),
ObjectList(Page.promote_panels, heading='Promote'),
ObjectList(Page.settings_panels, heading='Settings', classname="settings"),
])
Explanation
wagtail/admin/views/pages.py contains the create view, which will use the edit_handler, bind it to the model and the request and then call its get_form_class.
The form_class is used for the response here form = form_class(instance=page, parent_page=parent_page)
It gets called with the instance and the parent_page kwargs
Our custom get_form_class response takes those kwargs and injects an additional initial kwarg.
initial is used by Django forms to add any initial data - https://docs.djangoproject.com/en/3.0/ref/forms/api/#dynamic-initial-values
Finally, the Django form will merge the instance field values with the intial kwarg to generate the final pre-filled data for the form. You can see how this works in Django's BaseModelForm.
Be careful to consider what will happen on an update view, you likely do not want to override existing values with your initial values when a user has already entered something in the field.

Combining multiple GET endpoints into a single resource

I'm using flask restful to create an API and I want to refactor my code. I have something as shown below:
class Makequestions(Resource):
def get(self):
#somecode
class Managequestions(Resource):
def get(self, user_id):
#somecode
API.add_resource(MakeQuestions, '/MakeQuestions')
API.add_resource(ManageQuestions, '/ManageQuestions/<int:user_id>')
I need to combine these two into a single resource with multiple endpoints say forexample:
class Makequestions(Resource):
def get(self):
#somecode
def get(self, user_id):
#somecode
maybe with a final url along the lines of
API.add_resource(MakeQuestions, '/MakeQuestions','/MakeQuestions/<int:user_id>')
I'm hoping this will reduce the amount of times I have to create a 'Resource class' Is there someway I can do this?
I'm not super familiar with the Flask RESTful API, but while Python will let you overload a class with get(self) and get(self, question_id), if you're "making" a REST resource, that shouldn't be a GET request.
REST API URLs should not contain verbs such as "MakeObject" or "ManageObject". The HTTP method should dictate the operation being performed.
That being said, POST /question would make a question object. GET /question/:id should return a specific one.
Therefore, you'd define a post(self) and get(self, question_id) under a single Question Resource. Or add a Questions resource as well where you can get all questions, and move the post method there.
If you replace "Question" with "TODO", your problem is not that different from the full example on the Flask RESTful site
Note: plural URLs are also preferred over singular, so in your case, /questions and /questions/:id should be defined as two resources
The naming of your functions aside, you could probably do something like
class Question(Resource):
def get(self, user_id=0):
if(user_id):
return specific question
return list of questions

How to pass optional keyword named group to views.py in Django?

I am working on a search filter of an ecommerce site.
Current Situation:
When user select each platform filter, the platform name will be appended to URL and display the filtered result.
My approach:
url.py
url(r'^search/(?P<product_slug>[0-9a-z-]+)$', CustomSearchView(), name='search_result_detail'),enter code here
url(r'^search/(?P<product_slug>[0-9a-z-]+)_(?P<platform_slug>[0-9a-z-]+)$', CustomSearchView(),
name='search_result_platform'),
url(r'^search/(?P<product_slug>[0-9a-z-]+)_(?P<platform_slug>[0-9a-z-]+)_(?P<platform_slug2>[0-9a-z-]+)$',
CustomSearchView(), name='search_result_platform2'),
url(r'^search/(?P<product_slug>[0-9a-z-]+)_(?P<platform_slug>[0-9a-z-]+)_'
r'(?P<platform_slug2>[0-9a-z-]+)_(?P<platform_slug3>[0-9a-z-]+)$',
CustomSearchView(), name='search_result_platform3'),
Main Question:
I didn't want to limit the filtering number. So if there are 20 platform filters, I need to create 20 URLs. Definitely it's not a smart way. Any other smart way to avoid creating a batches of URL?
views.py
def __call__(self, request, product_slug, platform_slug=None,platform_slug2=None,platform_slug3 = None
,platform_slug4 = None,platform_slug5 = None):
if platform_slug is None:
self.product_review_list = SearchResult.objects.filter(products__slug=product_slug)
else:
self.product_review_list = SearchResult.objects.filter(Q(products__slug=product_slug),
Q(platform__slug=platform_slug)|(Q(platform__slug=platform_slug2)|Q(platform__slug=platform_slug3)
|Q(platform__slug=platform_slug4)|Q(platform__slug=platform_slug5)))
As mentioned in the comments, this kind of filtering is a job for GET parameters, not paths.
The URL pattern should just be r'^search/$', and you call it via /search/?platform_slug=x&platform_slug=y&platform_slug=z. Then, in the view, you can just use __in to filter:
def custom_search_view(request):
product_review_list = SearchResult.objects.filter(products__slug__in=request.GET.getlist('platform_slug')
One other point. You must absolutely not use classes like this in your URLs and then set attributes on self. That is not threadsafe and will cause all sorts of problems. If you really need to use a class, subclass the generic View class and use it in the URLs as CustomSearchView.as_view(), then override get() or post().

delegate from one view to another

I am using Pyramid with different views. I am wondering if it is possible to "delegate" parts of a views job to another view (another route).
For example:
http://localhost:6543/sample_project/testruns/testrun001/report.html?action=edit
=> delegate to:
http://localhost:6543/sample_project/testruns/testrun001/report.json
the views I am using:
# report:
#view_config(context=Root, route_name='report_route')
def report_view(context, request):
...
if 'edit' in request.GET.getall('action'):
# TODO: delegate to code_view
???
...
# render report from report.json
# editor:
#view_config(context=Root, route_name='report_edit_route')
#view_config(context=Root, route_name='code_route')
def code_view(context, request):
....
You can directly call views, they simply won't be going through the pyramid router mechanism which applies the permission and other such parameters to the view. Presumably if you are trying to call it, however, you already know these things.
In reality, you probably just want to refactor the common functionality into a separate function that each of your views can then delegate part of the work to.

Keeping filters in Django Admin

What I would like to achive is:
I go to admin site, apply some filters to the list of objects
I click and object edit, edit, edit, hit 'Save'
Site takes me to the list of objects... unfiltered. I'd like to have the filter from step 1 remembered and applied.
Is there an easy way to do it?
Unfortunately there's no easy way to do this. The filtering does not seem to be saved in any session variable.
Clicking back twice is the normal method, but it can be unweildy and annoying if you've just changed an object so that it should no longer be shown using your filter.
If it's just a one-off, click back twice or go through the filtering again, it's the easiest way.
If you're going to be filtering more often, or you just want to learn about hacking the admin (which is pretty open and easy), you'll want to write a FilterSpec.
Have a look here and here for examples of people writing their own.
A really, really terrible way to do this would be to edit the admin interface so that after you click "Save", you are redirected to you filtered URL. I wouldn't recommend this at all, but it's an option.
Another fairly simple way to do this would be to write a generic view to show your filtered objects, then use Django forms to edit the items from there. I'd have a look at this, you'll be stunned just how little code you have to write to get a simple view/edit page going.
Click 2 times "Back"?
There's a simple hack to do this, but it's not a general solution and requires modifying every ModelAdmin which you want to support this. Maybe there is a general way to do this, but I've not spent the time to solve it on a general level.
The first step is to write a custom FilterSpec for the filter (see Harley's post for links that will help) which saves the chosen filter value in the session (and deletes it when no longer wanted).
# in cust_admin/filterspecs.py
from django.contrib.admin.filterspecs import FilterSpec, ChoicesFilterSpec
class MyFilterSpec(ChoicesFilterSpec):
def __init__(self, f, request, params, model, model_admin):
super(MyFilterSpec, self).__init__(f, request, params, model,
model_admin)
if self.lookup_val is not None:
request.session[self.lookup_kwarg] = self.lookup_val
elif self.lookup_kwarg in request.session:
del(request.session[self.lookup_kwarg])
# Register the filter with a test function which will apply it to any field
# with a my_filter attribute equal to True
FilterSpec.filter_specs.insert(0, (lambda f: getattr(f, 'my_filter', False),
MyFilterSpec))
You must import the module this is in somewhere, for example your urls.py:
# in urls.py
from cust_admin import filterspecs
Set a property on the field you want to apply the filter to:
# in models.py
class MyModel(models.Model):
my_field = Models.IntegerField(choices=MY_CHOICES)
my_field.my_filter = True
In a custom ModelAdmin class, override the change_view method, so that after the user clicks save, they are returned to the list view with their filter field value added to the URL.
class MyModelAdmin(admin.ModelAdmin):
def change_view(self, request, object_id, extra_context=None):
result = super(MyModelAdmin, self).change_view(request, object_id,
extra_context)
if '_save' in request.POST:
if 'my_field__exact' in request.session:
result['Location'] = '/admin/myapp/mymodel/?my_field__exact=%s' \
% request.session['my_field__exact']
return result
Another way to do this is to embed the filter in the queryset.
You can dynamically create a proxy model with a manager that filters the way you want, then call admin.site.register() to create a new model admin. All the links would then be relative to this view.
In my opinion its better to override methods from ModelAdmin changelist_view and change_view:
Like so:
class FakturaAdmin(admin.ModelAdmin):
[...]
def changelist_view(self, request, extra_context=None):
result = super(FakturaAdmin, self).changelist_view(request, extra_context=None)
request.session['qdict'] = request.GET
return result
def change_view(self, request, object_id, extra_context=None):
result = super(FakturaAdmin, self).change_view(request, object_id, extra_context)
try:
result['location'] = result['location']+"?"+request.session['qdict'].urlencode()
except:
pass
return result
As you wish, after save object you go back to list of objects with active filters.
There is a change request at the Django project asking for exactly this functionality.
All it's waiting for to be checked in is some tests and documentation. You could write those, and help the whole project, or you could just take the proposed patch (near the bottom of the page) and try it out.
https://code.djangoproject.com/ticket/6903
This feature has been added to Django as part of the 1.6 release and is enabled now by default. It is described in the release notes:
ModelAdmin now preserves filters on the list view after creating,
editing or deleting an object. It’s possible to restore the previous
behavior of clearing filters by setting the preserve_filters attribute
to False.

Categories

Resources