Related
Alright, let me give you guys an example;
We have the following url configuration in Django.
Django will try to match the url with the rules down below. Once it finds a match, it will use the appropriate view and lookup the object in the model.
The thing is, once it finds a match in the URL pattern, it will match the view. But once the object
in the view can't be found, it will return a page not found (404) error.
urls.py
from django.urls import path
from . import views
urlpatterns = [
path('articles/<slug:category>/<slug:editor>/', views.ArticleByThemeView.as_view(), name='articles_by_editor'),
path('articles/<slug:category>/<slug:theme>/', views.ArticleDetailView.as_view(), name='articles_by_theme')
]
views.py
class ArticleByThemeView(ListView):
"""
List all articles by a certain theme; "World War 2".
"""
model = Article
def dispatch(self, request, *args, **kwargs):
try:
# Check if the theme_slug matches a theme
theme = ArticleTheme.objects.get(slug=self.kwargs['theme_slug'])
except ArticleTheme.DoesNotExist:
# Theme does not exist, slug must be an article_slug
return redirect(
'article_detail',
category_slug=category_slug
article_slug=theme_slug
)
return super().dispatch(request, *args, **kwargs)
class ArticleDetailView(DetailView):
"""
Detailview for a certain article
"""
model = Article
def get_object(self):
return get_object_or_404(
Article,
category__slug=self.kwargs['category_slug'],
slug=self.kwargs['article_slug']
)
We have the following url patterns, we can sort articles either by the editor or by theme. We do this to create a logical url structure for SEO purposes.
Is their any way we can redirect to another view once the object isn't found?
Can we modify the dispatch method to return to the url patterns and find the following matching rule?
What about redirection like this:
def articles_by_editor(request, category, editor):
try:
article = Article.objects.get(category=category, editor=editor)
# return article
except Article.DoesNotExist:
# redirect to another view
return redirect('articles_by_theme', category=category)
Alright,
Based on the suggestion from Sunderam Dubey, I'wrote a function view, which uses two differn routes to the same view.
urls.py
from django.urls import path
from . import views
urlpatterns = [
path('articles/<slug:category>/<slug:slug>/', views.article_theme_or_detail_view, name='article_by_theme'),
path('articles/<slug:category>/<slug:slug>/', views.article_theme_or_detail_view, name='article_detail')
]
views.py
def article_theme_or_detail_view(
request,
category_slug,
slug=None
):
"""
This view could either be for a theme view or detailview,
depending on the slug.
"""
try:
# Check if the slug represents a theme
theme = ArticleTheme.objects.get(slug=slug)
article_list = Article.object.filter(theme=theme)
# Add context
context = {
'theme': theme,
'article_list': article_list
}
# Render the template with context
return render(
request,
'article_by_theme.html',
context
)
except ArticleTheme.DoesNotExists:
# The theme does not exist so the slug must be for a detail view
context = {
article = Article.objects.get(slug=slug)
}
return render(
request,
'article_detail.html',
context
)
Todo:
Remove one of the url routes
I am trying to get serialized data from endpoint localhost:8000/v1/test/uuid, but hitting a 404 error - what is wrong with below?
views.py
from uuid in response, get site object
class Test(APIView):
def get(self, request, *args, **kwargs):
uuid = kwargs.get('uuid')
resp = {'site': None}
site = Site.objects.get(uuid=uuid)
resp['site'] = SiteSerializer(site).data
return Response(resp)
urls.py
from django.conf.urls import re_path
from rest_framework.routers import DefaultRouter
from site_config import views
router = DefaultRouter()
router.register(
r'site',
views.SiteViewSet,
basename='site')
urlpatterns = [
re_path(r'^v1/', include(router.urls)),
re_path('test/<uuid:uuid>/', views.Test.as_view(), name='test'),
]
models.py
site id as the pk
class Site(models.Model):
"""
Model that represents a Site
"""
uuid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True)
domain_name = models.CharField(max_length=255, unique=True)
created = models.DateTimeField(editable=False, auto_now_add=True)
modified = models.DateTimeField(editable=False, auto_now=True)
serializers.py
class SiteSerializer(serializers.ModelSerializer):
class Meta:
model = Site
fields = [
'uuid',
'domain_name'
]
FYI - the endpoint was working when the views.py was returning all Sites, but it isn't working when I try to filter on uuid.
views.py (previously working version)
class Test(APIView):
def get(self, request, *args, **kwargs):
resp = {'site': None}
site = Site.objects.all()
resp['site'] = SiteSerializer(site, many=True).data
return Response(resp)
Error Message on Browser:
Page not found (404)
Request Method: GET
Request URL: http://localhost:8000/v1/test/7c018183-c952-4040-a450-e3cb58f09745/
Using the URLconf defined in site_config.urls, Django tried these URL patterns, in this order:
^v1/ ^site/$ [name='site-list']
^v1/ ^site\.(?P<format>[a-z0-9]+)/?$ [name='site-list']
^v1/ ^site/(?P<uuid>[^/.]+)/$ [name='site-detail']
^v1/ ^site/(?P<uuid>[^/.]+)\.(?P<format>[a-z0-9]+)/?$ [name='site-detail']
^v1/ ^$ [name='api-root']
^v1/ ^\.(?P<format>[a-z0-9]+)/?$ [name='api-root']
test/<uuid:uuid> [name='test']
urls.py
As this is a routing problem the first place to have a look should be the urls.py.
Without recreating the app it looks like there are potentially three problems there:
Analysis
1. re_path
re_path is used, a regular Django path expression is provided. The django.urls documentation has some examples that speak for themselves.
2. the path itselt
The URL path starts with v1/ while the provided configuration starts with test/.
3. the order
As the re_path for ^v1/ matches anything beginning with v1/ the order in the pattern is important.
Anything that should available in that path must either be listed before the regex match, or be registered in the router.
Fix
urlpatterns = [
path('v1/test/<uuid:uuid>/', views.Test.as_view(), name='test'),
re_path(r'^v1/', include(router.urls)),
]
FYI - the endpoint was working when the views.py was returning all
Sites, but it isn't working when I try to filter on uuid.
As I do not have the breakpoint analysis for your views.py code I assume that your get method inside
class Test(APIView):
def get(self, request, *args, **kwargs):
uuid = kwargs.get('uuid')
resp = {'site': None}
site = Site.objects.get(uuid=uuid)
resp['site'] = SiteSerializer(site).data
return Response(resp)
is not getting resolved by the patterns in your urls.py or the uuid is not getting through.
If the case is former here what you can do is a direct mapping of the UUID inside the get method and avoiding the kwarg.get() by doing this
class Test(APIView):
def get(self, request, uuid, *args, **kwargs):
uuid = uuid
resp = {'site': None}
site = Site.objects.get(uuid=uuid)
resp['site'] = SiteSerializer(site).data
return Response(resp)
I've always written data into database when posting via Django Rest Framework endpoints. This time I would like to process received data and send it somewhere else without writing into DB. I switched from ModelViewSet to ViewSet, I can issue GET request OK but receiving Bad Request 400 when I curl or POST via DRF URL. Here's a working minimal code (removed need for authentication etc):
urls.py
from django.urls import path, include
from .views import ContactView
from rest_framework import routers
router = routers.DefaultRouter()
router.register('message', ContactView, basename='message')
urlpatterns = [
path('', include(router.urls)),
]
serializers.py
from rest_framework import serializers
class ContactSerializer(serializers.Serializer):
text = serializers.CharField(max_length=250)
views.py
from rest_framework.response import Response
from .serializers import ContactSerializer
from rest_framework import viewsets
class ContactView(viewsets.ViewSet):
def list(self, request):
return Response('Got it')
def create(self, request):
serializer = ContactSerializer(data=request.data)
if serializer.is_valid():
return Response(serializer.data)
else:
return Response('Invalid')
Would greatly appreciate your suggestions.
You can use GenericAPIView for get or post request and do some logic in validate method, for example do something with signals or edit something. Also u can use #detailt_route or #list_route for any ModelViewSet for write special url for instance, example for edit extra data.
how i did rarely:
in urls.py
urlpatterns = [
url('v1/message', ContactAPIView.as_view(), name='message'),
]
in view.py
class ContactAPIView(GenericAPIView):
serializer_class = ContactSerializer
permission_classes = ()
def post(self, request, *args, **kwargs):
serializer_class = self.get_serializer_class()
serializer = serializer_class(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
data = {"status": True}
return Response(data)
in serializers.py
class ContactSerializer(serializers.Serializer):
text = serializers.TextField()
def validate(self, attrs):
write some logic
you are getting this error because you are using Viewsets which uses DefaultRouter to register routers for you. What it does is that it creates 2 urls for your viewset
message
message/id
so in your current case; i.e. viewset you need to send some dummy number in your url to access this post function (which is not a good approach).
So, you should use any class which parent doesn't include ViewSetMixin (which gives functionality of Router Registration) like in your case inherit your view from these classes
ListModelMixin
CreateModelMixin
GenericAPIView
I'm running Django 1.2.1 for my personal website with a blog. It's all fine and dandy, but I've found that all the browsers I've tried (Firefox, Chromium, Opera) are caching webpages, which of course is a problem for other users viewing my blog (being that it won't load new posts up unless they empty their cache or force refresh the page). I didn't have this problem when my site ran on PHP, so how would I go about fixing this seemingly Django-related problem?
I've only been working with Django for about a week or so, so I don't really know where abouts I should be looking to fix something like this. Thanks in advance!
The way I do each page (and each blog post) is as a Page/Post object respectively so I can use the admin interface without having to write my own. Although the issue is happening for both situations, I'll just give the Post class for now:
class Post(models.Model):
author = models.ForeignKey(User, default=User.objects.get(username='nathan'))
status = models.ForeignKey(Status, default=Status.objects.get(text='Draft'))
date = models.DateTimeField(default=datetime.datetime.now())
title = models.CharField(max_length=100)
post = models.TextField()
categories = models.ManyToManyField(Category)
def __unicode__(self):
return u'%s' % self.title
def link(self):
return u'/blog/post/%s' % self.title
class Meta:
ordering = ['-date']
And here's my urls.py:
from django.conf.urls.defaults import *
from django.views.generic import list_detail
from feeds import PostFeed
from models import Post
blog_posts = {
'queryset': Post.objects.filter(status__text__exact='Published'),
}
urlpatterns = patterns('getoffmalawn.blog.views',
(r'^$', list_detail.object_list, blog_posts),
(r'^archive/(\d{4})$', 'archive'),
(r'^rss/$', PostFeed()),
(r'^search/$', 'search'),
(r'^tag/(.+)/$', 'tag'),
(r'^post/(.+)/$', 'post'),
)
If you guys would like to see the code from views.py, just ask and I'll throw that up too.
Edit:
Here's the views.
view.py for the blog App:
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response, redirect
from django.core.context_processors import csrf
from django.db.models import Q
from models import Post, Category
def post(request, title):
post = Post.objects.get(title=title)
c = locals()
c.update(csrf(request))
return render_to_response('blog/post_detail.html', c)
def blog_detail(request, blog_id):
post = get_object_or_404(Post, pk=blog_id)
return list_detail.object_detail(request, queryset=Post.objects.all(), object_id=blog_id)
def archive(request, month, year):
pass
def search(request):
if request.method == 'GET':
query = request.GET['q']
object_list = Post.objects.filter(Q(post__icontains=query) | Q(title__icontains=query), status__text__exact='Published')
return render_to_response('blog/post_list_sparse.html', locals())
def tag(request, tag):
object_list = Post.objects.filter(categories__text__exact=tag, status__text__exact='Published')
return render_to_response('blog/post_list.html', locals())
The problem was that it really was browser-based caching, as I was told by a member on the Django-users mailing list. The reason I didn't see this problem in PHP is that it was sending cache suppression headers.
The solution was to add the #never_cache decorator to the relevant views.
Here is the documentation for caching in django.
https://docs.djangoproject.com/en/1.2/topics/cache/
Not ideal, but you can make all of the pages tell the browsers not to cache anything, to see if that helps.
https://docs.djangoproject.com/en/1.2/topics/cache/#controlling-cache-using-other-headers
I want to be able to set an option in the user's settings that forces them to change their password upon the next login to the admin interface. Is this possible? How would it go about being implemented? I'm using the default auth model right now but not opposed to modifying or changing it. Thanks for any help.
I'm actually in the process of doing this myself. You need three components: a user profile (if not already in use on your site), a middleware component, and a pre_save signal.
My code for this is in an app named 'accounts'.
# myproject/accounts/models.py
from django.db import models
from django.db.models import signals
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
force_password_change = models.BooleanField(default=False)
def create_user_profile_signal(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
def password_change_signal(sender, instance, **kwargs):
try:
user = User.objects.get(username=instance.username)
if not user.password == instance.password:
profile = user.get_profile()
profile.force_password_change = False
profile.save()
except User.DoesNotExist:
pass
signals.pre_save.connect(password_change_signal, sender=User, dispatch_uid='accounts.models')
signals.post_save.connect(create_user_profile_signal, sender=User, dispatch_uid='accounts.models')
First, we create a UserProfile with a foreign key to User. The force_password_change boolean will, as its name describes, be set to true for a user whenever you want to force them to change their password. You could do anything here though. In my organization, we also chose to implement a mandatory change every 90 days, so I also have a DateTimeField that stores the last time a user changed their password. You then set that in the pre_save signal, password_changed_signal.
Second, we have the create_user_profile_signal. This is mostly added just for completeness. If you're just now adding user profiles into your project, you'll need a post_save signal that will create a UserProfile every time a User is created. This accomplishes that task.
Third, we have the password_changed_signal. This is a pre_save signal because at this point in the process the actual row in the User table hasn't be updated. Therefore, we can access both the previous password and the new password about to be saved. If the two don't match, that means the user has changed their password, and we can then reset the force_password_change boolean. This would be the point, also where you would take care of any other things you've added such as setting the DateTimeField previously mentioned.
The last two lines attach the two functions to their appropriate signals.
If you haven't already, you will also need to add the following line to your project's settings.py (changing the app label and model name to match your setup):
AUTH_PROFILE_MODULE = 'accounts.UserProfile'
That covers the basics. Now we need a middleware component to check the status of our force_password_change flag (and any other necessary checks).
# myproject/accounts/middleware.py
from django.http import HttpResponseRedirect
import re
class PasswordChangeMiddleware:
def process_request(self, request):
if request.user.is_authenticated() and \
re.match(r'^/admin/?', request.path) and \
not re.match(r'^/admin/password_change/?', request.path):
profile = request.user.get_profile()
if profile.force_password_change:
return HttpResponseRedirect('/admin/password_change/')
This very simple middleware hooks into the process_request stage of the page loading process. It checks that 1) the user has already logged in, 2) they are trying to access some page in the admin, and 3) the page they are accessing is not the password change page itself (otherwise, you'd get an infinite loop of redirects). If all of these are true and the force_password_change flag has been set to True, then the user is redirected to the password change page. They will not be able to navigate anywhere else until they change their password (firing the pre_save signal discussed previously).
Finally, you just need to add this middleware to your project's settings.py (again, changing the import path as necessary):
MIDDLEWARE_CLASSES = (
# Other middleware here
'myproject.accounts.middleware.PasswordChangeMiddleware',
)
I have used Chris Pratt's solution, with a little change: instead of using a middleware, that'd be executed for every page with the consequent resource use, I figured I'd just intercept the login view.
In my urls.py I have added this to my urlpatterns:
url(r'^accounts/login/$', 'userbase.views.force_pwd_login'),
then I added the following to userbase.views:
def force_pwd_login(request, *args, **kwargs):
response = auth_views.login(request, *args, **kwargs)
if response.status_code == 302:
#We have a user
try:
if request.user.get_profile().force_password_change:
return redirect('django.contrib.auth.views.password_change')
except AttributeError: #No profile?
pass
return response
It seems to work flawlessly on Django 1.2, but I have no reason to believe 1.3+ should have problems with it.
This is the middleware I use with Django 1.11 :
# myproject/accounts/middleware.py
from django.http import HttpResponseRedirect
from django.urls import reverse
class PasswordChangeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
next = reverse('client:password-update')
if request.user.is_authenticated() and request.path != next:
if request.user.account.force_password_change:
return HttpResponseRedirect(next)
return response
Still adding it to the settings middleware list :
MIDDLEWARE_CLASSES = (
# Other middleware here
'myproject.accounts.middleware.PasswordChangeMiddleware',
)
I spent 2 days on this issue recently, and a new solution came out.
Hopefully it's useful.
Just as above said, a new user model created.
newuser/models.py
class Users(AbstractUser):
default_pwd_updated = models.NullBooleanField(default=None, editable=False)
pwd_update_time = models.DateTimeField(editable=False, null=True, default=None) # reserved column to support further interval password (such as 60 days) update policy
def set_password(self, raw_password):
if self.default_pwd_updated is None:
self.default_pwd_updated = False
elif not self.default_pwd_updated:
self.default_pwd_updated = True
self.pwd_update_time = timezone.now()
else:
self.pwd_update_time = timezone.now()
super().set_password(raw_password)
Set this model as the AUTH_USER_MODEL.
[project]/settings.py
AUTH_USER_MODEL = 'newuser.Users'
Now you just need to customize LoginView and some methods in AdminSite.
[project]/admin.py
from django.contrib.admin import AdminSite
from django.contrib.auth.views import LoginView
from django.utils.translation import gettext as _, gettext_lazy
from django.urls import reverse
from django.views.decorators.cache import never_cache
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect
class NewLoginView(LoginView):
def get_redirect_url(self):
if self.request.method == "POST" and self.request.user.get_username()\
and not self.request.user.default_pwd_updated:
redirect_to = reverse("admin:password_change")
else:
redirect_to = self.request.POST.get(
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, '')
)
return redirect_to
class NewAdminSite(AdminSite):
site_header = site_title = gettext_lazy("Customized Admin Site")
def __init__(self, name="admin"):
super().__init__(name)
#never_cache
def login(self, request, extra_context=None):
"""
Display the login form for the given HttpRequest.
"""
if request.method == 'GET' and self.has_permission(request):
# Already logged-in, redirect to admin index
if request.user.get_username() and not request.user.default_pwd_updated:
# default password not changed, force to password_change view
path = reverse('admin:password_change', current_app=self.name)
else:
path = reverse('admin:index', current_app=self.name)
return HttpResponseRedirect(path)
from django.contrib.auth.views import LoginView
from django.contrib.admin.forms import AdminAuthenticationForm
context = {
**self.each_context(request),
'title': _('Log in'),
'app_path': request.get_full_path(),
'username': request.user.get_username(),
}
if (REDIRECT_FIELD_NAME not in request.GET and
REDIRECT_FIELD_NAME not in request.POST):
context[REDIRECT_FIELD_NAME] = reverse('admin:index', current_app=self.name)
context.update(extra_context or {})
defaults = {
'extra_context': context,
'authentication_form': self.login_form or AdminAuthenticationForm,
'template_name': self.login_template or 'admin/login.html',
}
request.current_app = self.name
return NewLoginView.as_view(**defaults)(request) # use NewLoginView
#never_cache
def index(self, request, extra_context=None):
if request.user.get_username() and not request.user.default_pwd_updated:
# if default password not updated, force to password_change page
context = self.each_context(request)
context.update(extra_context or {})
return self.password_change(request, context)
return super().index(request, extra_context)
admin_site = NewAdminSite(name="admin")
NOTE: if you intend to use custom template for changing default password, you could override each_context method and then determine which template should be used up to the flag force_pwd_change.
[project]/admin.py
def using_default_password(self, request):
if self.has_permission(request) and request.user.get_username() and not request.user.default_pwd_updated:
return True
return False
def each_context(self, request):
context = super().each_context(request)
context["force_pwd_change"] = self.using_default_password(request)
return context
From a thread on the Django Users mailing list:
This isn't ideal, but it should work
(or prompt someone to propose
something better).
Add a one-to-one table for the user,
with a field containing the initial
password (encrypted, of course, so it
looks like the password in the
auth_user table).
When the user logs in, have the login
page check to see if the passwords
match. If they do, redirect to the
password change page instead of the
normal redirect page.
Checkout this simple package based on session (Tested with django 1.8). https://github.com/abdullatheef/django_force_reset_password
Create custom view in myapp.views.py
class PassWordReset(admin.AdminSite):
def login(self, request, extra_context=None):
if request.method == 'POST':
response = super(PassWordReset, self).login(request, extra_context=extra_context)
if response.status_code == 302 and request.user.is_authenticated():
if not "fpr" in request.session or request.session['fpr']:
request.session['fpr'] = True
return HttpResponseRedirect("/admin/password_change/")
return response
return super(PassWordReset, self).login(request, extra_context=extra_context)
def password_change(self, request, extra_context=None):
if request.method == 'POST':
response = super(PassWordReset, self).password_change(request, extra_context=extra_context)
if response.status_code == 302 and request.user.is_authenticated():
request.session['fpr'] = False
return response
return super(PassWordReset, self).password_change(request, extra_context=extra_context)
pfr_login = PassWordReset().login
pfr_password_change = PassWordReset().admin_view(PassWordReset().password_change, cacheable=True)
Then in project/urls.py
from myapp.views import pfr_password_change, pfr_login
urlpatterns = [
......
url(r'^admin/login/$', pfr_login),
url(r'^admin/password_change/$', pfr_password_change),
url(r'^admin/', admin.site.urls),
....
]
Then add this middleware myapp/middleware.py
class FPRCheck(object):
def process_request(self, request):
if request.user.is_authenticated() \
and re.match(r'^/admin/?', request.path) \
and (not "fpr" in request.session or ("fpr" in request.session and request.session['fpr'])) \
and not re.match(r"/admin/password_change|/admin/logout", request.path):
return HttpResponseRedirect("/admin/password_change/")
Order of middleware
MIDDLEWARE_CLASSES = [
....
'myapp.middleware.FPRCheck'
]
Note
This will not need any extra model.
Also work with any Session Engine.
No db query inside middleware.