Merge pull request #249 from nephila/feature/per_sites_restrictions

Implement per site restriction for users
This commit is contained in:
Iacopo Spalletti 2016-05-11 19:48:13 +02:00
commit fb89f3b239
11 changed files with 326 additions and 6 deletions

View file

@ -15,6 +15,7 @@ History
* Removed meta-mixin compatibility code
* Changed slug size to 255 chars
* 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)
++++++++++++++++++

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
++++++++++++++++++++++

View file

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

View file

@ -75,6 +75,7 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
app_config_values = {
'default_published': 'publish'
}
_sites = None
def get_urls(self):
"""
@ -90,6 +91,7 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
def publish_post(self, request, pk):
"""
Admin view to publish a single post
:param request: request
:param pk: primary key of the post to publish
:return: Redirect to the post itself (if found) or fallback urls
@ -116,9 +118,47 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
field.max_length = 70
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):
"""
Customize the fieldsets according to the app settings
:param request: request
:param obj: post
:return: fieldsets configuration
@ -142,7 +182,7 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
fsets[0][1]['fields'].append('abstract')
if not get_setting('USE_PLACEHOLDER'):
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')
if request.user.is_superuser:
fsets[1][1]['fields'][0].append('author')
@ -158,6 +198,19 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
obj._set_default_author(request.user)
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:
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)
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)
self.assertTrue('author' in fsets[1][1]['fields'][0])
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.process_request(request)
post_admin = admin.site._registry[Post]
@ -177,6 +181,43 @@ class AdminTest(BaseTest):
self.assertContains(response, '<option value="%s">%s</option>' % (
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):
pages = self.get_pages()
@ -235,17 +276,26 @@ class AdminTest(BaseTest):
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)
post_admin._sites = None
fsets = post_admin.get_fieldsets(request)
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):
if request.user == self.user_normal:
fs[1][1]['fields'][0].append('author')
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):
fsets = post_admin.get_fieldsets(request)
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):
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 -*-
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 _