Django show object count on admin interface - django

I have modified my dashboard with the help of dashboard.py file. There in I have added a custom module/widget called 'My Widget' (haven't created myself but rather just appended as a child referring to a specific django app). Code is as follows:
# append an app list module for "Administration"
self.children.append(modules.AppList(
_('Administration'),
models=('django.contrib.*',),
))
self.children.append(modules.AppList(
_('My widget'),
models=('flights.*',),
children=[
]
))
flights is my django app which has three models. Dashboard looks like following:
On this widget I want to show the count of users who did log in today.
The count itself I was able to get (not on admin interface but through a view accessible via url for testing purpose) with following code snippet:
User.objects.filter(last_login__startswith=timezone.now().date()).count()
How can I show such a count on this widget?
The entire dashboard.py:
"""
This file was generated with the customdashboard management command, it
contains the two classes for the main dashboard and app index dashboard.
You can customize these classes as you want.
To activate your index dashboard add the following to your settings.py::
ADMIN_TOOLS_INDEX_DASHBOARD = 'api.dashboard.CustomIndexDashboard'
And to activate the app index dashboard::
ADMIN_TOOLS_APP_INDEX_DASHBOARD = 'api.dashboard.CustomAppIndexDashboard'
"""
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from admin_tools.dashboard import modules, Dashboard, AppIndexDashboard
from admin_tools.utils import get_admin_site_name
from admin_tools_stats.modules import DashboardCharts, get_active_graph
class CustomIndexDashboard(Dashboard):
"""
Custom index dashboard for api.
"""
columns = 3
def init_with_context(self, context):
site_name = get_admin_site_name(context)
# append a link list module for "quick links"
self.children.append(modules.LinkList(
_('Quick links'),
layout='inline',
draggable=False,
deletable=False,
collapsible=False,
children=[
[_('Return to site'), '/'],
[_('Change password'),
reverse('%s:password_change' % site_name)],
[_('Log out'), reverse('%s:logout' % site_name)],
]
))
graph_list = get_active_graph()
for i in graph_list:
kwargs = {}
kwargs['require_chart_jscss'] = True
kwargs['graph_key'] = i.graph_key
if context['request'].POST.get('select_box_' + i.graph_key):
kwargs['select_box_' + i.graph_key] = context['request'].POST['select_box_' + i.graph_key]
self.children.append(DashboardCharts(**kwargs))
# append an app list module for "Applications"
self.children.append(modules.AppList(
_('All Applications'),
exclude=('django.contrib.*',),
))
# append an app list module
self.children.append(modules.AppList(
_('Dashboard Stats Settings'),
models=('admin_tools_stats.*', ),
))
# append an app list module for "Administration"
self.children.append(modules.AppList(
_('Administration'),
models=('django.contrib.*',),
))
self.children.append(modules.AppList(
_('My widget'),
models=('flights.*',),
content='test content',
children=[
]
))
# append a recent actions module
self.children.append(modules.RecentActions(_('Recent Actions'), 4))
# append another link list module for "support".
self.children.append(modules.LinkList(
_('Support'),
children=[
{
'title': _('Django documentation'),
'url': 'http://docs.djangoproject.com/',
'external': True,
},
{
'title': _('Django "django-users" mailing list'),
'url': 'http://groups.google.com/group/django-users',
'external': True,
},
{
'title': _('Django irc channel'),
'url': 'irc://irc.freenode.net/django',
'external': True,
},
]
))
class CustomAppIndexDashboard(AppIndexDashboard):
"""
Custom app index dashboard for api.
"""
# we disable title because its redundant with the model list module
title = ''
def __init__(self, *args, **kwargs):
AppIndexDashboard.__init__(self, *args, **kwargs)
# append a model list module and a recent actions module
self.children += [
modules.ModelList(self.app_title, self.models),
modules.RecentActions(
_('Recent Actions'),
include_list=self.get_app_content_types(),
limit=5
)
]
def init_with_context(self, context):
"""
Use this method if you need to access the request context.
"""
return super(CustomAppIndexDashboard, self).init_with_context(context)

Related

Flask app-builder how to make REST API with file items

I'm making a REST api that files can be uploaded based in MODEL-VIEW in flask-appbuilder like this.
But I don't know how to call REST API (POST /File).
I tried several different ways. but I couldn't.
Let me know the correct or the alternative ways.
[client code]
file = {'file':open('test.txt', 'rb'),'description':'test'}
requests.post(url, headers=headers, files=file)
==> Failed
model.py
class Files(Model):
__tablename__ = "project_files"
id = Column(Integer, primary_key=True)
file = Column(FileColumn, nullable=False)
description = Column(String(150))
def download(self):
return Markup(
'<a href="'
+ url_for("ProjectFilesModelView.download", filename=str(self.file))
+ '">Download</a>'
)
def file_name(self):
return get_file_original_name(str(self.file))
view.py
class FileApi(ModelRestApi):
resource_name = "File"
datamodel = SQLAInterface(Files)
allow_browser_login = True
appbuilder.add_api(FileApi)
FileColumn is only a string field that saves the file name in the database. The actual file is saved to config['UPLOAD_FOLDER'].
This is taken care of by flask_appbuilder.filemanager.FileManager.
Furthermore, ModelRestApi assumes that you are POSTing JSON data. In order to upload files, I followed Flask's documentation, which suggests to send a multipart/form-data request. Because of this, one needs to override ModelRestApi.post_headless().
This is my solution, where I also make sure that when a Files database row
is deleted, so is the relative file from the filesystem.
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.api import ModelRestApi
from flask_appbuilder.const import API_RESULT_RES_KEY
from flask_appbuilder.filemanager import FileManager
from flask import current_app, request
from marshmallow import ValidationError
from sqlalchemy.exc import IntegrityError
from app.models import Files
class FileApi(ModelRestApi):
resource_name = "file"
datamodel = SQLAInterface(Files)
def post_headless(self):
if not request.form or not request.files:
msg = "No data"
current_app.logger.error(msg)
return self.response_400(message=msg)
file_obj = request.files.getlist('file')
if len(file_obj) != 1:
msg = ("More than one file provided.\n"
"Please upload exactly one file at a time")
current_app.logger.error(msg)
return self.response_422(message=msg)
else:
file_obj = file_obj[0]
fm = FileManager()
uuid_filename = fm.generate_name(file_obj.filename, file_obj)
form = request.form.to_dict(flat=True)
# Add the unique filename provided by FileManager, which will
# be saved to the database. The original filename can be
# retrieved using
# flask_appbuilder.filemanager.get_file_original_name()
form['file'] = uuid_filename
try:
item = self.add_model_schema.load(
form,
session=self.datamodel.session)
except ValidationError as err:
current_app.logger.error(err)
return self.response_422(message=err.messages)
# Save file to filesystem
fm.save_file(file_obj, item.file)
try:
self.datamodel.add(item, raise_exception=True)
return self.response(
201,
**{API_RESULT_RES_KEY: self.add_model_schema.dump(
item, many=False),
"id": self.datamodel.get_pk_value(item),
},
)
except IntegrityError as e:
# Delete file from filesystem if the db record cannot be
# created
fm.delete_file(item.file)
current_app.logger.error(e)
return self.response_422(message=str(e.orig))
def pre_delete(self, item):
"""
Delete file from filesystem before removing the record from the
database
"""
fm = FileManager()
current_app.logger.info(f"Deleting {item.file} from filesystem")
fm.delete_file(item.file)
You can use this.
from app.models import Project, ProjectFiles
class DataFilesModelView(ModelView):
datamodel = SQLAInterface(ProjectFiles)
label_columns = {"file_name": "File Name", "download": "Download"}
add_columns = ["file", "description", "project"]
edit_columns = ["file", "description", "project"]
list_columns = ["file_name", "download"]
show_columns = ["file_name", "download"]
Last add the view to the menu.
appbuilder.add_view(DataFilesModelView,"File View")

flask-admin incorrect relative url

I am trying to create a simple admin for editing a mongo collection. I have posted the code below. It all works perfectly locally or as a docker container. However when I deploy this in our micro-service architecture the app lives at: SERVER_NAME/TEAM_NAME/APP_NAME/.
Flask routes set with #app.route work correctly. However the urls in the admin templates are not correct and always start directly at SERVER_NAME ignoring team-name and app-name. The actual pages and resources are located at the correct urls but the urls for the static resources are not found. How do I make sure the urls generated within flask-admin also take into account the relative url?
The code:
import os
import flask_admin
from wtforms import form, fields
from flask_admin.contrib.pymongo import ModelView, filters
# User admin
class WordPairsForm(form.Form):
text = fields.StringField("Text")
language = fields.SelectField("Language", choices=[("de", "german"), ("en", "english"), ("pl", "polish")])
label = fields.SelectField("Label", choices=[("badword", "bad word"), ("no_stay", "no overnight stay")])
active = fields.BooleanField("Active", default="checked")
class WordPairsView(ModelView):
column_list = ("text", "language", "label", "active")
column_sortable_list = ("text", "language", "label", "active")
column_searchable_list = ("text",)
column_filters = (
filters.FilterLike("text", "Text"),
filters.FilterNotLike("text", "Text"),
filters.FilterEqual("language", "Language", options=[("de", "german"), ("en", "english"), ("pl", "polish")]),
filters.FilterEqual("label", "Label", options=[("badword", "bad word"), ("no_stay", "no overnight stay")]),
filters.BooleanEqualFilter("active", "Active")
)
form = WordPairsForm
def create_form(self):
_form = super(WordPairsView, self).create_form()
return _form
def edit_form(self, obj):
_form = super(WordPairsView, self).edit_form(obj)
return _form
def get_list(self, *args, **kwargs):
count, data = super(WordPairsView, self).get_list(*args, **kwargs)
return count, data
def get_url
def add_admin(app):
admin = flask_admin.Admin(
app,
name="CQAS Admin",
url=os.getenv(
"F_ADMIN_URL",
"/admin"
),
static_url_path=os.getenv("F_ADMIN_STATIC_URL", None),
subdomain=os.getenv("F_ADMIN_SUBDOMAIN", None),
endpoint=os.getenv("F_ADMIN_ENDPOINT", None)
)
admin.add_view(WordPairsView(app.data.data, "WordPairs"))

Programmatically create a django group with permissions

In the Admin console, I can add a group and add a bunch of permissions that relate to my models, e.g.
api | project | Can add project
api | project | Can change project
api | project | Can delete project
How can I do this programmatically. I can't find any information out there on how to do this.
I have:
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from api.models import Project
new_group, created = Group.objects.get_or_create(name='new_group')
# Code to add permission to group ???
ct = ContentType.objects.get_for_model(Project)
# Now what - Say I want to add 'Can add project' permission to new_group?
UPDATE: Thanks for the answer you provided. I was able to use that to work out what I needed. In my case, I can do the following:
new_group, created = Group.objects.get_or_create(name='new_group')
proj_add_perm = Permission.objects.get(name='Can add project')
new_group.permissions.add(proj_add_perm)
Use below code
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from api.models import Project
new_group, created = Group.objects.get_or_create(name='new_group')
# Code to add permission to group ???
ct = ContentType.objects.get_for_model(Project)
# Now what - Say I want to add 'Can add project' permission to new_group?
permission = Permission.objects.create(codename='can_add_project',
name='Can add project',
content_type=ct)
new_group.permissions.add(permission)
I needed to create a default set of groups and permission (view only) for those groups. I came up with a manage.py command that may be useful to others (create_groups.py). You can add it to your <app>/management/commands dir, and then run via manage.py create_groups:
"""
Create permission groups
Create permissions (read only) to models for a set of groups
"""
import logging
from django.core.management.base import BaseCommand
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
GROUPS = ['developers', 'devops', 'qa', 'operators', 'product']
MODELS = ['video', 'article', 'license', 'list', 'page', 'client']
PERMISSIONS = ['view', ] # For now only view permission by default for all, others include add, delete, change
class Command(BaseCommand):
help = 'Creates read only default permission groups for users'
def handle(self, *args, **options):
for group in GROUPS:
new_group, created = Group.objects.get_or_create(name=group)
for model in MODELS:
for permission in PERMISSIONS:
name = 'Can {} {}'.format(permission, model)
print("Creating {}".format(name))
try:
model_add_perm = Permission.objects.get(name=name)
except Permission.DoesNotExist:
logging.warning("Permission not found with name '{}'.".format(name))
continue
new_group.permissions.add(model_add_perm)
print("Created default group and permissions.")
UPDATE: A bit more sophisticated now:
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
READ_PERMISSIONS = ['view', ] # For now only view permission by default for all, others include add, delete, change
WRITE_PERMISSIONS = ['add', 'change', 'delete']
EMAIL_USER_DOMAIN = 'your-domain.com'
# Add your groups here, app and model code
GROUP_MODEL = ('auth', 'group')
USER_MODEL = ('auth', 'user')
PERMISSION_MODEL = ('auth', 'permission')
LOG_ENTRY_MODEL = ('admin', 'logentry')
def add_group_permissions(group_names, model_natural_keys, permissions):
"""
Add permissions to the provided groups for the listed models.
Error raised if permission or `ContentType` can't be found.
:param group_names: iterable of group names
:param model_natural_keys: iterable of 2-tuples containing natural keys for ContentType
:param permissions: iterable of str (permission names i.e. add, view)
"""
for group_name in group_names:
group, created = Group.objects.get_or_create(name=group_name)
for model_natural_key in model_natural_keys:
perm_to_add = []
for permission in permissions:
# using the 2nd element of `model_natural_key` which is the
# model name to derive the permission `codename`
permission_codename = f"{permission}_{model_natural_key[1]}"
try:
perm_to_add.append(
Permission.objects.get_by_natural_key(
permission_codename, *model_natural_key
)
)
except Permission.DoesNotExist:
# trying to add a permission that doesn't exist; log and continue
logging.error(
f"permissions.add_group_permissions Permission not found with name {permission_codename!r}."
)
raise
except ContentType.DoesNotExist:
# trying to add a permission that doesn't exist; log and continue
logging.error(
"permissions.add_group_permissions ContentType not found with "
f"natural name {model_natural_key!r}."
)
raise
group.permissions.add(*perm_to_add)
def set_users_group(users, group):
"""
Adds users to specific permission group.
If user or group does not exist, they are created.
Intended for use with special users for api key auth.
:param users: list of str, usernames
:param group: str, group for which users should be added to
:return: list, user objects added to group
"""
users = users or []
user_objs = []
for user_name in users:
try:
user = User.objects.get(username=user_name)
except User.DoesNotExist:
user = User.objects.create_user(username=user_name,
email=f'{user_name}#{EMAIL_USER_DOMAIN}',
password='')
user_objs.append(user)
group, created = Group.objects.get_or_create(name=group)
group.user_set.add(user)
return user_objs
API_READ_GROUP = 'api-read-users'
API_WRITE_GROUP = 'api-write-users'
READ_GROUPS = [API_READ_GROUP, ]
WRITE_GROUPS = [API_WRITE_GROUP, ] # Can be used in same way as read users below
# Adding users to a group
set_users_group(READ_USERS, API_READ_GROUP)
# Setting up the group permissions i.e. read for a group of models
add_group_permissions(READ_GROUPS, [GROUP_MODEL, USER_MODEL, LOG_ENTRY_MODEL], READ_PERMISSIONS)
I also found that using manage.py update_permissions is useful to sort out/clean up stale permissions if models have changed etc.. Its part of django-extensions commands.
Inspired by radtek's answer I created a bit better version (in my opinion).
It allows specifying model as object (instead of string) and specifying all configuration in one dictionary (instead of several lists)
# backend/management/commands/initgroups.py
from django.core.management import BaseCommand
from django.contrib.auth.models import Group, Permission
from backend import models
GROUPS_PERMISSIONS = {
'ConnectionAdmins': {
models.StaticCredentials: ['add', 'change', 'delete', 'view'],
models.NamedCredentials: ['add', 'change', 'delete', 'view'],
models.Folder: ['add', 'change', 'delete', 'view'],
models.AppSettings: ['view'],
},
}
class Command(BaseCommand):
def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs)
help = "Create default groups"
def handle(self, *args, **options):
# Loop groups
for group_name in GROUPS_PERMISSIONS:
# Get or create group
group, created = Group.objects.get_or_create(name=group_name)
# Loop models in group
for model_cls in GROUPS_PERMISSIONS[group_name]:
# Loop permissions in group/model
for perm_index, perm_name in \
enumerate(GROUPS_PERMISSIONS[group_name][model_cls]):
# Generate permission name as Django would generate it
codename = perm_name + "_" + model_cls._meta.model_name
try:
# Find permission object and add to group
perm = Permission.objects.get(codename=codename)
group.permissions.add(perm)
self.stdout.write("Adding "
+ codename
+ " to group "
+ group.__str__())
except Permission.DoesNotExist:
self.stdout.write(codename + " not found")
Taking ideas from the answers of #radtek and #Pavel I created my own version of create_groups.py which I call using python manage.py create_groups. This file is stored on app_name/management/commands/create_groups.py. I created a __init__.py inside each the management and commands folders.
I have created the possibility to control each model permissions separately because I had a group of users called Member that must have different permissions on different models.
I also added the possibility to create users with emails and a default password that would have to be changed afterwards and associate them with a certain group.
from django.core.management import BaseCommand
from django.contrib.auth.models import User, Group , Permission
import logging
GROUPS = {
"Administration": {
#general permissions
"log entry" : ["add","delete","change","view"],
"group" : ["add","delete","change","view"],
"permission" : ["add","delete","change","view"],
"user" : ["add","delete","change","view"],
"content type" : ["add","delete","change","view"],
"session" : ["add","delete","change","view"],
#django app model specific permissions
"project" : ["add","delete","change","view"],
"order" : ["add","delete","change","view"],
"staff time sheet" : ["add","delete","change","view"],
"staff" : ["add","delete","change","view"],
"client" : ["add","delete","change","view"],
},
"Member": {
#django app model specific permissions
"project" : ["view"],
"order" : ["view"],
"staff time sheet" : ["add","delete","change","view"],
},
}
USERS = {
"my_member_user" : ["Member","member#domain.cu","1234*"],
"my_admin_user" : ["Administration","admin#domain.ca","1234"],
"Admin" : ["Administration","superuser#domain.cu","1234"],
}
class Command(BaseCommand):
help = "Creates read only default permission groups for users"
def handle(self, *args, **options):
for group_name in GROUPS:
new_group, created = Group.objects.get_or_create(name=group_name)
# Loop models in group
for app_model in GROUPS[group_name]:
# Loop permissions in group/model
for permission_name in GROUPS[group_name][app_model]:
# Generate permission name as Django would generate it
name = "Can {} {}".format(permission_name, app_model)
print("Creating {}".format(name))
try:
model_add_perm = Permission.objects.get(name=name)
except Permission.DoesNotExist:
logging.warning("Permission not found with name '{}'.".format(name))
continue
new_group.permissions.add(model_add_perm)
for user_name in USERS:
new_user = None
if user_name == "Admin":
new_user, created = User.objects.get_or_create(username=user_name,is_staff = True,is_superuser = True, email = USERS[user_name][1])
else:
new_user, created = User.objects.get_or_create(username=user_name,is_staff = True, email = USERS[user_name][1])
new_user.set_password(USERS[user_name][2])
new_user.save()
if USERS[user_name][0] == str(new_group):
new_group.user_set.add(new_user)
print("Adding {} to {}".format(user_name,new_group))

mocking a method on django model using post_save signal

So here's something I'm trying to figure out. I've got a method that is triggered by post_save
for this "Story" model. Works fine. What I need to do is figure out how to mock out the test, so I can fake the call and make assertions on my returns. I think I need to patch it somehow, but I've tried a couple different ways without much success. Best i can get is a object instance, but it ignores values I pass in.
I've commented in my test where my confusion lies. Any help would be welcome.
Here's my test:
from django.test import TestCase
from django.test.client import Client
from marketing.blog.models import Post, Tag
from unittest.mock import patch, Mock
class BlogTestCase(TestCase):
fixtures = [
'auth-test.json',
'blog-test.json',
]
def setUp(self):
self.client = Client()
def test_list(self):
# verify that we can load the list page
r = self.client.get('/blog/')
self.assertEqual(r.status_code, 200)
self.assertContains(r, "<h1>The Latest from Our Blog</h1>")
self.assertContains(r, 'Simple JavaScript Date Formatting')
self.assertContains(r, 'Page 1 of 2')
# loading a page out of range should redirect to last page
r = self.client.get('/blog/5/', follow=True)
self.assertEqual(r.redirect_chain, [
('http://testserver/blog/2/', 302)
])
self.assertContains(r, 'Page 2 of 2')
# verify that unpublished posts are not displayed
with patch('requests') as mock_requests:
# my futile attempt at mocking.
# creates <MagicMock> object but not able to call return_values
mock_requests.post.return_value = mock_response = Mock()
# this doesn't get to the magic mock object. Why?
mock_response.status_code = 201
p = Post.objects.get(id=5)
p.published = False
# post_save signal runs here and requests is called.
# Needs to be mocked.
p.save()
r = self.client.get('/blog/')
self.assertNotContains(r, 'Simple JavaScript Date Formatting')
Here's the model:
from django.db import models
from django.conf import settings
from django.db.models import signals
import requests
def update_console(sender, instance, raw, created, **kwargs):
# ignoring raw so that test fixture data can load without
# hitting this method.
if not raw:
update = instance
json_obj = {
'author': {
'alias': 'the_dude',
'token': 'the_dude'
},
'text': update.description,
}
headers = {'content-type': 'application/json'}
path = 'http://testserver.com:80/content/add/'
request = requests(path, 'POST',
json_obj, headers=headers,
)
if request.status_code < 299:
story_id = request.json().get('id')
if story_id:
# disconnect and reconnect signal so
# we don't enter recursion-land
signals.post_save.disconnect(
update_console,
sender = Story, )
update.story_id = story_id
update.save()
signals.post_save.connect(
update_console,
sender = Story, )
else:
raise AttributeError('Error Saving to console, '+ request.text)
class Story(models.Model):
"""Lets tell a story"""
story_id = models.CharField(
blank=True,
max_length=10,
help_text="This maps to the id of the post"
)
slug = models.SlugField(
unique=True,
help_text="This is used in URL and in code references.",
)
description = models.TextField(
help_text='2-3 short paragraphs about the story.',
)
def __str__(self):
return self.short_headline
# add/update this record as a custom update in console
signals.post_save.connect(update_console, sender = Story)
You need to patch requests in the module where it is actually used, i.e.
with patch('path.to.your.models.requests') as mock_requests:
mock_requests.return_value.status_code = 200
mock_requests.return_value.json.return_value = {'id': story_id'}
...
The documentation offers more detailed explanations on where to patch:
patch works by (temporarily) changing the object that a name points to with another one. There can be many names pointing to any individual object, so for patching to work you must ensure that you patch the name used by the system under test.
The basic principle is that you patch where an object is looked up, which is not necessarily the same place as where it is defined.
Here, you need to patch the name requests inside the models module, hence the need to provide its full path.

django social auth limiting user data

I have configured django social auth's to take from google only e-mail, but google shows this screen alerting app user that gender, date of birth, picture, language will be collect:
My django-social-auth config is as follow:
WHITE_LISTED_DOMAINS = [ 'some_domain', ]
GOOGLE_WHITE_LISTED_DOMAINS = WHITE_LISTED_DOMAINS
SOCIAL_AUTH_EXTRA_DATA = False
#LOGIN_ERROR_URL = '/login-error/' Not set
#SOCIAL_AUTH_DEFAULT_USERNAME = 'new_social_auth_user' Not set
#GOOGLE_CONSUMER_KEY = '' Not set
#GOOGLE_CONSUMER_SECRET = '' Not set
#GOOGLE_OAUTH2_CLIENT_ID = '' Not set
#GOOGLE_OAUTH2_CLIENT_SECRET = '' Not set
SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL = False
SOCIAL_AUTH_PROTECTED_USER_FIELDS = ['email',]
INSTALLED_APPS = (
'django.contrib.auth',
...
'social_auth',
)
How can I do to avoid this google message?
EDITED
I have move to GoogleOauth2 auth and inherit and change google backend:
from social_auth.backends.google import *
GOOGLE_OAUTH2_SCOPE = ['https://www.googleapis.com/auth/userinfo.email',]
class GoogleOAuth2(BaseOAuth2):
"""Google OAuth2 support"""
AUTH_BACKEND = GoogleOAuth2Backend
AUTHORIZATION_URL = 'https://accounts.google.com/o/oauth2/auth'
ACCESS_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'
REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke'
REVOKE_TOKEN_METHOD = 'GET'
SETTINGS_SECRET_NAME = 'GOOGLE_OAUTH2_CLIENT_SECRET'
SCOPE_VAR_NAME = 'GOOGLE_OAUTH_EXTRA_SCOPE'
DEFAULT_SCOPE = GOOGLE_OAUTH2_SCOPE
REDIRECT_STATE = False
print DEFAULT_SCOPE #<------ to be sure
def user_data(self, access_token, *args, **kwargs):
"""Return user data from Google API"""
return googleapis_profile(GOOGLEAPIS_PROFILE, access_token)
#classmethod
def revoke_token_params(cls, token, uid):
return {'token': token}
#classmethod
def revoke_token_headers(cls, token, uid):
return {'Content-type': 'application/json'}
But google still ask for profile data, profile is still in scope:
https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/userinfo.email+https://www.googleapis.com/auth/userinfo.profile&redirect_uri=...
Runs fine if I modify by hand social-auth code instead inherit:
def get_scope(self):
return ['https://www.googleapis.com/auth/userinfo.email',]
What is wrong with my code?
That's because the default scope used on google backend is set to that (email and profile information), it's defined here. In order to avoid that you can create your own google backend which just sets the desired scope, then use that backend instead of the built in one. Example:
from social_auth.backends.google import GoogleOAuth2
class SimplerGoogleOAuth2(GoogleOAuth2):
DEFAULT_SCOPE = ['https://www.googleapis.com/auth/userinfo.email']
Those who don't know how to add in AUTHENTICATION_BACKENDS, if using the way Omab suggested you need to add newly defined backend in your setting.py file:
AUTHENTICATION_BACKENDS = (
'app_name.file_name.class_name', #ex: google_auth.views.SimplerGoogleOAuth2
# 'social_core.backends.google.GoogleOAuth2', # comment this as no longer used
'django.contrib.auth.backends.ModelBackend',
)
To know how to create the class SimplerGoogleOAuth2 check Omab's answer.