Django: How to get domain name inside a signal handler - python

I'm trying to send an email with my website address from django signal. I found this question: https://stackoverflow.com/a/15521046/2385132 and proceeded as was advised in the accepted answer, but when using that code, I'm getting this error:
AttributeError: 'NoneType' object has no attribute 'get_host'
Which is coming from the get_current_site in my code:
#receiver(post_save, sender=MyModel)
def post_obj_save(sender, instance: MyModel, **kwargs):
def _get_html(obj: MyModel):
return render_to_string('confirmation_email.html', _get_context(obj))
def _get_context(obj: MyModel):
current_site = get_current_site(request=None)
domain = current_site.domain
action = reverse('obj_activation', request=None, format=None, kwargs={})
url = '{protocol}://{domain}/{action}'.format(protocol=PROTOCOL, domain=domain, action=action)
return {
'header': _('Thank you for registering with ASDF.'),
'prompt': _('In order to be able to log in into ASDF administrator panel, you have to activate your account using'),
'link_name': _('this link'),
'activation_url': url
}
send_mail(
_('ASDF account activation'),
_get_html(instance),
EMAIL_FROM,
[obj.owner.email],
fail_silently=False,
)
So the question is: how do I get full url of my view in a signal?
Btw. I'm using django-rest-framework.

In recent Django versions (probably your case), the domain is always taken from the request if SITE_ID is not defined in your settings. See this change introduced in 1.8 Django version:
Changed in Django 1.8:
This function will now lookup the current site based on
request.get_host() if the SITE_ID setting is not defined.
So, in your case request=None you must have the sites framework enabled, an entry for your current site/domain and SITE_ID setting pointing to the right instance in the Site table, try this and you will see :)

Related

Can I safely use `self.request.user.is_authenticated` in views?

We are using Django 2.1 for Speedy Net. There is a view which I want to be different for admin (staff) than for regular users. I added the following (3 first) lines to the code:
def get_user_queryset(self):
if (self.request.user.is_authenticated):
if ((self.request.user.is_staff) and (self.request.user.is_superuser)):
return User.objects.get_queryset()
return User.objects.active()
But the problem is, one of the tests fails with an error message:
AttributeError: 'WSGIRequest' object has no attribute 'user'
(link) And I want to know - Is it safe to use self.request.user.is_authenticated in views? Should I fix the test to work or can my code fail in production? The test uses RequestFactory(), is there a problem with RequestFactory() not containing attribute 'user'? Or is there a problem with my code which may also fail in production?
If I should fix the test, how do I do it? Here is the code of the test which fails:
class UserMixinTextCaseMixin(object):
def set_up(self):
super().set_up()
self.factory = RequestFactory()
self.user = ActiveUserFactory(slug='look-at-me', username='lookatme')
self.other_user = ActiveUserFactory()
def test_find_user_by_exact_slug(self):
view = UserMixinTestView.as_view()(self.factory.get('/look-at-me/some-page/'), slug='look-at-me')
self.assertEqual(first=view.get_user().id, second=self.user.id)
By the way, I'm not sure what is the purpose of slug='look-at-me' in the view = ... line of the test?
Yeah... It's safe to use request.user or self.request.user in Django and it is the Django way of checking whether the User is authenticated or not.
AttributeError: 'WSGIRequest' object has no attribute 'user'
Django assigning the authenticated user to the request object within the Django Middleware execution, specifically within the AuthenticationMiddleware class (source-code of AuthenticationMiddleware class).
So, adding the AuthenticationMiddleware class to the Middleware settings or creating your own middleware class is will solve the AttributeError: 'WSGIRequest' object has no attribute 'user' error.
I found out this answer on Stack Overflow.
From the docs:
# Recall that middleware are not supported. You can simulate a
# logged-in user by setting request.user manually.
request.user = self.user
# Or you can simulate an anonymous user by setting request.user to
# an AnonymousUser instance.
request.user = AnonymousUser()
Adding request.user = AnonymousUser() in the tests solves the problem:
class UserMixinTextCaseMixin(object):
def set_up(self):
super().set_up()
self.factory = RequestFactory()
self.user = ActiveUserFactory(slug='look-at-me', username='lookatme')
self.other_user = ActiveUserFactory()
def test_find_user_by_exact_slug(self):
request = self.factory.get('/look-at-me/some-page/')
request.user = AnonymousUser()
view = UserMixinTestView.as_view()(request=request, slug='look-at-me')
self.assertEqual(first=view.get_user().id, second=self.user.id)
Replacing MIDDLEWARE with MIDDLEWARE_CLASSES doesn't solve the problem and we are already using 'django.contrib.auth.middleware.AuthenticationMiddleware', but this is not used by RequestFactory().

Django Rest Framework gives 302 in Unit tests when force_login() on detail view?

I'm using Django Rest Framework to serve an API. I've got a couple tests which work great. To do a post the user needs to be logged in and I also do some checks for the detail view for a logged in user. I do this as follows:
class DeviceTestCase(APITestCase):
USERNAME = "username"
EMAIL = 'a#b.com'
PASSWORD = "password"
def setUp(self):
self.sa_group, _ = Group.objects.get_or_create(name=settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME)
self.authorized_user = User.objects.create_user(self.USERNAME, self.EMAIL, self.PASSWORD)
self.sa_group.user_set.add(self.authorized_user)
def test_post(self):
device = DeviceFactory.build()
url = reverse('device-list')
self.client.force_login(self.authorized_user)
response = self.client.post(url, data={'some': 'test', 'data': 'here'}, format='json')
self.client.logout()
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
# And some more tests here
def test_detail_logged_in(self):
device = DeviceFactory.create()
url = reverse('device-detail', kwargs={'pk': device.pk})
self.client.force_login(self.authorized_user)
response = self.client.get(url)
self.client.logout()
self.assertEqual(status.HTTP_200_OK, response.status_code, 'Wrong response code for {}'.format(url))
# And some more tests here
The first test works great. It posts the new record and all checks pass. The second test fails though. It gives an error saying
AssertionError: 200 != 302 : Wrong response code for /sa/devices/1/
It turns out the list view redirects the user to the login screen. Why does the first test log the user in perfectly, but does the second test redirect the user to the login screen? Am I missing something?
Here is the view:
class APIAuthGroup(InAuthGroup):
"""
A permission to allow all GETS, but only allow a POST if a user is logged in,
and is a member of the slimme apparaten role inside keycloak.
"""
allowed_group_names = [settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME]
def has_permission(self, request, view):
return request.method in SAFE_METHODS \
or super(APIAuthGroup, self).has_permission(request, view)
class DevicesViewSet(DatapuntViewSetWritable):
"""
A view that will return the devices and makes it possible to post new ones
"""
queryset = Device.objects.all().order_by('id')
serializer_class = DeviceSerializer
serializer_detail_class = DeviceSerializer
http_method_names = ['post', 'list', 'get']
permission_classes = [APIAuthGroup]
Here is why you are getting this error.
Dependent Libraries
I did some searching by Class Names to find which libraries you were using so that I can re-create the problem on my machine. The library causing the problem is the one called keycloak_idc. This library installs another library mozilla_django_oidc which would turn out to be the reason you are getting this.
Why This Library Is Causing The Problem
Inside the README file of this library, it gives you instructions on how to set it up. These are found in this file. Inside these instructions, it instructed you to add the AUTHENTICATION_BACKENDS
AUTHENTICATION_BACKENDS = [
'keycloak_oidc.auth.OIDCAuthenticationBackend',
...
]
When you add this authentication backend, all your requests pass through a Middleware defined inside the SessionRefresh class defined inside mozilla_django_oidc/middleware.py. Inside this class, the method process_request() is always called.
The first thing this method does is call the is_refreshable_url() method which always returns False if the request method was POST. Otherwise (when the request method is GET), it will return True.
Now the body of this if condition was as follows.
if not self.is_refreshable_url(request):
LOGGER.debug('request is not refreshable')
return
# lots of stuff in here
return HttpResponseRedirect(redirect_url)
Since this is a middleware, if the request was POST and the return was None, Django would just proceed with actually doing your request. However when the request is GET and the line return HttpResponseRedirect(redirect_url) is triggered instead, Django will not even proceed with calling your view and will return the 302 response immediately.
The Solution
After a couple of hours debugging this, I do not the exact logic behind this middleware or what exactly are you trying to do to provide a concrete solution since this all started based off guess-work but a naive fix can be that you remove the AUTHENTICATION_BACKENDS from your settings file. While I feel that this is not acceptable, maybe you can try using another library that accomplishes what you're trying to do or find an alternative way to do it. Also, maybe you can contact the author and see what they think.
So i guess you have tested this and you get still the same result:
class APIAuthGroup(InAuthGroup):
def has_permission(self, request, view):
return True
Why do you use DeviceFactory.build() in the first test and DeviceFactory.create() in the second?
Maybe a merge of the two can help you:
def test_get(self):
device = DeviceFactory.build()
url = reverse('device-list')
response = self.client.get(url)
self.assertEqual(status.HTTP_200_OK, response.status_code)
Is this a problem with the setUp() method? From what I see, you may be setting self.authorize_user to a user that was already created on the first test.
Instead, I would create the user on each test, making sure that the user doesn't exist already, like so:
user_exists = User.objects.filter(username=self.USERNAME, email=self.EMAIL).exists()
if not user_exists:
self.authorize_user = User.objects.create_user....
That would explain why your first test did pass, why your second didn't, and why #anupam-chaplot's answer didn't reproduce the error.
Your reasoning and code looks ok.
However you are not giving the full code, there must be error you are not seeing.
Suspicious fact
It isn't be default 302 when you are not logged in.
(#login_required, etc redirects but your code doesn't have it)
Your APIAuthGroup permission does allow GET requests for non-logged-in user ( return request.method in SAFE_METHODS), and you are using GET requests (self.client.get(url))
So it means you are not hitting the endpoint that you think you are hitting (your get request is not hitting the DevicesViewSet method)
Or it could be the case you have some global permission / redirect related setting in your settings.py which could be DRF related..
eg :
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Guess
url = reverse('device-detail', kwargs={'pk': device.pk})
might not point to the url you are thinking..
maybe there's another url (/sa/devices/1/) that overrides the viewset's url. (You might have a django view based url)
And I didn't address why you are getting redirected after force_login.
If it's indeed login related redirect, all I can think of is self.authorized_user.refresh_from_db() or refreshing the request ..
I guess some loggin related property (such as session, or request.user) might point to old instance .. (I have no evidence or fact this can happen, but just a hunch) and you better off not logging out/in for every test case)
You should make a seperate settings file for testing and add to the test command --settings=project_name.test_settings, that's how I was told to do.

Authenticate in Django without a database

I have a Django app that gets it's data completely from apis. so I don't have to use database. Session data is stored on signed cookies. I tried to code a custom User model and a custom auth backend like on the docs, but I get the following error:
django.core.exceptions.ImproperlyConfigured: AUTH_USER_MODEL refers to model 'my_app.MyUser' that has not been installed
My settings.py:
AUTH_USER_MODEL = 'my_app.MyUser'
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',
'my_app.backends.LoginAuthBackend',)
models.py:
class MyUser(object):
def save(self):
pass
objects = None
username = ""
Here, If a try use the AbstractUser from django instead of Object I got the following error: AttributeError: 'NoneType' object has no attribute '_meta' or the db table doesn't exit.
backends.py
class LoginAuthBackend(object):
def authenticate(self, username=None, password=None):
if username and password:
try:
response = my_auth_function(username, password)
if response.status_code == 200:
token = response.get('my_key')
user = MyUser()
return user
except MyCustomException:
return None
It's drives me crazy. Looks like Django that's not easy to use without a DB.
EDIT
After several of tries, a simple way to solve this is remove 'django.contrib.auth.backends.ModelBackend' from AUTHENTICATION_BACKENDS and AUTH_USER_MODEL from settings. The model continues basically the same way. works smoothly
The default set of authentication back-end processors is defined in the AUTHENTICATION_BACKENDS setting. See the Django documentation for Customizing authentication.
By default, AUTHENTICATION_BACKENDS is set to:
['django.contrib.auth.backends.ModelBackend']
That’s the basic authentication backend that checks the Django users database and queries the built-in permissions.
So, if you don't want the django.contrib.auth.backends.ModelBackend authentication method, remove that from the list. You'll probably want to find (or create) a different one and add that to the list.

request.user not being populated by my django auth backend

I have written a authorization backend class that implements a authenticate method and a get_user method as per the django docs. I've added
AUTHENTICATION_BACKENDS = ('src.lib.auth_backend.MyBackend',)
to my settings.py file. Through print statements I can see that my code is being run and that it is returning a user object of the class I defined with AUTH_USER_MODEL in my settings.py.
By the time I get the request object within my django-rest-framework has_object_permsion function, request.user is always set to AnonymousUser.
Am I missing a step?
I've tried this with and without django.contrib.auth.middleware.AuthenticationMiddleware installed and get the same outcome.
This failure is happening in the following unit test
def test_get_user(self):
client = APIClient() # rest_framework.test.APIClient
client.login(username='user1',password='user1Password')
res = client.get('/websvc/users/' + str(user.user_id) + '/') # request.user will be AnonymousUser
self.assertEqual(res.status_code, 200) # it will be 403, which is appropriate for AnonymousUser
A couple of things may be wrong.
Have you created the user, before runnign the test?
What returns client.login?
Show us the whole test, to tell you what's wrong.

where django store request['user'] in what file

As mentioned in the documentation, authenticated user's object is stored within user variable in templates. i need where django stored user variable in apps file thanks:
user = request.user
request['user'] = user #where is?
thanks for help
It's in the AuthenticationMiddleware.
The official doc mentioned it:
link:
AuthenticationMiddleware associates users with requests using
sessions.
link:
class AuthenticationMiddleware
Adds the user attribute, representing
the currently-logged-in user, to every incoming HttpRequest object.
See Authentication in Web requests.
source code(django.contrib.auth.middleware.py):
class AuthenticationMiddleware(object):
def process_request(self, request):
assert hasattr(request, 'session'), "The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."
request.user = SimpleLazyObject(lambda: get_user(request))
Make sure you're using RequestContext. Otherwise user is not available in the templates.

Categories

Resources