Commit 205c657e authored by Sofia Papagiannaki's avatar Sofia Papagiannaki
Browse files

Merge branch 'aquarium', remote-tracking branch 'origin'

Conflicts:
	snf-astakos-app/astakos/im/settings.py
parents df4c7b3c 4d65a5a1
# Copyright 2012 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import pika
import json
import uuid
from urlparse import urlparse
from time import time
def exchange_connect(exchange, vhost='/'):
"""Format exchange as a URI: rabbitmq://user:pass@host:port/exchange"""
parts = urlparse(exchange)
if parts.scheme != 'rabbitmq':
return None
if len(parts.path) < 2 or not parts.path.startswith('/'):
return None
exchange = parts.path[1:]
connection = pika.BlockingConnection(pika.ConnectionParameters(
host=parts.hostname, port=parts.port, virtual_host=vhost,
credentials=pika.PlainCredentials(parts.username, parts.password)))
channel = connection.channel()
channel.exchange_declare(exchange=exchange, type='topic', durable=True)
return (connection, channel, exchange)
def exchange_close(conn):
connection, channel, exchange = conn
connection.close()
def exchange_send(conn, key, value):
"""Messages are sent to exchanges at a key."""
connection, channel, exchange = conn
channel.basic_publish(exchange=exchange,
routing_key=key,
body=json.dumps(value))
def exchange_route(conn, key, queue):
"""Set up routing of keys to queue."""
connection, channel, exchange = conn
channel.queue_declare(queue=queue, durable=True,
exclusive=False, auto_delete=False)
channel.queue_bind(exchange=exchange,
queue=queue,
routing_key=key)
def queue_callback(conn, queue, cb):
def handle_delivery(channel, method_frame, header_frame, body):
#print 'Basic.Deliver %s delivery-tag %i: %s' % (header_frame.content_type,
# method_frame.delivery_tag,
# body)
if cb:
cb(json.loads(body))
channel.basic_ack(delivery_tag=method_frame.delivery_tag)
connection, channel, exchange = conn
channel.basic_consume(handle_delivery, queue=queue)
def queue_start(conn):
connection, channel, exchange = conn
channel.start_consuming()
class Receipt(object):
def __init__(self, client, user, resource, value, details=None):
self.eventVersion = 1
self.id = str(uuid.uuid4())
self.timestamp = int(time() * 1000)
self.clientId = client
self.userId = user
self.resource = resource
self.value = value
if details:
self.details = details
def format(self):
return self.__dict__
# Copyright 2012 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from queue import Queue
__all__ = ["Queue"]
# Copyright 2012 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from astakos.im.queue import exchange_connect, exchange_send, Receipt
class Queue(object):
"""Queue.
Required constructor parameters: exchange, message_key, client_id.
"""
def __init__(self, **params):
exchange = params['exchange']
self.conn = exchange_connect(exchange)
self.message_key = params['message_key']
self.client_id = params['client_id']
def send(self, user, resource, value, details):
body = Receipt(self.client_id, user, resource, value, details).format()
exchange_send(self.conn, self.message_key, body)
......@@ -158,6 +158,8 @@ uniq User email (uniq identifier used by Astakos)
auth_token Authentication token
auth_token_expires Token expiration date
auth_token_created Token creation date
has_credits Whether user has credits
has_signed_terms Whether user has aggred on terms
=========================== ============================
Example reply:
......@@ -168,7 +170,9 @@ Example reply:
"uniq": "papagian@example.com"
"auth_token": "0000",
"auth_token_expires": "Tue, 11-Sep-2012 09:17:14 ",
"auth_token_created": "Sun, 11-Sep-2011 09:17:14 "}
"auth_token_created": "Sun, 11-Sep-2011 09:17:14 ",
"has_credits": false,
"has_signed_terms": true}
|
......@@ -177,7 +181,7 @@ Return Code Description
=========================== =====================
204 (No Content) The request succeeded
400 (Bad Request) The request is invalid
401 (Unauthorized) Missing token or inactive user
401 (Unauthorized) Missing token or inactive user or penging approval terms
500 (Internal Server Error) The request cannot be completed because of an internal error
=========================== =====================
......
......@@ -64,12 +64,16 @@ ASTAKOS_SITENAME GRNET Cloud
ASTAKOS_CLOUD_SERVICES ({'icon': 'home-icon.png', 'id': 'cloud', 'name': 'grnet cloud', 'url': '/'}, Cloud services appear in the horizontal bar
{'id': 'okeanos', 'name': 'cyclades', 'url': '/okeanos.html'},
{'id': 'pithos', 'name': 'pithos+', 'url': '/ui/'})
ASTAKOS_RECAPTCHA_ENABLED True Enable recaptcha
ASTAKOS_RECAPTCHA_PUBLIC_KEY Recaptcha public key obtained after registration here: http://recaptcha.net
ASTAKOS_RECAPTCHA_PRIVATE_KEY Recaptcha private key obtained after registration here: http://recaptcha.net
ASTAKOS_RECAPTCHA_OPTIONS {'theme': 'white'} Options for customizing reCAPTCHA look and feel
(see: http://code.google.com/intl/el-GR/apis/recaptcha/docs/customization.html)
ASTAKOS_LOGOUT_NEXT Where the user should be redirected after logout
(if not set and no next parameter is defined it renders login page with message)
ASTAKOS_BILLING_FIELDS ['id', 'is_active', 'provider', 'third_party_identifier'] AstakosUser fields to propagate in the billing system
ASTAKOS_QUEUE_CONNECTION The queue connection ex. 'rabbitmq://guest:guest@localhost:5672/astakos'
(if it is not set, it does not send messages)
ASTAKOS_RE_USER_EMAIL_PATTERNS [] Email patterns that are automatically activated ex. ['^[a-zA-Z0-9\._-]+@grnet\.gr$']
============================== ============================================================================= ===========================================================================================
......@@ -89,4 +93,5 @@ listusers List users
modifyuser Modify a user's attributes
showinvitation Show invitation info
showuser Show user info
addterms Add new approval terms
=============== ===========================
......@@ -46,6 +46,7 @@ from django.core.urlresolvers import reverse
from astakos.im.faults import BadRequest, Unauthorized, InternalServerError
from astakos.im.models import AstakosUser
from astakos.im.settings import CLOUD_SERVICES, INVITATIONS_ENABLED
from astakos.im.util import has_signed_terms
logger = logging.getLogger(__name__)
......@@ -85,14 +86,19 @@ def authenticate(request):
# Check if the token has expired.
if (time() - mktime(user.auth_token_expires.timetuple())) > 0:
return render_fault(request, Unauthorized('Authentication expired'))
if not has_signed_terms(user):
return render_fault(request, Unauthorized('Pending approval terms'))
response = HttpResponse()
response.status=204
user_info = {'username':user.username,
'uniq':user.email,
'auth_token':user.auth_token,
'auth_token_created':user.auth_token_created.isoformat(),
'auth_token_expires':user.auth_token_expires.isoformat()}
'auth_token_expires':user.auth_token_expires.isoformat(),
'has_credits':user.has_credits,
'has_signed_terms':has_signed_terms(user)}
response.content = json.dumps(user_info)
response['Content-Type'] = 'application/json; charset=UTF-8'
response['Content-Length'] = len(response.content)
......@@ -149,7 +155,7 @@ def get_menu(request, with_extra_links=False, with_signout=True):
if with_signout:
l.append({ 'url': absolute(reverse('astakos.im.views.logout')),
'name': "Sign out"})
callback = request.GET.get('callback', None)
data = json.dumps(tuple(l))
mimetype = 'application/json'
......
......@@ -43,7 +43,8 @@ def im_modules(request):
return {'im_modules': IM_MODULES}
def next(request):
return {'next' : request.GET.get('next', '')}
query_dict = request.__getattribute__(request.method)
return {'next' : query_dict.get('next', '')}
def code(request):
return {'code' : request.GET.get('code', '')}
......
......@@ -31,6 +31,7 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from urlparse import urljoin
from datetime import datetime
from django import forms
from django.utils.translation import ugettext as _
......@@ -40,10 +41,14 @@ from django.contrib.auth.tokens import default_token_generator
from django.template import Context, loader
from django.utils.http import int_to_base36
from django.core.urlresolvers import reverse
from django.utils.functional import lazy
from astakos.im.models import AstakosUser
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL
from astakos.im.widgets import DummyWidget, RecaptchaWidget
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, RECAPTCHA_ENABLED
from astakos.im.widgets import DummyWidget, RecaptchaWidget, ApprovalTermsWidget
# since Django 1.4 use django.core.urlresolvers.reverse_lazy instead
from astakos.im.util import reverse_lazy, get_latest_terms
import logging
import recaptcha.client.captcha as captcha
......@@ -63,7 +68,8 @@ class LocalUserCreationForm(UserCreationForm):
class Meta:
model = AstakosUser
fields = ("email", "first_name", "last_name")
fields = ("email", "first_name", "last_name", "has_signed_terms")
widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
def __init__(self, *args, **kwargs):
"""
......@@ -74,9 +80,12 @@ class LocalUserCreationForm(UserCreationForm):
kwargs.pop('ip')
super(LocalUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email', 'first_name', 'last_name',
'password1', 'password2',
'recaptcha_challenge_field',
'recaptcha_response_field']
'password1', 'password2']
if get_latest_terms():
self.fields.keyOrder.append('has_signed_terms')
if RECAPTCHA_ENABLED:
self.fields.keyOrder.extend(['recaptcha_challenge_field',
'recaptcha_response_field',])
def clean_email(self):
email = self.cleaned_data['email']
......@@ -88,6 +97,12 @@ class LocalUserCreationForm(UserCreationForm):
except AstakosUser.DoesNotExist:
return email
def clean_has_signed_terms(self):
has_signed_terms = self.cleaned_data['has_signed_terms']
if not has_signed_terms:
raise forms.ValidationError(_('You have to agree with the terms'))
return has_signed_terms
def clean_recaptcha_response_field(self):
if 'recaptcha_challenge_field' in self.cleaned_data:
self.validate_captcha()
......@@ -112,6 +127,7 @@ class LocalUserCreationForm(UserCreationForm):
"""
user = super(LocalUserCreationForm, self).save(commit=False)
user.renew_token()
user.date_signed_terms = datetime.now()
if commit:
user.save()
logger.info('Created user %s', user)
......@@ -126,17 +142,15 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm):
class Meta:
model = AstakosUser
fields = ("email", "first_name", "last_name")
fields = ("email", "first_name", "last_name", "has_signed_terms")
widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
def __init__(self, *args, **kwargs):
"""
Changes the order of fields, and removes the username field.
"""
super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email', 'inviter', 'first_name',
'last_name', 'password1', 'password2',
'recaptcha_challenge_field',
'recaptcha_response_field']
#set readonly form fields
self.fields['inviter'].widget.attrs['readonly'] = True
self.fields['email'].widget.attrs['readonly'] = True
......@@ -270,3 +284,28 @@ class ExtendedPasswordResetForm(PasswordResetForm):
from_email = DEFAULT_FROM_EMAIL
send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
t.render(Context(c)), from_email, [user.email])
class SignApprovalTermsForm(forms.ModelForm):
class Meta:
model = AstakosUser
fields = ("has_signed_terms",)
def __init__(self, *args, **kwargs):
super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
def clean_has_signed_terms(self):
has_signed_terms = self.cleaned_data['has_signed_terms']
if not has_signed_terms:
raise forms.ValidationError(_('You have to agree with the terms'))
return has_signed_terms
def save(self, commit=True):
"""
Saves the , after the normal
save behavior is complete.
"""
user = super(SignApprovalTermsForm, self).save(commit=False)
user.date_signed_terms = datetime.now()
if commit:
user.save()
return user
\ No newline at end of file
......@@ -42,7 +42,7 @@ from urlparse import urljoin
from random import randint
from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, SITENAME, BASEURL, DEFAULT_ADMIN_EMAIL
from astakos.im.models import Invitation
from astakos.im.models import Invitation, AstakosUser
logger = logging.getLogger(__name__)
......@@ -151,3 +151,11 @@ def invite(inviter, username, realname, email_template_name='im/welcome_email.tx
send_invitation(invitation, email_template_name)
inviter.invitations = max(0, inviter.invitations - 1)
inviter.save()
def set_user_credibility(email, has_credits):
try:
user = AstakosUser.objects.get(email=email)
user.has_credits = has_credits
user.save()
except AstakosUser.DoesNotExist, e:
logger.exception(e)
# Copyright 2012 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from optparse import make_option
from random import choice
from string import digits, lowercase, uppercase
from uuid import uuid4
from time import time
from os.path import abspath
from django.core.management.base import BaseCommand, CommandError
from astakos.im.models import ApprovalTerms
class Command(BaseCommand):
args = "<location>"
help = "Insert approval terms"
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("Invalid number of arguments")
location = abspath(args[0].decode('utf8'))
try:
f = open(location, 'r')
except IOError:
raise CommandError("Invalid location")
terms = ApprovalTerms(location=location)
terms.save()
msg = "Created term id %d" % (terms.id,)
self.stdout.write(msg + '\n')
......@@ -37,6 +37,8 @@ from string import digits, lowercase, uppercase
from uuid import uuid4
from django.core.management.base import BaseCommand, CommandError
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from astakos.im.models import AstakosUser
......@@ -74,6 +76,11 @@ class Command(BaseCommand):
args = [a.decode('utf8') for a in args]
email, first, last, affiliation = args
try:
validate_email( email )
except ValidationError:
raise CommandError("Invalid email")
username = uuid4().hex[:30]
password = options.get('password')
if password is None:
......
......@@ -37,7 +37,6 @@ from django.db import transaction
from astakos.im.functions import send_verification
from ._common import get_user
class Command(BaseCommand):
args = "<user ID or email> [user ID or email] ..."
......
......@@ -72,9 +72,12 @@ class Command(BaseCommand):
'invitations': user.invitations,
'invitation level': user.level,
'provider': user.provider,
'verified': format_bool(user.is_verified)
'verified': format_bool(user.is_verified),
'has_credits': format_bool(user.has_credits),
'has_signed_terms': format_bool(user.has_signed_terms),
'date_signed_terms': format_date(user.date_signed_terms)
}
for key, val in sorted(kv.items()):
line = '%s: %s\n' % (key.rjust(16), val)
line = '%s: %s\n' % (key.rjust(17), val)
self.stdout.write(line.encode('utf8'))
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'AstakosUser.has_credits'
db.add_column('im_astakosuser', 'has_credits', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
def backwards(self, orm):
# Deleting field 'AstakosUser.has_credits'
db.delete_column('im_astakosuser', 'has_credits')
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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'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'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'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', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},