Is there a tool that allows me to password protect all but one URL in Django without requiring authentication?
You can write a custom middleware for this:
class LoggedInUserCheckMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
full_path = request.get_full_path()
if not full_path == '/no-need-auth-url' and not request.user.is_authenticated:
raise Http404
response = self.get_response(request)
return response
And add it to MIDDLEWARE in settings:
MIDDLEWARE = [
# rest of middlewares
'path.to.LoggedInUserCheckMiddleware'
]
you can use #loging_required decorator
from django.contrib.auth.decorators import login_required
on all your views except that one view/url.
I am writing a test for a DetailView that queries get_object() by accessing a value set in a Middleware. This is for a Companies application and Company Model. Each user is in a Company.
To access the company throughout the project, I set the current user's Company.uuid on the request via a custom middleware.
Middleware
from django.utils.deprecation import MiddlewareMixin
class DynamicCompanyUUIDMiddleware(MiddlewareMixin):
""" Adds the current organization's UUID from the current user."""
def process_request(self, request):
try:
company_uuid = request.user.company_uuid
except:
company_uuid = None
request.company_uuid = company_uuid
That is used in the CompanyDetailView's get_object() method via a Mixin that I use for the other Company Views.
Mixin
class CompanyMixin(LoginRequiredMixin, SetHeadlineMixin):
model = Company
def get_object(self):
return get_object_or_404(
self.model,
uuid=self.request.user.company_uuid)
Test
The test that I'm trying to write is:
from django.test import RequestFactory
from django.urls import reverse, resolve
from test_plus.test import TestCase
from ..models import Company
from ..views import CompanyDetailView
class BaseCompanyTestCase(TestCase):
def setUp(self):
self.user = self.make_user()
self.object = Company.objects.create(owner=self.user, name="testcompany")
self.user.company_uuid = self.object.uuid
self.factory = RequestFactory()
class TestCompanyDetailView(BaseCompanyTestCase):
def setUp(self):
super(TestCompanyDetailView, self).setUp()
self.client.login(username="testuser", password="password")
self.view = CompanyDetailView()
self.view.object = self.object
request = self.factory.get(reverse('companies:detail'))
request.user = self.user
request.company_uuid = self.user.company_uuid
response = CompanyDetailView.as_view()(request)
self.assertEqual(response.status_code, 200)
def test_get_headline(self):
self.assertEqual(
self.view.get_headline(),
'%s Members' % self.object.name
Result
This results in a 404 with the testuser's company not being found.
Walking through it:
I create the user
Create the company for this new testuser
Set the user.company_uuid
This should allow the mixin to access the company_uuid
Therefore return the user's company in the request
However I'm not returning the company as the 404 shows.
Question
Where am I going wrong on this? Thanks in advance for your help.
Answer
I was mixing Django's Client & RequestFactory. I have corrected the code above which is correct.
I was mixing Django's Client & RequestFactory. After stepping away, I figured it out below -
from django.test import RequestFactory
from django.urls import reverse, resolve
from test_plus.test import TestCase
from ..models import Company
from ..views import CompanyDetailView
class BaseCompanyTestCase(TestCase):
def setUp(self):
self.user = self.make_user()
self.object = Company.objects.create(owner=self.user, name="testcompany")
self.user.company_uuid = self.object.uuid
self.factory = RequestFactory()
class TestCompanyDetailView(BaseCompanyTestCase):
def setUp(self):
super(TestCompanyDetailView, self).setUp()
self.client.login(username="testuser", password="password")
self.view = CompanyDetailView()
self.view.object = self.object
request = self.factory.get(reverse('companies:detail'))
request.user = self.user
request.company_uuid = self.user.company_uuid
response = CompanyDetailView.as_view()(request)
self.assertEqual(response.status_code, 200)
def test_get_headline(self):
self.assertEqual(
self.view.get_headline(),
'%s Members' % self.object.name
I feel like this is really simple and I'm just missing it.
I have a very simple generic class based view detail.
When I do teh get_object I want to make sure that the request.user is in the set that belongs to the object. If not, redirect them to the login.
Here's my view:
class AwesomeDetail(LoginRequiredMixin, DetailView):
"""
An awesome detail
"""
template_name = "awesomeness/detail.html"
def get_object(self):
awesomeness = ModelName.objects.get(id=self.kwargs['pk'])
if self.request.user in awesomeness.staff.all():
return awesomness
else:
return redirect('account_login')
Staff is a many to many to users. What am I missing? The redirect isn't happening. It renders the template, but of course, awesomeness is missing.
UserPassesTestMixin was introduced in Django 1.9.
You define a test_func method to return True/Fales depending on whether the test passes.
If the user fails the test they will be redirected to settings.LOGIN_URL with a redirect field.
UserPassesTestMixin supports any of the fields for AccessMixin.
from django.contrib.auth.mixins import UserPassesTestMixin
class AwesomeDetail(UserPassesTestMixin, LoginRequiredMixin, DetailView):
"""
An awesome detail
"""
template_name = "awesomeness/detail.html"
model = ModelName
def test_func(self):
if self.request.user in self.object.staff.all():
return True
else:
return False
I'm not very sure about this, but sounds like get method is the one you should work on:
class AwesomeDetail(LoginRequiredMixin, DetailView):
"""
An awesome detail
"""
template_name = "awesomeness/detail.html"
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.request.user in self.object.staff.all():
return redirect('account_login')
else:
return super(AwesomeDetail, self).get(request, *args, **kwargs)
I have a model with a created_by field that is linked to the standard Django User model. I need to automatically populate this with the ID of the current User when the model is saved. I can't do this at the Admin layer, as most parts of the site will not use the built-in Admin. Can anyone advise on how I should go about this?
UPDATE 2020-01-02
⚠ The following answer was never updated to the latest Python and Django versions. Since writing this a few years ago packages have been released to solve this problem. Nowadays I highly recommend using django-crum which implements the same technique but has tests and is updated regularly: https://pypi.org/project/django-crum/
The least obstrusive way is to use a CurrentUserMiddleware to store the current user in a thread local object:
current_user.py
from threading import local
_user = local()
class CurrentUserMiddleware(object):
def process_request(self, request):
_user.value = request.user
def get_current_user():
return _user.value
Now you only need to add this middleware to your MIDDLEWARE_CLASSES after the authentication middleware.
settings.py
MIDDLEWARE_CLASSES = (
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
...
'current_user.CurrentUserMiddleware',
...
)
Your model can now use the get_current_user function to access the user without having to pass the request object around.
models.py
from django.db import models
from current_user import get_current_user
class MyModel(models.Model):
created_by = models.ForeignKey('auth.User', default=get_current_user)
Hint:
If you are using Django CMS you do not even need to define your own CurrentUserMiddleware but can use cms.middleware.user.CurrentUserMiddleware and the cms.utils.permissions.get_current_user function to retrieve the current user.
If you want something that will work both in the admin and elsewhere, you should use a custom modelform. The basic idea is to override the __init__ method to take an extra parameter - request - and store it as an attribute of the form, then also override the save method to set the user id before saving to the database.
class MyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
return super(MyModelForm, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
kwargs['commit']=False
obj = super(MyModelForm, self).save(*args, **kwargs)
if self.request:
obj.user = self.request.user
obj.save()
return obj
Daniel's answer won't work directly for the admin because you need to pass in the request object. You might be able to do this by overriding the get_form method in your ModelAdmin class but it's probably easier to stay away from the form customisation and just override save_model in your ModelAdmin.
def save_model(self, request, obj, form, change):
"""When creating a new object, set the creator field.
"""
if not change:
obj.creator = request.user
obj.save()
This whole approach bugged the heck out of me. I wanted to say it exactly once, so I implemented it in middleware. Just add WhodidMiddleware after your authentication middleware.
If your created_by & modified_by fields are set to editable = False then you will not have to change any of your forms at all.
"""Add user created_by and modified_by foreign key refs to any model automatically.
Almost entirely taken from https://github.com/Atomidata/django-audit-log/blob/master/audit_log/middleware.py"""
from django.db.models import signals
from django.utils.functional import curry
class WhodidMiddleware(object):
def process_request(self, request):
if not request.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
if hasattr(request, 'user') and request.user.is_authenticated():
user = request.user
else:
user = None
mark_whodid = curry(self.mark_whodid, user)
signals.pre_save.connect(mark_whodid, dispatch_uid = (self.__class__, request,), weak = False)
def process_response(self, request, response):
signals.pre_save.disconnect(dispatch_uid = (self.__class__, request,))
return response
def mark_whodid(self, user, sender, instance, **kwargs):
if 'created_by' in instance._meta.fields and not instance.created_by:
instance.created_by = user
if 'modified_by' in instance._meta.fields:
instance.modified_by = user
here's how I do it with generic views:
class MyView(CreateView):
model = MyModel
def form_valid(self, form):
object = form.save(commit=False)
object.owner = self.request.user
object.save()
return super(MyView, self).form_valid(form)
If you are using class based views Daniel's answer needs more. Add the following to ensure that the request object is available for us in your ModelForm object
class BaseCreateView(CreateView):
def get_form_kwargs(self):
"""
Returns the keyword arguments for instanciating the form.
"""
kwargs = {'initial': self.get_initial()}
if self.request.method in ('POST', 'PUT'):
kwargs.update({
'data': self.request.POST,
'files': self.request.FILES,
'request': self.request})
return kwargs
Also, as already mentioned, you need to return the obj at the end of ModelForm.save()
what is the problem with using something like:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
exclude = ['created_by']
def save(self, user):
obj = super().save(commit = False)
obj.created_by = user
obj.save()
return obj
Now call it like myform.save(request.user) in the views.
here is ModelForm's save function, which has only a commit parameter.
For future references, best solution I found about this subject:
https://pypi.python.org/pypi/django-crum/0.6.1
This library consist of some middleware.
After setting up this libary, simply override the save method of model and do the following,
from crum import get_current_user
def save(self, *args, **kwargs):
user = get_current_user()
if not self.pk:
self.created_by = user
else:
self.changed_by = user
super(Foomodel, self).save(*args, **kwargs)
if you create and abstract model and inherit from it for all your model, you get your auto populated created_by and changed_by fields.
Based on bikeshedder's answer, I found a solution since his did not actually work for me.
app/middleware/current_user.py
from threading import local
_user = local()
class CurrentUserMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
_user.value = request.user
return self.get_response(request)
def get_current_user():
return _user.value
settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'common.middleware.current_user.CurrentUserMiddleware',
]
model.py
from common.middleware import current_user
created_by = models.ForeignKey(User, blank=False, related_name='created_by', editable=False, default=current_user.get_current_user)
I'm using python 3.5 and django 1.11.3
From the Django documentation Models and request.user:
" To track the user that created an object using a CreateView, you can
use a custom ModelForm. In the view, ensure that you
don’t include [the user field] in the list of fields to edit, and override
form_valid() to add the user:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView
from myapp.models import Author
class AuthorCreate(LoginRequiredMixin, CreateView):
model = Author
fields = ['name']
def form_valid(self, form):
form.instance.created_by = self.request.user
return super().form_valid(form)
The 'save' method from forms.ModelForm returns the saved instanced.
You should add one last line to MyModelForm:
...
return obj
This change is necessary if you are using create_object or update_object generic views.
They use the saved object to do the redirect.
I don't believe Daniel's answer is the best there is since it changes the default behaviour of a model form by always saving the object.
The code I would use:
forms.py
from django import forms
class MyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(MyModelForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
obj = super(MyModelForm, self).save(commit=False)
if obj.created_by_id is None:
obj.created_by = self.user
if commit:
obj.save()
return obj
Note sure if you were looking for this, but adding the following
user = models.ForeignKey('auth.User')
to a model will work to add the user id to the model.
In the following, each hierarchy belongs to a user.
class Hierarchy(models.Model):
user = models.ForeignKey('auth.User')
name = models.CharField(max_length=200)
desc = models.CharField(max_length=1500)