Commit d243decf authored by Luc Saffre's avatar Luc Saffre
Browse files
parent 19bea378
......@@ -562,7 +562,6 @@ class Action(Parametrizable, Permittable):
"""
# if not actor.editable and not self.readonly:
# return False
if self.defining_actor is not None:
# already defined in another actor
return True
......@@ -625,7 +624,10 @@ class Action(Parametrizable, Permittable):
on an action it is a instance method.
Note that this method is not called for actions which are rendered
in a toolbar (:srcref:`docs/tickets/105`)
in a toolbar (:ticket:`1336`).
Usage examples:
:class:`lino.modlib.users.actions.SendWelcomeMail`
"""
......
......@@ -267,7 +267,6 @@ class Actor(with_metaclass(ActorMetaClass, type('NewBase', (actions.Parametrizab
the user has permission to view the actor or not.
"""
required_roles = set([SiteUser])
"""See :attr:`lino.core.permissions.Permittable.required_roles`"""
......
# Copyright 2009-2016 Luc Saffre
# Copyright 2009-2017 Luc Saffre
# License: BSD (see file COPYING for details)
"""
......@@ -218,22 +218,25 @@ def discover():
if issubclass(rpt, frames.Frame) and rpt is not frames.Frame:
register_frame(rpt)
logger.debug("Instantiate model tables...")
for model in get_models():
# Not getattr but __dict__.get because of the mixins.Listings
# trick.
rpt = model.__dict__.get('_lino_default_table', None)
# rpt = getattr(model,'_lino_default_table',None)
# logger.debug('20111113 %s._lino_default_table = %s',model,rpt)
if rpt is None:
rpt = table_factory(model)
if rpt is None:
raise Exception("table_factory() failed for %r." % model)
register_report(rpt)
rpt.class_init()
# rpt.collect_actions()
model._lino_default_table = rpt
# logger.debug("Create default tables...")
# for model in get_models():
# # Note that automatic models (created by ManyToManyField with
# # a `through`) do not yet exist here.
# # Not getattr but __dict__.get because of the mixins.Listings
# # trick:
# rpt = model.__dict__.get('_lino_default_table', None)
# # rpt = getattr(model,'_lino_default_table',None)
# # logger.debug('20111113 %s._lino_default_table = %s',model,rpt)
# if rpt is None:
# rpt = table_factory(model)
# if rpt is None:
# raise Exception("table_factory() failed for %r." % model)
# # print ("20170104 No table for {}, created default table.".format(model))
# register_report(rpt)
# rpt.class_init()
# # rpt.collect_actions()
# model._lino_default_table = rpt
logger.debug("Analyze %d slave tables...", len(slave_reports))
for rpt in slave_reports:
......
......@@ -422,6 +422,28 @@ class Kernel(object):
a.class_init()
dbtables.discover()
# Create default tables. Every model for which there is no
# table at all, will now get an automatically created default
# table. This includes automatic models created by
# ManyToManyField.
for model in models_list:
# Not getattr but __dict__.get because of the mixins.Listings
# trick:
rpt = model.__dict__.get('_lino_default_table', None)
# rpt = getattr(model,'_lino_default_table',None)
# logger.debug('20111113 %s._lino_default_table = %s',model,rpt)
if rpt is None:
rpt = dbtables.table_factory(model)
if rpt is None:
raise Exception("table_factory() failed for %r." % model)
# print ("20170104 No table for {}, created default table.".format(model))
dbtables.register_report(rpt)
rpt.class_init()
# rpt.collect_actions()
model._lino_default_table = rpt
#~ choosers.discover()
actions.discover_choosers()
......@@ -443,15 +465,27 @@ class Kernel(object):
site.on_each_app('site_setup') # deprecated
# Actor.after_site_setup() is called after the apps'
# Actor.after_site_setup() is called after the plugins's
# site_setup(). Example: pcsw.site_setup() adds a detail to
# properties.Properties, the base class for
# properties.PropsByGroup. The latter would not install a
# `detail_action` during her after_site_setup() and also would
# never get it later.
# In a first loop we run it on actors who are being used as
# default tables for a model. Because the defining_actor of
# model actions will be the first actor to which they get
# attached.
later = []
for a in actors.actors_list:
if a.model and a == a.model.get_default_table():
a.after_site_setup(site)
else:
later.append(a)
for a in later:
a.after_site_setup(site)
#~ site.on_site_startup()
......
......@@ -620,7 +620,10 @@ class ParamsLayout(BaseLayout):
params_store = None
def get_data_elem(self, name):
return self._datasource.get_param_elem(name)
de = self._datasource.get_param_elem(name)
if de is None:
de = self._datasource.get_data_elem(name)
return de
def setup_handle(self, lh):
# if str(self._datasource) == 'courses.Pupils':
......
......@@ -204,9 +204,9 @@ class Model(models.Model):
workflow_state_field = None
"""If this is set on a Model, then it will be used as default value
for
:attr:`workflow_state_field<lino.core.table.Table.workflow_state_field>`
of all tables based on this Model.
for :attr:`workflow_state_field
<lino.core.table.Table.workflow_state_field>` of all tables based
on this Model.
"""
......@@ -726,6 +726,7 @@ class Model(models.Model):
l = []
if ar is not None:
actor = ar.actor
# print(20170102, actor)
state = actor.get_row_state(obj)
sep = ''
show = True # whether to show the state
......@@ -745,6 +746,7 @@ class Model(models.Model):
#~ sep = u" \u25b8 "
for ba in actor.get_actions():
assert ba.actor == actor # 20170102
if ba.action.show_in_workflow:
if actor.get_row_permission(obj, ar, state, ba):
if show and isinstance(ba.action, ChangeStateAction):
......
......@@ -173,18 +173,19 @@ def make_view_permission_handler_(
# if action.action_name == "export_excel":
# print 20150828, profile.role, required_roles
return profile.has_required_roles(required_roles)
else:
def allow(action, profile):
return True
if not readonly:
allow3 = allow
if not readonly:
allow3 = allow
def allow(action, profile):
if not allow3(action, profile):
return False
if profile.readonly:
return False
return True
else:
def allow(action, profile):
if not allow3(action, profile):
return False
if profile.readonly:
return False
return True
if debug_permissions: # False:
......
......@@ -522,8 +522,10 @@ class Site(object):
"""
user_types_module = None
"""The full Python path of the **user profiles module** to be used on
this site.
"""The name of the **user profiles module** to be used on this site.
Default value is `None`, meaning that permission control is
inactive: everything is permitted.
This must be set if you want to enable permission control based on
user roles defined in :attr:`Permittable.required_roles
......@@ -531,12 +533,9 @@ class Site(object):
:attr:`UserType.role
<lino.modlib.users.choicelists.UserType.role>`.
Default value is `None`, meaning that role-based permission
control is inactive: every user can see everything.
If set, Lino will import this module during site startup. It is
expected to define application-specific user roles (if necessary)
and to fill the :class:`UserTypes
If set, Lino will import the named module during site startup. It
is expected to define application-specific user roles (if
necessary) and to fill the :class:`UserTypes
<lino.modlib.users.choicelists.UserTypes>` choicelist.
For example::
......@@ -546,7 +545,7 @@ class Site(object):
Examples of such user profiles modules are
:mod:`lino.modlib.users.roles` and
:mod:`lino.projects.presto.roles`.
:mod:`lino_noi.projects.noi.roles`.
"""
......@@ -760,11 +759,11 @@ class Site(object):
"""
anonymous_user_type = '000'
"""The user profile to be assigned to the anonymous user
(:class:`AnonymousUser <lino.modlib.users.utils.AnonymousUser>`).
# anonymous_user_type = '000'
# """The user profile to be assigned to the anonymous user
# (:class:`AnonymousUser <lino.modlib.users.utils.AnonymousUser>`).
"""
# """
remote_user_header = None
"""The name of the header (set by the web server) that Lino should
......
......@@ -137,7 +137,7 @@
{{ ln }}
{%- endfor -%}
{# anonymous request using permalink: forward request.path as "on_login" URL #}
{%- if settings.SITE.user_model and not request.user.authenticated and on_ready -%}
{%- if settings.SITE.user_model and on_ready and not request.user.authenticated and not settings.SITE.plugins.users.online_registration -%}
{%- set on_ready = "Lino.show_login_window(" + py2js(request.path) + ")" -%}
{%- endif -%}
{# Render main window #}
......
......@@ -895,10 +895,11 @@ class ExtRenderer(HtmlRenderer):
def lino_js_parts(self):
profile = jsgen.get_user_profile()
# return ('genjs',
return ('cache', 'js',
'lino_' + profile.value + '_'
+ translation.get_language() + '.js')
filename = 'lino_'
if profile is not None:
filename += profile.value + '_'
filename += translation.get_language() + '.js'
return ('cache', 'js', filename)
def linolib_template(self):
env = jinja2.Environment(loader=jinja2.FileSystemLoader(
......@@ -1106,6 +1107,8 @@ class ExtRenderer(HtmlRenderer):
yield " action_name: '%s'," % tbl.action_name
yield " ls_url: %s," % py2js(dh.layout._url)
yield " window_title: %s," % py2js(tbl.label)
# if not tbl.select_rows:
# yield " default_record_id: -99999,"
yield " before_row_edit : function(record) {"
for ln in self.before_row_edit(dh.main):
......
......@@ -2500,8 +2500,10 @@ Lino.ActionFormPanel = Ext.extend(Lino.ActionFormPanel, {
pk = panel.get_current_record().id;
}
if (pk == undefined) {
Lino.alert("Sorry, dialog action without base_params.mk");
return;
// 20170101 VerifyUser action
pk = '-99998';
// Lino.alert("Sorry, dialog action without base_params.mk");
// return;
}
var self = this;
// function on_success() { self.get_containing_window().close(); };
......
......@@ -58,10 +58,10 @@
<a href="#"
onclick="javascript:Lino.show_login_window()">{{_("log in")}}</a>
{{_("using the <b>Log in</b> button in the upper right corner.")}}
{% if not request.user.profile.readonly %}
{% if site.plugins.users.online_registration and not request.user.profile.readonly %}
{{_("Or {} as a new user.").format(
E.tostring(ar.window_action_button(
site.actors.users.RegisterUsers.insert_action,
site.actors.users.Register.insert_action,
label=_("register"), icon_name=None)))}}
{% endif %}
</p>
......
......@@ -390,6 +390,7 @@ uploaded file in a new browser window."""),
'lino.modlib.uploads.models.MyUploads.model' : _("""alias of Upload"""),
'lino.modlib.uploads.models.UploadsByController.model' : _("""alias of Upload"""),
'lino.modlib.users.Plugin' : _("""See /dev/plugins."""),
'lino.modlib.users.Plugin.online_registration' : _("""Whether this site offers online registration of new users."""),
'lino.modlib.users.actions.SendWelcomeMail' : _("""Send a welcome mail to this user."""),
'lino.modlib.users.actions.ChangePassword' : _("""Change the password of this user."""),
'lino.modlib.users.actions.ChangePassword.current' : _("""The current password. Leave empty if the user has no password
......@@ -397,13 +398,13 @@ yet. And SiteAdmin users don't need to specify this at all."""),
'lino.modlib.users.actions.ChangePassword.new1' : _("""The new password."""),
'lino.modlib.users.actions.ChangePassword.new2' : _("""The new password a second time. Both passwords must match."""),
'lino.modlib.users.choicelists.UserType' : _("""Base class for all user profiles."""),
'lino.modlib.users.choicelists.UserType.hidden_languages' : _("""A subset of languages which
should be hidden in this user profile. Default value is
hidden_languages. This is
used on multilingual sites with more than 4 or 5 languages."""),
'lino.modlib.users.choicelists.UserType.role' : _("""The role of users having this profile. This is an instance of
'lino.modlib.users.choicelists.UserType.role' : _("""The role of users having this type. This is an instance of
<lino.core.roles.UserRole> or some subclass thereof."""),
'lino.modlib.users.choicelists.UserType.readonly' : _("""Whether users with this profile get only write-proteced access."""),
'lino.modlib.users.choicelists.UserType.readonly' : _("""Whether users of this type get only write-proteced access."""),
'lino.modlib.users.choicelists.UserType.hidden_languages' : _("""A subset of languages
which should be hidden for users of this type. Default value
is hidden_languages. This
is used on multilingual sites with more than 4 or 5 languages."""),
'lino.modlib.users.choicelists.UserTypes' : _("""The list of user profiles available on this site."""),
'lino.modlib.users.choicelists.UserTypes.item_class' : _("""alias of UserType"""),
'lino.modlib.users.choicelists.UserTypes.hidden_languages' : _("""Default value for the
......@@ -450,6 +451,7 @@ Leaving this empty means that the user cannot log in."""),
this user."""),
'lino.modlib.users.models.User.person' : _("""A virtual read-only field which returns the Person MTI child corresponding
to the partner (if it exists) and otherwise None."""),
'lino.modlib.users.models.User.last_login' : _("""Not used in Lino."""),
'lino.modlib.users.models.User.authenticated' : _("""This is always True.
See also lino.modlib.users.utils.AnonymousUser.authenticated."""),
'lino.modlib.users.models.Authority' : _("""An Authority is when a user gives another user the right to
......
......@@ -29,11 +29,20 @@ from lino.api import ad, _
class Plugin(ad.Plugin):
"See :doc:`/dev/plugins`."
"""See :doc:`/dev/plugins`.
.. attribute:: online_registration
Whether this site offers :ref:`online registration
<online_registration>` of new users.
"""
verbose_name = _("Users")
needs_plugins = ['lino.modlib.system']
online_registration = False
def on_init(self):
super(Plugin, self).on_init()
self.site.set_user_model('users.User')
......
......@@ -10,39 +10,82 @@ from builtins import object
from django.utils.encoding import python_2_unicode_compatible
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from lino.api import dd, rt
from lino.api import dd, rt, _
from lino.core.roles import SiteAdmin
class SendWelcomeMail(dd.Action):
"""Send a welcome mail to this user."""
label = _("Welcome mail")
show_in_bbar = True
show_in_workflow = False
if False: # #1336
show_in_bbar = True
show_in_workflow = False
else:
show_in_bbar = False
show_in_workflow = True
button_text = u"\u2709" # ✉
required_roles = dd.required(SiteAdmin)
parameters = dict(
email=models.EmailField(_('e-mail address')),
verification_code=models.CharField(
_("Verification code"), max_length=50))
subject=models.CharField(_('Subject'), max_length=250),
#body_text=dd.RichTextField(_('Body text'), format='html'),
)
# params_layout = dd.Panel("""email
# subject
# body_text""", window_size=(60, 15))
# params_layout = dd.Panel("""
# email
# subject
# welcome_email_body""", window_size=(60, 15))
params_layout = """
email
subject
"""
def action_param_defaults(self, ar, obj, **kw):
kw = super(SendWelcomeMail, self).action_param_defaults(
ar, obj, **kw)
if obj is not None:
kw.update(email=obj.email)
kw.update(subject=_("Welcome on {site}").format(
site=settings.SITE.title or settings.SITE.verbose_name))
# template = rt.get_template('users/welcome_email.eml')
# context = dict(obj=obj, E=E, rt=rt)
# body = template.render(**context)
# body = E.fromstring(body)
# kw.update(body_text=body)
return kw
def get_action_permission(self, ar, obj, state):
if obj == ar.get_user():
user = ar.get_user()
if not obj.email:
return False
if not user.profile.has_required_roles([SiteAdmin]):
return False
return super(
SendWelcomeMail, self).get_action_permission(ar, obj, state)
def run_from_ui(self, ar, **kw):
sender = settings.SERVER_EMAIL
done_for = []
for obj in ar.selected_rows:
obj.send_welcome_email()
done_for.append(str(obj))
assert len(ar.selected_rows) == 1
obj = ar.selected_rows[0]
subject = ar.action_param_values.subject
email = ar.action_param_values.email
# body = ar.action_param_values.body_text
# body = "<body>" + body + "</body>"
email = "{} <{}>".format(obj, email)
body = obj.get_welcome_email_body(ar)
print(20170102, obj, email, body)
# send_welcome_email(obj)
rt.send_email(subject, sender, body, [email])
done_for.append(email)
msg = _("Welcome mail has been sent to {}.").format(
', '.join(done_for))
......
......@@ -11,7 +11,7 @@ from builtins import str
from django.conf import settings
from lino.core.choicelists import ChoiceList, Choice
from lino.core.roles import UserRole, SiteUser, SiteAdmin
from lino.core.roles import SiteAdmin
from lino.api import dd, _
......@@ -19,28 +19,31 @@ from lino.api import dd, _
class UserType(Choice):
"""Base class for all user profiles.
"""
.. attribute:: role
hidden_languages = None
"""A subset of :attr:`languages<lino.core.site.Site.languages>` which
should be hidden in this user profile. Default value is
:attr:`hidden_languages<UserTypes.hidden_languages>`. This is
used on multilingual sites with more than 4 or 5 languages.
The role of users having this type. This is an instance of
:class:`<lino.core.roles.UserRole>` or some subclass thereof.
.. attribute:: readonly
Whether users of this type get only write-proteced access.
.. attribute:: hidden_languages
A subset of :attr:`languages<lino.core.site.Site.languages>`
which should be hidden for users of this type. Default value
is :attr:`hidden_languages<UserTypes.hidden_languages>`. This
is used on multilingual sites with more than 4 or 5 languages.
"""
role = None
hidden_languages = None
readonly = False
"""Whether users with this profile get only write-proteced access."""
# authenticated = True
# """Whether users with this profile should be considered authenticated."""
role = None
"""The role of users having this profile. This is an instance of
:class:`<lino.core.roles.UserRole>` or some subclass thereof.
"""
def __init__(self, value, text, role_class,
name=None, # authenticated=True,
readonly=False,
......@@ -51,7 +54,6 @@ class UserType(Choice):
super(UserType, self).__init__(value, text, name)
self.role = role_class()
self.readonly = readonly
# self.authenticated = authenticated
self.kw = kw
def attach(self, cls):
......@@ -104,6 +106,14 @@ class UserTypes(ChoiceList):
You can see the user profiles available in your application via
:menuselection:`Explorer --> System --> User Profiles`.
Every site should define at least three named user types:
.. attribute:: anonymous
.. attribute:: user
.. attribute:: admin
"""
required_roles = dd.login_required(SiteAdmin)
item_class = UserType
......@@ -124,11 +134,10 @@ class UserTypes(ChoiceList):
"""
add = UserTypes.add_item
add('000', _("Anonymous"), UserRole, name='anonymous', readonly=True)
add('100', _("User"), SiteUser, name='user')
add('900', _("Administrator"), SiteAdmin, name='admin')
# add = UserTypes.add_item
# add('000', _("Anonymous"), UserRole, 'anonymous', readonly=True)
# add('100', _("User"), SiteUser, 'user')
# add('900', _("Administrator"), SiteAdmin, 'admin')
......@@ -2,7 +2,9 @@
<p>Dear {{obj.get_full_name()}},</p>
<p>Welcome to the
<a href="{{rt.settings.SITE.server_url}}">{{rt.settings.SITE.server_url}}</a>
<a href="{{rt.settings.SITE.server_url}}">{{rt.settings.SITE.server_url}}</a> site.
</p>
</body>
Please click {{E.tostring(ar.instance_action_button(obj.verify))}} and enter your verificataion code: <b>{{obj.verification_code}}</b>.
</body>
\ No newline at end of file
......@@ -10,7 +10,7 @@ from lino.core import actions
from lino.core.roles import SiteAdmin
from .choicelists import UserTypes
from .actions import SendWelcomeMail
class UserDetail(dd.DetailLayout):
......@@ -71,9 +71,11 @@ class Users(dd.Table):
#~ return False
class AllUsers(Users):
"""Shows the list of all users on this site."""
required_roles = dd.required(SiteAdmin)
send_welcome_email = SendWelcomeMail()
class UsersOverview(Users):
......
......@@ -11,11 +11,10 @@ from builtins import object
from django.utils.encoding import python_2_unicode_compatible
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django.contrib.auth.base_user import AbstractBaseUser
from lino.api import dd, rt
from lino.api import dd, rt, _
from lino.utils.xmlgen.html import E
from lino.core import userprefs
from lino.core.fields import NullCharField
......@@ -111,6 +110,8 @@ class User(AbstractBaseUser, CreatedModified, TimezoneHolder):