Merge pull request #249 from nephila/feature/per_sites_restrictions
Implement per site restriction for users
This commit is contained in:
commit
fb89f3b239
11 changed files with 326 additions and 6 deletions
|
@ -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)
|
||||
++++++++++++++++++
|
||||
|
|
22
README.rst
22
README.rst
|
@ -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
|
||||
++++++++++++++++++++++
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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'),)
|
||||
|
|
|
@ -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
22
tests/test_utils/admin.py
Normal 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)
|
45
tests/test_utils/migrations/0001_initial.py
Normal file
45
tests/test_utils/migrations/0001_initial.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
0
tests/test_utils/migrations/__init__.py
Normal file
0
tests/test_utils/migrations/__init__.py
Normal 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
|
||||
|
|
115
tests/test_utils/south_migrations/0001_initial.py
Normal file
115
tests/test_utils/south_migrations/0001_initial.py
Normal 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']
|
2
tests/test_utils/south_migrations/__init__.py
Normal file
2
tests/test_utils/south_migrations/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.utils.translation import ugettext_lazy as _
|
Loading…
Reference in a new issue