Implement per site restriction for users

This commit is contained in:
Iacopo Spalletti 2016-05-11 17:22:12 +02:00
parent 9c228df813
commit 01880591b7
No known key found for this signature in database
GPG key ID: BDCBC2EB289F60C6
11 changed files with 326 additions and 6 deletions

View file

@ -15,6 +15,7 @@ History
* Removed meta-mixin compatibility code * Removed meta-mixin compatibility code
* Changed slug size to 255 chars * Changed slug size to 255 chars
* Fixed pagination setting in list views * Fixed pagination setting in list views
* Added API to set default sites if user has permission only for a subset of sites
0.7.0 (2016-03-19) 0.7.0 (2016-03-19)
++++++++++++++++++ ++++++++++++++++++

View file

@ -340,6 +340,28 @@ To add the blog Sitemap, add the following code to the project ``urls.py``::
}), }),
) )
Multisite
+++++++++
django CMS blog provides full support for multisite setups.
Each blog post can be assigned to none, one or more sites: if no site is selected, then
it's visible on all sites.
This is matched with and API that allows to restrict users to only be able to edit
blog posts only for some sites.
To implement this API, you must add a ``get_sites`` method on the user model which
returns a queryset of sites the user is allowed to add posts to.
Example::
class CustomUser(AbstractUser):
sites = models.ManyToManyField('sites.Site')
def get_sites(self):
return self.sites
django CMS 3.2+ Wizard django CMS 3.2+ Wizard
++++++++++++++++++++++ ++++++++++++++++++++++

View file

@ -2,6 +2,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
import os
from tempfile import mkdtemp from tempfile import mkdtemp
@ -20,7 +22,6 @@ HELPER_SETTINGS = dict(
'taggit', 'taggit',
'taggit_autosuggest', 'taggit_autosuggest',
'aldryn_apphooks_config', 'aldryn_apphooks_config',
'tests.test_utils',
'aldryn_search', 'aldryn_search',
], ],
LANGUAGE_CODE='en', LANGUAGE_CODE='en',
@ -91,7 +92,7 @@ HELPER_SETTINGS = dict(
SITE_ID=1, SITE_ID=1,
HAYSTACK_CONNECTIONS={ HAYSTACK_CONNECTIONS={
'default': {} 'default': {}
} },
) )
try: try:
@ -124,9 +125,9 @@ try:
'ROUTING': 'knocker.routing.channel_routing', 'ROUTING': 'knocker.routing.channel_routing',
}, },
} }
except ImportError: except ImportError:
pass pass
os.environ['AUTH_USER_MODEL'] = 'tests.test_utils.CustomUser'
def run(): def run():

View file

@ -75,6 +75,7 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
app_config_values = { app_config_values = {
'default_published': 'publish' 'default_published': 'publish'
} }
_sites = None
def get_urls(self): def get_urls(self):
""" """
@ -90,6 +91,7 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
def publish_post(self, request, pk): def publish_post(self, request, pk):
""" """
Admin view to publish a single post Admin view to publish a single post
:param request: request :param request: request
:param pk: primary key of the post to publish :param pk: primary key of the post to publish
:return: Redirect to the post itself (if found) or fallback urls :return: Redirect to the post itself (if found) or fallback urls
@ -116,9 +118,47 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
field.max_length = 70 field.max_length = 70
return field return field
def has_restricted_sites(self, request):
"""
Whether the current user has permission on one site only
:param request: current request
:return: boolean: user has permission on only one site
"""
sites = self.get_restricted_sites(request)
return sites and sites.count() == 1
def get_restricted_sites(self, request):
"""
The sites on which the user has permission on.
To return the permissions, the method check for the ``get_sites``
method on the user instance (e.g.: ``return request.user.get_sites()``)
which must return the queryset of enabled sites.
If the attribute does not exists, the user is considered enabled
for all the websites.
:param request: current request
:return: boolean or a queryset of available sites
"""
if self._sites is None:
try:
self._sites = request.user.get_sites()
except AttributeError: # pragma: no cover
self._sites = False
return self._sites
def _set_config_defaults(self, request, form, obj=None):
form = super(PostAdmin, self)._set_config_defaults(request, form, obj)
sites = self.get_restricted_sites(request)
if 'sites' in form.base_fields and sites.exists():
form.base_fields['sites'].queryset = self.get_restricted_sites(request).all()
return form
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):
""" """
Customize the fieldsets according to the app settings Customize the fieldsets according to the app settings
:param request: request :param request: request
:param obj: post :param obj: post
:return: fieldsets configuration :return: fieldsets configuration
@ -142,7 +182,7 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
fsets[0][1]['fields'].append('abstract') fsets[0][1]['fields'].append('abstract')
if not get_setting('USE_PLACEHOLDER'): if not get_setting('USE_PLACEHOLDER'):
fsets[0][1]['fields'].append('post_text') fsets[0][1]['fields'].append('post_text')
if get_setting('MULTISITE'): if get_setting('MULTISITE') and not self.has_restricted_sites(request):
fsets[1][1]['fields'][0].append('sites') fsets[1][1]['fields'][0].append('sites')
if request.user.is_superuser: if request.user.is_superuser:
fsets[1][1]['fields'][0].append('author') fsets[1][1]['fields'][0].append('author')
@ -158,6 +198,19 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
obj._set_default_author(request.user) obj._set_default_author(request.user)
super(PostAdmin, self).save_model(request, obj, form, change) super(PostAdmin, self).save_model(request, obj, form, change)
def get_queryset(self, request):
qs = super(PostAdmin, self).get_queryset(request)
sites = self.get_restricted_sites(request)
if sites.exists():
qs = qs.filter(sites__in=sites.all())
return qs
def save_related(self, request, form, formsets, change):
super(PostAdmin, self).save_related(request, form, formsets, change)
obj = form.instance
sites = self.get_restricted_sites(request)
obj.sites = sites.all()
class Media: class Media:
css = { css = {
'all': ('%sdjangocms_blog/css/%s' % (settings.STATIC_URL, 'djangocms_blog_admin.css'),) 'all': ('%sdjangocms_blog/css/%s' % (settings.STATIC_URL, 'djangocms_blog_admin.css'),)

View file

@ -164,12 +164,16 @@ class AdminTest(BaseTest):
fsets = post_admin.get_fieldsets(request) fsets = post_admin.get_fieldsets(request)
self.assertFalse('sites' in fsets[1][1]['fields'][0]) self.assertFalse('sites' in fsets[1][1]['fields'][0])
request = self.get_page_request('/', self.user, r'/en/blog/?app_config=%s' % self.app_config_1.pk, edit=False) request = self.get_page_request(
'/', self.user, r'/en/blog/?app_config=%s' % self.app_config_1.pk, edit=False
)
fsets = post_admin.get_fieldsets(request) fsets = post_admin.get_fieldsets(request)
self.assertTrue('author' in fsets[1][1]['fields'][0]) self.assertTrue('author' in fsets[1][1]['fields'][0])
with self.login_user_context(self.user): with self.login_user_context(self.user):
request = self.get_request('/', 'en', user=self.user, path=r'/en/blog/?app_config=%s' % self.app_config_1.pk) request = self.get_request(
'/', 'en', user=self.user, path=r'/en/blog/?app_config=%s' % self.app_config_1.pk
)
msg_mid = MessageMiddleware() msg_mid = MessageMiddleware()
msg_mid.process_request(request) msg_mid.process_request(request)
post_admin = admin.site._registry[Post] post_admin = admin.site._registry[Post]
@ -177,6 +181,43 @@ class AdminTest(BaseTest):
self.assertContains(response, '<option value="%s">%s</option>' % ( self.assertContains(response, '<option value="%s">%s</option>' % (
self.category_1.pk, self.category_1.safe_translation_getter('name', language_code='en') self.category_1.pk, self.category_1.safe_translation_getter('name', language_code='en')
)) ))
self.assertContains(response, 'id="id_sites" name="sites"')
self.user.sites.add(self.site_1)
with self.login_user_context(self.user):
request = self.get_request('/', 'en', user=self.user,
path=r'/en/blog/?app_config=%s' % self.app_config_1.pk)
msg_mid = MessageMiddleware()
msg_mid.process_request(request)
post_admin = admin.site._registry[Post]
post_admin._sites = None
response = post_admin.add_view(request)
response.render()
self.assertNotContains(response, 'id="id_sites" name="sites"')
post_admin._sites = None
self.user.sites.clear()
def test_admin_queryset(self):
posts = self.get_posts()
posts[0].sites.add(self.site_1)
posts[1].sites.add(self.site_2)
request = self.get_request('/', 'en', user=self.user,
path=r'/en/blog/?app_config=%s' % self.app_config_1.pk)
post_admin = admin.site._registry[Post]
post_admin._sites = None
qs = post_admin.get_queryset(request)
self.assertEqual(qs.count(), 4)
self.user.sites.add(self.site_2)
request = self.get_request('/', 'en', user=self.user,
path=r'/en/blog/?app_config=%s' % self.app_config_1.pk)
post_admin = admin.site._registry[Post]
post_admin._sites = None
qs = post_admin.get_queryset(request)
self.assertEqual(qs.count(), 1)
self.assertEqual(qs[0], posts[1])
self.user.sites.clear()
def test_admin_auto_author(self): def test_admin_auto_author(self):
pages = self.get_pages() pages = self.get_pages()
@ -235,17 +276,26 @@ class AdminTest(BaseTest):
post_admin = admin.site._registry[Post] post_admin = admin.site._registry[Post]
request = self.get_page_request('/', self.user_normal, r'/en/blog/?app_config=%s' % self.app_config_1.pk) request = self.get_page_request('/', self.user_normal, r'/en/blog/?app_config=%s' % self.app_config_1.pk)
post_admin._sites = None
fsets = post_admin.get_fieldsets(request) fsets = post_admin.get_fieldsets(request)
self.assertFalse('author' in fsets[1][1]['fields'][0]) self.assertFalse('author' in fsets[1][1]['fields'][0])
self.assertTrue('sites' in fsets[1][1]['fields'][0])
post_admin._sites = None
def filter_function(fs, request, obj=None): def filter_function(fs, request, obj=None):
if request.user == self.user_normal: if request.user == self.user_normal:
fs[1][1]['fields'][0].append('author') fs[1][1]['fields'][0].append('author')
return fs return fs
self.user_normal.sites.add(self.site_1)
request = self.get_page_request('/', self.user_normal, r'/en/blog/?app_config=%s' % self.app_config_1.pk)
post_admin._sites = None
with self.settings(BLOG_ADMIN_POST_FIELDSET_FILTER=filter_function): with self.settings(BLOG_ADMIN_POST_FIELDSET_FILTER=filter_function):
fsets = post_admin.get_fieldsets(request) fsets = post_admin.get_fieldsets(request)
self.assertTrue('author' in fsets[1][1]['fields'][0]) self.assertTrue('author' in fsets[1][1]['fields'][0])
self.assertFalse('sites' in fsets[1][1]['fields'][0])
post_admin._sites = None
self.user_normal.sites.clear()
def test_admin_post_text(self): def test_admin_post_text(self):
pages = self.get_pages() pages = self.get_pages()

22
tests/test_utils/admin.py Normal file
View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import ugettext_lazy as _
from .models import CustomUser
class CustomUserAdmin(UserAdmin):
model = CustomUser
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
(_('Sites'), {'fields': ('sites',)})
)
admin.site.register(CustomUser, CustomUserAdmin)

View file

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.core.validators
import django.utils.timezone
import django.contrib.auth.models
class Migration(migrations.Migration):
dependencies = [
('auth', '0006_require_contenttypes_0002'),
('sites', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
('password', models.CharField(verbose_name='password', max_length=128)),
('last_login', models.DateTimeField(verbose_name='last login', null=True, blank=True)),
('is_superuser', models.BooleanField(verbose_name='superuser status', help_text='Designates that this user has all permissions without explicitly assigning them.', default=False)),
('username', models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], max_length=30, error_messages={'unique': 'A user with that username already exists.'})),
('first_name', models.CharField(verbose_name='first name', max_length=30, blank=True)),
('last_name', models.CharField(verbose_name='last name', max_length=30, blank=True)),
('email', models.EmailField(verbose_name='email address', max_length=254, blank=True)),
('is_staff', models.BooleanField(verbose_name='staff status', help_text='Designates whether the user can log into this admin site.', default=False)),
('is_active', models.BooleanField(verbose_name='active', help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', default=True)),
('date_joined', models.DateTimeField(verbose_name='date joined', default=django.utils.timezone.now)),
('groups', models.ManyToManyField(help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_query_name='user', related_name='user_set', verbose_name='groups', blank=True, to='auth.Group')),
('sites', models.ManyToManyField(to='sites.Site')),
('user_permissions', models.ManyToManyField(help_text='Specific permissions for this user.', related_query_name='user', related_name='user_set', verbose_name='user permissions', blank=True, to='auth.Permission')),
],
options={
'verbose_name': 'user',
'abstract': False,
'verbose_name_plural': 'users',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View file

View file

@ -1 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
sites = models.ManyToManyField('sites.Site')
def get_sites(self):
return self.sites

View file

@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'CustomUser'
db.create_table('test_utils_customuser', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('password', self.gf('django.db.models.fields.CharField')(max_length=128)),
('last_login', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
('is_superuser', self.gf('django.db.models.fields.BooleanField')(default=False)),
('username', self.gf('django.db.models.fields.CharField')(unique=True, max_length=30)),
('first_name', self.gf('django.db.models.fields.CharField')(blank=True, max_length=30)),
('last_name', self.gf('django.db.models.fields.CharField')(blank=True, max_length=30)),
('email', self.gf('django.db.models.fields.EmailField')(blank=True, max_length=75)),
('is_staff', self.gf('django.db.models.fields.BooleanField')(default=False)),
('is_active', self.gf('django.db.models.fields.BooleanField')(default=True)),
('date_joined', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
))
db.send_create_signal('test_utils', ['CustomUser'])
# Adding M2M table for field groups on 'CustomUser'
m2m_table_name = db.shorten_name('test_utils_customuser_groups')
db.create_table(m2m_table_name, (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('customuser', models.ForeignKey(orm['test_utils.customuser'], null=False)),
('group', models.ForeignKey(orm['auth.group'], null=False))
))
db.create_unique(m2m_table_name, ['customuser_id', 'group_id'])
# Adding M2M table for field user_permissions on 'CustomUser'
m2m_table_name = db.shorten_name('test_utils_customuser_user_permissions')
db.create_table(m2m_table_name, (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('customuser', models.ForeignKey(orm['test_utils.customuser'], null=False)),
('permission', models.ForeignKey(orm['auth.permission'], null=False))
))
db.create_unique(m2m_table_name, ['customuser_id', 'permission_id'])
# Adding M2M table for field sites on 'CustomUser'
m2m_table_name = db.shorten_name('test_utils_customuser_sites')
db.create_table(m2m_table_name, (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('customuser', models.ForeignKey(orm['test_utils.customuser'], null=False)),
('site', models.ForeignKey(orm['sites.site'], null=False))
))
db.create_unique(m2m_table_name, ['customuser_id', 'site_id'])
def backwards(self, orm):
# Deleting model 'CustomUser'
db.delete_table('test_utils_customuser')
# Removing M2M table for field groups on 'CustomUser'
db.delete_table(db.shorten_name('test_utils_customuser_groups'))
# Removing M2M table for field user_permissions on 'CustomUser'
db.delete_table(db.shorten_name('test_utils_customuser_user_permissions'))
# Removing M2M table for field sites on 'CustomUser'
db.delete_table(db.shorten_name('test_utils_customuser_sites'))
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'blank': 'True', 'to': "orm['auth.Permission']"})
},
'auth.permission': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission', 'ordering': "('content_type__app_label', 'content_type__model', 'codename')"},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'contenttypes.contenttype': {
'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'", 'ordering': "('name',)"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'sites.site': {
'Meta': {'object_name': 'Site', 'db_table': "'django_site'", 'ordering': "('domain',)"},
'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'test_utils.customuser': {
'Meta': {'object_name': 'CustomUser'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'max_length': '75'}),
'first_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '30'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'blank': 'True', 'to': "orm['auth.Group']", 'related_name': "'user_set'"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '30'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'sites': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['sites.Site']"}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'blank': 'True', 'to': "orm['auth.Permission']", 'related_name': "'user_set'"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
}
}
complete_apps = ['test_utils']

View file

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy as _