Commit 1fc7d5f5 authored by Luc Saffre's avatar Luc Saffre
Browse files
parent 48574df7
......@@ -97,7 +97,6 @@ Permissions:
- :class:`UserGroups <lino.modlib.users.mixins.UserGroups>`
- :class:`UserLevels <lino.modlib.users.mixins.UserLevels>`
- :func:`add_user_group <lino.modlib.users.mixins.add_user_group>`
Workflows:
......
......@@ -75,6 +75,12 @@
{{_("Or sign in using social auth:")}}
<div id="google-plus-button">[Google+ Sign In]</div>
{% endif %}
{% if site.social_auth_backends %}
{{_("Or sign in using ")}}
{% for e in site.get_social_auth_links() %}
{{E.tostring(e)}}
{% endfor %}
{% endif %}
</p>
{% if site.is_demo_site %}
<p>
......
......@@ -18,13 +18,8 @@ from django.utils.encoding import force_text
from django.conf import settings
from django.db import models
from lino import AFTER17, AFTER18
if AFTER17:
from django.apps import apps
get_models = apps.get_models
else:
from django.db.models.loading import get_models
from django.apps import apps
get_models = apps.get_models
from lino.core import constants
......@@ -66,10 +61,7 @@ def discover_choosers():
#~ logger.debug("Instantiate model reports...")
for model in get_models():
#~ n = 0
if AFTER17:
allfields = model._meta.fields
else:
allfields = model._meta.fields + model._meta.virtual_fields
allfields = model._meta.fields
for field in allfields:
check_for_chooser(model, field)
#~ logger.debug("Discovered %d choosers in model %s.",n,model)
......@@ -145,11 +137,7 @@ def setup_params_choosers(self):
if isinstance(fld, models.ForeignKey):
msg = "Invalid target %s in parameter {} of {}".format(
k, self)
# Before Django 1.8:
if AFTER18:
fld.rel.model = resolve_model(fld.rel.model, strict=msg)
else:
fld.rel.to = resolve_model(fld.rel.to, strict=msg)
fld.rel.model = resolve_model(fld.rel.model, strict=msg)
from lino.core.kernel import set_default_verbose_name
set_default_verbose_name(fld)
#~ if fld.verbose_name is None:
......@@ -280,7 +268,7 @@ class Action(Parametrizable, Permittable):
def get_data_elem(self, name):
# same as in Actor but here it is an instance method
return None
return self.defining_actor.get_data_elem(name)
def get_param_elem(self, name):
# same as in Actor but here it is an instance method
......
......@@ -636,11 +636,11 @@ class Actor(with_metaclass(ActorMetaClass, type('NewBase', (actions.Parametrizab
vf = fields.VirtualField(v, field_getter(k))
cls.add_virtual_field(k, vf)
@classmethod
def inject_field(cls, name, fld):
# called from auth.add_user_group()
setattr(cls, name, fld)
cls.register_class_attribute(name, fld)
# @classmethod
# def inject_field(cls, name, fld):
# # called from auth.add_user_group()
# setattr(cls, name, fld)
# cls.register_class_attribute(name, fld)
@classmethod
def get_pk_field(self):
......
......@@ -224,6 +224,17 @@ class FakeField(object):
def has_default(self):
return self.default is not NOT_PROVIDED
def get_default(self):
return self.default
def set_attributes_from_name(self, name):
if not self.name:
self.name = name
self.attname = name
self.column = None
self.concrete = False
# if self.verbose_name is None and self.name:
# self.verbose_name = self.name.replace('_', ' ')
class RemoteField(FakeField):
"""A field on a related object.
......@@ -306,7 +317,7 @@ class DisplayField(FakeField):
raise NotImplementedError
def value_from_object(self, obj, ar=None):
return ''
return self.default
class HtmlBox(DisplayField):
......@@ -541,7 +552,6 @@ def virtualfield(return_type):
class Constant(object):
"""
Deserves more documentation.
"""
......@@ -549,7 +559,6 @@ class Constant(object):
def __init__(self, text_fn):
self.text_fn = text_fn
def constant():
"""Decorator to turn a function into a :class:`Constant`. The
function must accept one positional argument `datasource`.
......@@ -849,7 +858,6 @@ class DummyField(FakeField):
def set_attributes_from_name(self, k):
pass
def wildcard_data_elems(model):
"""Yield names to be used as wildcard in the :attr:`column_names` of a
table or when :func:`fields_list` finds a ``*``.
......
......@@ -202,6 +202,8 @@ class Menu(MenuItem):
def __init__(self, user_profile, name, label=None, parent=None, **kw):
MenuItem.__init__(self, name, label, **kw)
self.parent = parent
if settings.SITE.user_types_module:
assert user_profile is not None
self.user_profile = user_profile
self.clear()
......
......@@ -499,6 +499,30 @@ class Site(object):
"""
social_auth_backends = None
"""A list of backends for `Python Social Auth
<https://github.com/python-social-auth>`__ (PSA).
Having this at a value different from `None` means that this site
uses authentication via third-party providers.
Sites which use this must also install PSA into their
environment::
$ pip install social-auth-app-django
Depending on the backend you must also add credentials in your
local :xfile:`settings.py` file, e.g.::
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = \
'1234567890-a1b2c3d4e5.apps.googleusercontent.com'
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'SH6da...'
A working example is in the :mod:`lino_book.projects.team` demo
project.
"""
use_ipdict = False
"""Whether this site uses :mod:`lino.modlib.ipdict`.
......@@ -1904,8 +1928,19 @@ this field.
backends.append('lino.core.auth.backends.RemoteUserBackend')
else:
backends.append('lino.core.auth.backends.ModelBackend')
if self.social_auth_backends is not None:
backends += self.social_auth_backends
self.define_settings(AUTHENTICATION_BACKENDS=backends)
self.update_settings(
LOGIN_URL='/accounts/login/',
LOGIN_REDIRECT_URL = '/',
# LOGIN_REDIRECT_URL = '/accounts/profile/',
LOGOUT_REDIRECT_URL = None)
def collect_settings_subdirs(lst, name, max_count=None):
def add(p):
p = p.replace(os.sep, "/")
......@@ -2579,10 +2614,38 @@ this field.
# version = getattr(yaml, '__version__', '')
# yield ("PyYaml", version, "http://pyyaml.org/")
if self.social_auth_backends is not None:
try:
import social_django
version = social_django.__version__
except ImportError:
version = self.not_found_msg
name = "social-django"
yield (name, version, "https://github.com/python-social-auth")
for p in self.installed_plugins:
for u in p.get_used_libs(html):
yield u
def get_social_auth_links(self):
# print("20171207 site.py")
# elems = []
if self.social_auth_backends is None:
return
from social_core.backends.utils import load_backends
# from collections import OrderedDict
# from django.conf import settings
# from social_core.backends.base import BaseAuth
# backend = module_member(auth_backend)
# if issubclass(backend, BaseAuth):
for b in load_backends(
self.social_auth_backends).values():
yield E.a(b.name, href="/oauth/login/"+b.name)
# print("20171207 a", elems)
# return E.div(*elems)
def apply_languages(self):
"""This function is called when a Site object gets instantiated,
i.e. while Django is still loading the settings. It analyzes
......@@ -3325,6 +3388,9 @@ Please convert to Plugin method".format(mod, methname)
yield 'lino.core.auth.middleware.RemoteUserMiddleware'
if self.use_ipdict:
yield 'lino.modlib.ipdict.middleware.Middleware'
if self.social_auth_backends:
yield 'social_django.middleware.SocialAuthExceptionMiddleware'
#~ yield 'lino.utils.editing.EditingMiddleware'
if True:
......@@ -3409,6 +3475,9 @@ Please convert to Plugin method".format(mod, methname)
if self.use_ipdict:
yield 'lino.modlib.ipdict'
if self.social_auth_backends:
yield 'social_django'
yield self.default_ui
......
......@@ -40,6 +40,12 @@ for p in site.installed_plugins:
else:
urlpatterns.append(url(prx, include(pat)))
if site.social_auth_backends:
urlpatterns.append(
url('^oauth/', include('social_django.urls', namespace='social')))
if site.django_admin_prefix: # not tested
from django.contrib import admin
admin.autodiscover()
......
......@@ -1427,7 +1427,16 @@ class HtmlBoxElement(DisplayElement):
# ~ if self.field.drop_zone: # testing with drop_zone 'FooBar'
# kw.update(listeners=dict(render=js_code('initialize%sDropZone' % self.field.drop_zone)))
kw.update(items=js_code("new Ext.BoxComponent({autoScroll:true})"))
html = self.field.default
if html is NOT_PROVIDED:
js = "new Ext.BoxComponent({autoScroll:true})"
else:
if callable(html):
html = html()
js = "new Ext.BoxComponent({autoScroll:true, html:%s})"
js = js % py2js(html)
kw.update(items=js_code(js))
if self.label:
kw.update(title=self.label)
return kw
......@@ -2228,6 +2237,8 @@ def create_layout_element(lh, name, **kw):
elems.append(create_field_element(lh, bf, **kw))
return elems
return create_field_element(lh, de, **kw)
if isinstance(de, fields.DisplayField):
return create_field_element(lh, de, **kw)
if isinstance(de, GenericForeignKey):
# create a horizontal panel with 2 comboboxes
......
......@@ -25,11 +25,9 @@ from __future__ import print_function
from lino.api import ad, _
# raise Exception("20160528")
class Plugin(ad.Plugin):
needs_plugins = ['lino.modlib.users']
needs_plugins = ['lino.modlib.users', 'social_django']
ui_label = _("Social Authentication")
......@@ -44,7 +42,9 @@ class Plugin(ad.Plugin):
yield (name, version, "https://github.com/python-social-auth")
def on_init(self):
self.needs_plugins.append('social_django')
raise Exception("No longer used. See Site.social_auth_backends.")
# self.needs_plugins.append()
ds = self.site.django_settings
if False:
ds['SOCIAL_AUTH_PIPELINE'] = (
......
......@@ -150,6 +150,21 @@ class SignOut(dd.Action):
_("User {} logged out.").format(user),
goto_url=ar.renderer.plugin.build_plain_url())
from lino.utils.xmlgen.html import E
# from lino.core.fields import DisplayField, DummyField
# class SocialAuthField(DisplayField):
# def value_from_object(self, obj, ar=None):
# elems = []
# elems.append(E.a("foo"))
# return E.p(elems)
# def social_auth_field():
# if settings.SITE.social_auth_backends:
# return SocialAuthField()
# return DummyField()
class SignIn(dd.Action):
label = _("Sign in")
......@@ -164,7 +179,6 @@ class SignIn(dd.Action):
password
""", label_align="left")
def run_from_ui(self, ar, **kw):
pv = ar.action_param_values
user = auth.authenticate(
......@@ -179,3 +193,25 @@ class SignIn(dd.Action):
close_window=True,
goto_url=ar.renderer.plugin.build_plain_url())
class SignInWithSocialAuth(SignIn):
# 20171207 nice as an example of a action dialog window with a
# HtmlBox, but we don't currently use it.
parameters = dict(
social_auth_links=dd.HtmlBox(
# _("Other authentications"),
default=E.div(*settings.SITE.get_social_auth_links())),
# social_auth_links=dd.Constant(
# settings.SITE.get_social_auth_links),
# social=social_auth_field(),
username=dd.CharField(_("Username")),
password=dd.PasswordField(_("Password"), blank=True)
)
# params_layout = dd.ActionParamsLayout("""
params_layout = dd.Panel("""
username
password
social_auth_links
""", label_align="left", window_size=(60,10))
......@@ -6,13 +6,13 @@
Documentation is in :doc:`/specs/users` and :doc:`/dev/users`
"""
from django.conf import settings
from lino.api import dd, rt, _
from lino.core import actions
from lino.core.roles import SiteAdmin
from .choicelists import UserTypes
from .actions import SendWelcomeMail, SignIn
from .actions import SendWelcomeMail, SignIn, SignInWithSocialAuth
class UserDetail(dd.DetailLayout):
......@@ -25,7 +25,7 @@ class UserDetail(dd.DetailLayout):
main = """
box1 #MembershipsByUser:20
remarks:40 AuthoritiesGiven:20
remarks:40 AuthoritiesGiven:20 SocialAuthsByUser:30
"""
main_m = """
......@@ -99,6 +99,10 @@ class UsersOverview(Users):
column_names = 'username user_type language'
exclude = dict(user_type='')
sign_in = SignIn()
# if settings.SITE.social_auth_backends is None:
# sign_in = SignIn()
# else:
# sign_in = SignInWithSocialAuth()
class MySettings(Users):
# use_as_default_table = False
......@@ -132,4 +136,22 @@ class AuthoritiesTaken(Authorities):
column_names = 'user'
auto_fit_column_widths = True
if settings.SITE.social_auth_backends:
try:
import social_django
except ImportError:
raise Exception(
"Sites with social_auth_backends must also install PSA "
"into their environment: "
"$ pip install social-auth-app-django")
class SocialAuthsByUser(dd.Table):
# required_roles = dd.login_required(SiteAdmin)
model = 'social_django.UserSocialAuth'
master_key = 'user'
else:
class SocialAuthsByUser(dd.Dummy):
pass
......@@ -31,6 +31,33 @@ from .actions import ChangePassword, SignOut
from lino.core.auth.utils import AnonymousUser
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, username, email, password, **extra_fields):
"""
Creates and saves a User with the given username, email and password.
"""
if not username:
raise ValueError('The given username must be set')
email = self.normalize_email(email)
username = self.model.normalize_username(username)
user = self.model(username=username, email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, username, email=None, password=None, **extra_fields):
extra_fields.setdefault('user_type', UserTypes.user)
return self._create_user(username, email, password, **extra_fields)
def create_superuser(self, username, email, password, **extra_fields):
extra_fields.setdefault('user_type', UserTypes.admin)
return self._create_user(username, email, password, **extra_fields)
@python_2_unicode_compatible
class User(AbstractBaseUser, Contactable, CreatedModified, TimezoneHolder):
class Meta(object):
......@@ -42,15 +69,16 @@ class User(AbstractBaseUser, Contactable, CreatedModified, TimezoneHolder):
USERNAME_FIELD = 'username'
_anon_user = None
objects = BaseUserManager()
objects = UserManager()
preferred_foreignkey_width = 15
hidden_columns = 'password remarks'
authenticated = True
# seems that Django doesn't like nullable username
# username = dd.NullCharField(_('Username'), max_length=30, unique=True)
username = models.CharField(_('Username'), max_length=30, unique=True)
# seems that Django doesn't like nullable username
user_type = UserTypes.field(blank=True)
initials = models.CharField(_('Initials'), max_length=10, blank=True)
first_name = models.CharField(_('First name'), max_length=30, blank=True)
......@@ -81,6 +109,14 @@ class User(AbstractBaseUser, Contactable, CreatedModified, TimezoneHolder):
# return join_words(self.last_name.upper(),self.first_name)
return str(self)
# @dd.displayfield(_("Other authentication providers"))
# def social_auth_links(self, ar=None):
# return settings.SITE.get_social_auth_links ()
# # elems = []
# # for backend in get_social_auth_backends()
# # elems.append(E.a("foo"))
# # return E.p(elems)
def get_person(self):
if self.partner:
return self.partner.get_mti_child('person')
......@@ -159,11 +195,13 @@ class User(AbstractBaseUser, Contactable, CreatedModified, TimezoneHolder):
action_param_values=pv,
renderer=settings.SITE.kernel.default_renderer)
btn = btn.ar2button(label=self.username)
return E.li(
btn, ' : ',
str(self), ', ',
str(self.user_type), ', ',
E.strong(settings.SITE.LANGUAGE_DICT.get(self.language)))
items = [ btn, ' : ',
str(self), ', ',
str(self.user_type)]
if self.language:
items += [', ',
E.strong(settings.SITE.LANGUAGE_DICT.get(self.language))]
return E.li(*items)
# if settings.SITE.is_demo_site:
# p = "'{0}', '{1}'".format(self.username, '1234')
# else:
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment