--- /dev/null
+# 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 'User.provider'
+ db.add_column('im_user', 'provider', self.gf('django.db.models.fields.CharField')(default='', max_length=255), keep_default=False)
+
+ # Deleting field 'Invitation.is_accepted'
+ db.delete_column('im_invitation', 'is_accepted')
+
+ # Deleting field 'Invitation.accepted'
+ db.delete_column('im_invitation', 'accepted')
+
+ # Adding field 'Invitation.is_consumed'
+ db.add_column('im_invitation', 'is_consumed', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
+
+ # Adding field 'Invitation.consumed'
+ db.add_column('im_invitation', 'consumed', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'User.provider'
+ db.delete_column('im_user', 'provider')
+
+ # Adding field 'Invitation.is_accepted'
+ db.add_column('im_invitation', 'is_accepted', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
+
+ # Adding field 'Invitation.accepted'
+ db.add_column('im_invitation', 'accepted', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), keep_default=False)
+
+ # Deleting field 'Invitation.is_consumed'
+ db.delete_column('im_invitation', 'is_consumed')
+
+ # Deleting field 'Invitation.consumed'
+ db.delete_column('im_invitation', 'consumed')
+
+
+ models = {
+ 'im.invitation': {
+ 'Meta': {'object_name': 'Invitation'},
+ 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.User']"}),
+ 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'uniq': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'im.user': {
+ 'Meta': {'object_name': 'User'},
+ 'affiliation': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
+ 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'email': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'is_admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}),
+ 'password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'provider': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'realname': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '16'}),
+ 'uniq': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ }
+ }
+
+ complete_apps = ['im']
('ACTIVE', 'Active'),
('DELETED', 'Deleted'),
('SUSPENDED', 'Suspended'),
- ('UNVERIFIED', 'Unverified')
+ ('UNVERIFIED', 'Unverified'),
+ ('PENDING', 'Pending')
)
uniq = models.CharField('Unique ID', max_length=255, null=True)
realname = models.CharField('Real Name', max_length=255, default='')
email = models.CharField('Email', max_length=255, default='')
affiliation = models.CharField('Affiliation', max_length=255, default='')
+ provider = models.CharField('Provider', max_length=255, default='')
state = models.CharField('Account state', choices=ACCOUNT_STATE,
- max_length=16, default='ACTIVE')
+ max_length=16, default='PENDING')
#for invitations
level = models.IntegerField('Inviter level', default=4)
def quota(self, value):
set_quota(self.uniq, value)
+ @property
+ def invitation(self):
+ return Invitation.objects.get(uniq=self.uniq)
+
def save(self, update_timestamps=True, **kwargs):
if update_timestamps:
if not self.id:
def __unicode__(self):
return self.uniq
-
class Invitation(models.Model):
inviter = models.ForeignKey(User, related_name='invitations_sent',
null=True)
realname = models.CharField('Real name', max_length=255)
- uniq = models.CharField('Real name', max_length=255)
+ uniq = models.CharField('Unique ID', max_length=255)
code = models.BigIntegerField('Invitation code', db_index=True)
- is_accepted = models.BooleanField('Accepted?', default=False)
+ is_consumed = models.BooleanField('Consumed?', default=False)
created = models.DateTimeField('Creation date', auto_now_add=True)
- accepted = models.DateTimeField('Acceptance date', null=True, blank=True)
+ consumed = models.DateTimeField('Consumption date', null=True, blank=True)
def __unicode__(self):
return '%s -> %s [%d]' % (self.inviter, self.uniq, self.code)
from urllib import unquote
def login(request):
- username = '%s@local' % request.POST.get('username')
+ username = request.POST.get('username')
password = request.POST.get('password')
if not username:
return HttpResponseBadRequest('No user')
- if not username:
+ if not password:
return HttpResponseBadRequest('No password')
try:
return prepare_response(request, user, next)
-def activate(request):
- token = request.GET.get('auth')
- next = request.GET.get('next')
- try:
- user = User.objects.get(auth_token=token)
- except User.DoesNotExist:
- return HttpResponseBadRequest('No such user')
-
- user.state = 'ACTIVE'
- user.save()
- return prepare_response(request, user, next, renew=True)
+#def activate(request):
+# token = request.GET.get('auth')
+# next = request.GET.get('next')
+# try:
+# user = User.objects.get(auth_token=token)
+# except User.DoesNotExist:
+# return HttpResponseBadRequest('No such user')
+#
+# user.state = 'ACTIVE'
+# user.save()
+# return prepare_response(request, user, next, renew=True)
def reset_password(request):
if request.method == 'GET':
{% block body %}
<ul class="unstyled">
- <li><strong>{{ stats.users }}</strong> Users</li>
- <li><strong>{{ stats.invitations }}</strong> Invitation{{ stats.invitations|pluralize }} sent (<strong>{{ stats.invitations_accepted }}</strong> accepted)</li>
+ <li><strong>{{ stats.users }}</strong> User{{ stats.users|pluralize }} (<strong>{{ stats.pending }}</strong> pending)</li>
+ <li><strong>{{ stats.invitations }}</strong> Invitation{{ stats.invitations|pluralize }} sent (<strong>{{ stats.invitations_consumed }}</strong> consumed)</li>
</ul>
{% endblock body %}
<li{% ifequal tab "users" %} class="active"{% endifequal %}>
<a href="{% url pithos.im.views.users_list %}">Users</a>
</li>
+ <li{% ifequal tab "pending" %} class="active"{% endifequal %}>
+ <a href="{% url pithos.im.views.pending_users %}">Pending Users</a>
+ </li>
<li{% ifequal tab "invitations" %} class="active"{% endifequal %}>
<a href="{% url pithos.im.views.invitations_list %}">Invitations</a>
</li>
{% block body%}
<div class="row">
- {% if "local" in standard_modules %}
+ {% if "local" in im_modules %}
<div class="span4">
<h4>Local account</h4>
<form action="{% url pithos.im.target.local.login %}" method="post" class="form-stacked">
</form>
</div>
{% endif %}
- {% if "invitation" in standard_modules %}
- <div class="span4">
- <h4>Invitation</h4>
- <form action="{% url pithos.im.target.invitation.login %}" class="form-stacked">
- <fieldset>
- <div class="clearfix">
- <label for="code">Code:</label>
- <div class="input">
- <input class="span3" id="user-code" name="code" type="text" />
- </div>
- </div>
- </fieldset>
- <div class="">
- <input type="hidden" name="next" value="{{ next }}">
- <button type="submit" class="btn primary">Go</button>
- </div>
- </form>
- </div>
- {% endif %}
{% if other_modules %}
<div class="span8">
<h4>Other provider</h4>
<th>Code</th>
<th>Inviter Uniq</th>
<th>Inviter Real Name</th>
- <th>Is_accepted</th>
+ <th>Is consumed</th>
<th>Created</th>
- <th>Accepted</th>
+ <th>Consumed</th>
</tr>
</thead>
<tbody>
<td>{{ inv.code }}</td>
<td>{{ inv.inviter.uniq }}</td>
<td>{{ inv.inviter.realname }}</td>
- <td>{{ inv.is_accepted }}</td>
+ <td>{{ inv.is_consumed }}</td>
<td>{{ inv.created }}</td>
- <td>{{ inv.accepted }}</td>
+ <td>{{ inv.consumed }}</td>
</tr>
{% endfor %}
</tbody>
{% block body %}
<form action="{% url pithos.im.views.local_create %}" method="post">
+{% if provider == 'local'%}
<div class="clearfix">
<label for="user-uniq">Username</label>
<div class="input">
- <input class="span4" id="user-uniq" name="uniq" type="text" />
+ <input class="span4" id="user-uniq" name="uniq" {%if inv %} value={{ inv.uniq }} {% endif %} type="text" />
</div>
</div>
</div>
<div class="clearfix">
+ <label for="user-password">Confirm Password</label>
+ <div class="input">
+ <input class="span4" id="user-retype-password" name="retype_password" type="password" />
+ </div>
+ </div>
+
+ <div class="clearfix">
<label for="user-realname">Real Name</label>
<div class="input">
- <input class="span4" id="user-realname" name="realname" type="text" />
+ <input class="span4" id="user-realname" name="realname" {%if inv %} value={{ inv.realname }} {% endif %} type="text" />
</div>
</div>
<input class="span4" id="user-email" name="email" type="text" />
</div>
</div>
-
+{% endif %}
+
+{% if provider == 'twitter' %}
+ <div class="clearfix">
+ <label for="user-uniq">Twitter </label>
+ <div class="input">
+ <input class="span4" id="user-uniq" name="uniq" {%if inv %} value={{ inv.uniq }} {% endif %} type="text" />
+ </div>
+ </div>
+{% endif %}
+
<div class="actions">
<button type="submit" class="btn primary">Create</button>
<button type="reset" class="btn">Reset</button>
# or implied, of GRNET S.A.
from django.conf import settings
-from django.conf.urls.defaults import patterns
-
+from django.conf.urls.defaults import patterns, include
urlpatterns = patterns('pithos.im.views',
(r'^$', 'index'),
(r'^admin/users/(\d+)/modify/?$', 'users_modify'),
(r'^admin/users/(\d+)/delete/?$', 'users_delete'),
(r'^admin/users/export/?$', 'users_export'),
+ (r'^admin/users/pending/?$', 'pending_users'),
+ (r'^admin/users/activate/(\d+)/?$', 'users_activate'),
(r'^admin/invitations/?$', 'invitations_list'),
(r'^admin/invitations/export/?$', 'invitations_export'),
(r'^profile/?$', 'users_profile'),
(r'^profile/edit/?$', 'users_edit'),
+
+ (r'^signup/?$', 'signup'),
+ (r'^local/create/?$', 'local_create'),
+ (r'^openid/create/?$', 'openid_create'),
+)
+
+
+urlpatterns += patterns('',
+ (r'^account/', include('django_authopenid.urls')),
)
urlpatterns += patterns('pithos.im.target',
{'document_root': settings.PROJECT_PATH + '/im/static'})
)
-if 'local' in settings.IM_STANDARD_MODULES:
+
+if 'local' in settings.IM_MODULES:
urlpatterns += patterns('pithos.im.views',
- (r'^local/create/?$', 'local_create'),
+# (r'^local/create/?$', 'local_create'),
(r'^local/reclaim/?$', 'reclaim_password')
)
urlpatterns += patterns('pithos.im.target',
(r'^local/?$', 'local.login'),
- (r'^local/activate/?$', 'local.activate'),
+# (r'^local/activate/?$', 'local.activate'),
(r'^local/reset/?$', 'local.reset_password')
)
-if 'invitation' in settings.IM_STANDARD_MODULES:
- urlpatterns += patterns('pithos.im.views',
- (r'^invite/?$', 'invite'),
- )
- urlpatterns += patterns('pithos.im.target',
- (r'^login/invitation/?$', 'invitation.login')
- )
+#if 'invitation' in settings.IM_STANDARD_MODULES:
+# urlpatterns += patterns('pithos.im.views',
+# (r'^invite/?$', 'invite'),
+# )
+# urlpatterns += patterns('pithos.im.target',
+# (r'^login/invitation/?$', 'invitation.login')
+# )
-if 'shibboleth' in settings.IM_OTHER_MODULES:
+if 'shibboleth' in settings.IM_MODULES:
urlpatterns += patterns('pithos.im.target',
(r'^login/shibboleth/?$', 'shibboleth.login')
)
-if 'twitter' in settings.IM_OTHER_MODULES:
+if 'twitter' in settings.IM_MODULES:
urlpatterns += patterns('pithos.im.target',
(r'^login/twitter/?$', 'twitter.login'),
(r'^login/twitter/authenticated/?$', 'twitter.authenticated')
from django.conf import settings
from django.core.mail import send_mail
-from django.http import HttpResponse, HttpResponseRedirect
+from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.utils.http import urlencode
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
+from django import forms
+
+from django_authopenid.forms import *
from urllib import quote
def index(request):
- kwargs = {'standard_modules':settings.IM_STANDARD_MODULES,
- 'other_modules':settings.IM_OTHER_MODULES}
+ kwargs = {'im_modules':settings.IM_MODULES,
+ 'other_modules':settings.IM_MODULES[1:]}
return render_response('index.html',
next=request.GET.get('next', ''),
**kwargs)
def admin(request):
stats = {}
stats['users'] = User.objects.count()
+ stats['pending'] = User.objects.filter(state = 'PENDING').count()
invitations = Invitation.objects.all()
stats['invitations'] = invitations.count()
- stats['invitations_accepted'] = invitations.filter(is_accepted=True).count()
+ stats['invitations_consumed'] = invitations.filter(is_consumed=True).count()
return render_response('admin.html', tab='home', stats=stats)
user.save()
return redirect(users_info, user.id)
-
@requires_admin
def users_delete(request, user_id):
user = User.objects.get(id=user_id)
user.delete()
return redirect(users_list)
+@requires_admin
+def pending_users(request):
+ users = User.objects.order_by('id')
+
+ users = users.filter(state = 'PENDING')
+
+ try:
+ page = int(request.GET.get('page', 1))
+ except ValueError:
+ page = 1
+ offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
+ limit = offset + settings.ADMIN_PAGE_LIMIT
+
+ npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT))
+ prev = page - 1 if page > 1 else None
+ next = page + 1 if page < npages else None
+ return render_response('pending_users.html',
+ users=users[offset:limit],
+ filter=filter,
+ pages=range(1, npages + 1),
+ page=page,
+ prev=prev,
+ next=next)
+
+def send_greeting(baseurl, user):
+ url = baseurl
+ subject = _('Welcome to Pithos')
+ message = render_to_string('welcome.txt', {
+ 'user': user,
+ 'url': url,
+ 'baseurl': baseurl,
+ 'service': settings.SERVICE_NAME,
+ 'support': settings.DEFAULT_CONTACT_EMAIL})
+ sender = settings.DEFAULT_FROM_EMAIL
+ send_mail(subject, message, sender, [user.email])
+ logging.info('Sent greeting %s', user)
+
+@requires_admin
+def users_activate(request, user_id):
+ user = User.objects.get(id=user_id)
+ user.state = 'ACTIVE'
+ status = 'success'
+ try:
+ send_greeting(request.build_absolute_uri('/').rstrip('/'), user)
+ message = _('Greeting sent to %s' % user.email)
+ user.save()
+ except (SMTPException, socket.error) as e:
+ status = 'error'
+ name = 'strerror'
+ message = getattr(e, name) if hasattr(e, name) else e
+
+ users = User.objects.order_by('id')
+ users = users.filter(state = 'PENDING')
+
+ try:
+ page = int(request.POST.get('page', 1))
+ except ValueError:
+ page = 1
+ offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
+ limit = offset + settings.ADMIN_PAGE_LIMIT
+
+ npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT))
+ prev = page - 1 if page > 1 else None
+ next = page + 1 if page < npages else None
+ return render_response('pending_users.html',
+ users=users[offset:limit],
+ filter=filter,
+ pages=range(1, npages + 1),
+ page=page,
+ prev=prev,
+ next=next,
+ message=message)
def generate_invitation_code():
while True:
def send_invitation(baseurl, inv):
- url = settings.INVITATION_LOGIN_TARGET % (baseurl, inv.code, quote(baseurl))
+ url = settings.SIGNUP_TARGET % (baseurl, inv.code, quote(baseurl))
subject = _('Invitation to Pithos')
message = render_to_string('invitation.txt', {
'invitation': inv,
def local_create(request):
if request.method == 'GET':
- return render_response('local_create.html')
+ provider = request.GET.get('provider', None)
+ if not provider:
+ return HttpResponseBadRequest('No provider')
+ code = request.GET.get('code', None)
+ kwargs = {'provider':provider}
+ if code:
+ try:
+ invitation = Invitation.objects.get(code = code)
+ kwargs['inv']=invitation
+ except Invitation.DoesNotExist:
+ return HttpResponseBadRequest('Wrong invitation code')
+ return render_response('local_create.html', **kwargs)
elif request.method == 'POST':
username = request.POST.get('uniq')
realname = request.POST.get('realname')
email = request.POST.get('email')
password = request.POST.get('password')
+ retype_password = request.POST.get('retype_passwords')
status = 'success'
cookie_value = None
if not username:
elif not password:
status = 'error'
message = 'No password provided'
+ elif not retype_password:
+ status = 'error'
+ message = 'Need to enter password twice'
+ elif password != retype_password:
+ status = 'error'
+ message = 'Passwords do not match'
elif not email:
status = 'error'
message = 'No email provided'
if status == 'success':
- username = '%s@local' % username
try:
user = User.objects.get(uniq=username)
status = 'error'
user.password = request.POST.get('password')
user.is_admin = False
user.quota = 0
- user.state = 'UNVERIFIED'
+ user.state = 'ACTIVE' if is_preaccepted(user) else 'PENDING'
+ if user.invitation:
+ user.invitation.is_consumed = True
user.level = 1
user.renew_token()
try:
message = _('Verification sent to %s' % user.email)
user.save()
except (SMTPException, socket.error) as e:
+ print e
status = 'error'
name = 'strerror'
message = getattr(e, name) if hasattr(e, name) else e
@requires_admin
def users_create(request):
if request.method == 'GET':
- return render_response('users_create.html')
+ return render_response('users_local_create.html')
if request.method == 'POST':
user = User()
user.uniq = request.POST.get('uniq')
user.affiliation = request.POST.get('affiliation')
user.quota = int(request.POST.get('quota') or 0) * (1024 ** 3) # In GiB
user.renew_token()
+ user.provider = 'local'
user.save()
return redirect(users_info, user.id)
'status': status,
'message': message})
return HttpResponse(html)
-
+
+def is_preaccepted(user):
+ return True if settings.INVITATIONS_ENABLED and user.invitation else False
+
+def signup(request):
+ if request.method == 'GET':
+ kwargs = {'im_modules':settings.IM_MODULES}
+ if settings.INVITATIONS_ENABLED:
+ code = request.GET.get('code')
+ if not code:
+ return HttpResponseBadRequest('No code')
+ try:
+ invitation = Invitation.objects.get(code=code)
+ if invitation.is_consumed:
+ return HttpResponseBadRequest('Invitation has beeen used')
+ except Invitation.DoesNotExist:
+ return HttpResponseBadRequest('Wrong invitation code')
+ kwargs['inv'] = invitation
+ return render_response('signup.html', **kwargs)
+ elif request.method == 'POST':
+ choice = request.POST.get('choice')
+ if not choice:
+ return HttpResponseBadRequest('No provider selected')
+
+ provider = choice
+
+ code = request.POST.get('code')
+
+ if provider == 'local':
+ url = reverse('pithos.im.views.local.create')
+ if settings.INVITATIONS_ENABLED and code:
+ url = '%s?code=%s&provider=%s' % (url, code, provider)
+ return redirect(url)
+ elif provider == 'twitter':
+ url = reverse('pithos.im.views.openid_create')
+ return redirect(url)
+
+def openid_create(request):
+ form1 = OpenidSigninForm()
+ kwargs = {'form1':form1}
+ return render_response('openid_create.html')
\ No newline at end of file
DEFAULT_FROM_EMAIL = 'Pithos <no-reply@grnet.gr>'
DEFAULT_CONTACT_EMAIL = 'support@pithos.grnet.gr'
-# Where users should login with their invitation code
-INVITATION_LOGIN_TARGET = '%s/im/login/invitation?code=%d&next=%s'
+# Where users should signup with their invitation code
+SIGNUP_TARGET = '%s/im/signup/?code=%d&next=%s'
# Where users should activate their local account
ACTIVATION_LOGIN_TARGET = '%s/im/local/activate/?auth=%s&next=%s'
PASSWORD_RESET_TARGET = '%s/im/local/reset/?username=%s&next=%s'
# Identity Management enabled modules
-IM_STANDARD_MODULES = ['local', 'invitation']
-IM_OTHER_MODULES = ['twitter', 'shibboleth']
+IM_MODULES = ['local', 'twitter', 'shibboleth']
# Force user profile verification
FORCE_PROFILE_UPDATE = False
+
+#Enable invitations
+INVITATIONS_ENABLED = True