1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
11 # 2. Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials
14 # provided with the distribution.
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
39 from time import asctime
40 from datetime import datetime, timedelta
41 from base64 import b64encode
42 from urlparse import urlparse, urlunparse
43 from random import randint
45 from django.db import models, IntegrityError
46 from django.contrib.auth.models import User, UserManager, Group
47 from django.utils.translation import ugettext as _
48 from django.core.exceptions import ValidationError
49 from django.template.loader import render_to_string
50 from django.core.mail import send_mail
51 from django.db import transaction
52 from django.db.models.signals import post_save, post_syncdb
54 from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, \
55 AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION, SITENAME, \
56 EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL
58 QUEUE_CLIENT_ID = 3 # Astakos.
60 logger = logging.getLogger(__name__)
62 class AstakosUser(User):
64 Extends ``django.contrib.auth.models.User`` by defining additional fields.
66 # Use UserManager to get the create_user method, etc.
67 objects = UserManager()
69 affiliation = models.CharField('Affiliation', max_length=255, blank=True)
70 provider = models.CharField('Provider', max_length=255, blank=True)
73 user_level = DEFAULT_USER_LEVEL
74 level = models.IntegerField('Inviter level', default=user_level)
75 invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
77 auth_token = models.CharField('Authentication Token', max_length=32,
78 null=True, blank=True)
79 auth_token_created = models.DateTimeField('Token creation date', null=True)
80 auth_token_expires = models.DateTimeField('Token expiration date', null=True)
82 updated = models.DateTimeField('Update date')
83 is_verified = models.BooleanField('Is verified?', default=False)
85 # ex. screen_name for twitter, eppn for shibboleth
86 third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
88 email_verified = models.BooleanField('Email verified?', default=False)
90 has_credits = models.BooleanField('Has credits?', default=False)
91 has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
92 date_signed_terms = models.DateTimeField('Signed terms date', null=True, blank=True)
94 activation_sent = models.DateTimeField('Activation sent data', null=True, blank=True)
96 __has_signed_terms = False
99 def __init__(self, *args, **kwargs):
100 super(AstakosUser, self).__init__(*args, **kwargs)
101 self.__has_signed_terms = self.has_signed_terms
103 self.__groupnames = [g.name for g in self.groups.all()]
105 self.is_active = False
109 return '%s %s' %(self.first_name, self.last_name)
112 def realname(self, value):
113 parts = value.split(' ')
115 self.first_name = parts[0]
116 self.last_name = parts[1]
118 self.last_name = parts[0]
121 def invitation(self):
123 return Invitation.objects.get(username=self.email)
124 except Invitation.DoesNotExist:
127 def save(self, update_timestamps=True, **kwargs):
128 if update_timestamps:
130 self.date_joined = datetime.now()
131 self.updated = datetime.now()
133 # update date_signed_terms if necessary
134 if self.__has_signed_terms != self.has_signed_terms:
135 self.date_signed_terms = datetime.now()
139 while not self.username:
140 username = uuid.uuid4().hex[:30]
142 AstakosUser.objects.get(username = username)
143 except AstakosUser.DoesNotExist, e:
144 self.username = username
145 if not self.provider:
146 self.provider = 'local'
147 report_user_event(self)
148 self.validate_unique_email_isactive()
149 if self.is_active and self.activation_sent:
150 # reset the activation sent
151 self.activation_sent = None
152 super(AstakosUser, self).save(**kwargs)
154 # set group if does not exist
155 groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default'
156 if groupname not in self.__groupnames:
158 group = Group.objects.get(name = groupname)
159 self.groups.add(group)
160 except Group.DoesNotExist, e:
163 def renew_token(self):
165 md5.update(self.username)
166 md5.update(self.realname.encode('ascii', 'ignore'))
167 md5.update(asctime())
169 self.auth_token = b64encode(md5.digest())
170 self.auth_token_created = datetime.now()
171 self.auth_token_expires = self.auth_token_created + \
172 timedelta(hours=AUTH_TOKEN_DURATION)
173 msg = 'Token renewed for %s' % self.email
174 logger._log(LOGGING_LEVEL, msg, [])
176 def __unicode__(self):
179 def conflicting_email(self):
180 q = AstakosUser.objects.exclude(username = self.username)
181 q = q.filter(email = self.email)
186 def validate_unique_email_isactive(self):
188 Implements a unique_together constraint for email and is_active fields.
190 q = AstakosUser.objects.exclude(username = self.username)
191 q = q.filter(email = self.email)
192 q = q.filter(is_active = self.is_active)
194 raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]})
196 def signed_terms(self):
197 term = get_latest_terms()
200 if not self.has_signed_terms:
202 if not self.date_signed_terms:
204 if self.date_signed_terms < term.date:
205 self.has_signed_terms = False
206 self.date_signed_terms = None
211 class ApprovalTerms(models.Model):
213 Model for approval terms
216 date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
217 location = models.CharField('Terms location', max_length=255)
219 class Invitation(models.Model):
221 Model for registring invitations
223 inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
225 realname = models.CharField('Real name', max_length=255)
226 username = models.CharField('Unique ID', max_length=255, unique=True)
227 code = models.BigIntegerField('Invitation code', db_index=True)
228 is_consumed = models.BooleanField('Consumed?', default=False)
229 created = models.DateTimeField('Creation date', auto_now_add=True)
230 consumed = models.DateTimeField('Consumption date', null=True, blank=True)
232 def __init__(self, *args, **kwargs):
233 super(Invitation, self).__init__(*args, **kwargs)
235 self.code = _generate_invitation_code()
238 self.is_consumed = True
239 self.consumed = datetime.now()
242 def __unicode__(self):
243 return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
245 def report_user_event(user):
246 def should_send(user):
247 # report event incase of new user instance
248 # or if specific fields are modified
251 db_instance = AstakosUser.objects.get(id = user.id)
252 for f in BILLING_FIELDS:
253 if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
257 if QUEUE_CONNECTION and should_send(user):
259 from astakos.im.queue.userevent import UserEvent
260 from synnefo.lib.queue import exchange_connect, exchange_send, \
263 eventType = 'create' if not user.id else 'modify'
264 body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
265 conn = exchange_connect(QUEUE_CONNECTION)
266 parts = urlparse(QUEUE_CONNECTION)
267 exchange = parts.path[1:]
268 routing_key = '%s.user' % exchange
269 exchange_send(conn, routing_key, body)
272 def _generate_invitation_code():
274 code = randint(1, 2L**63 - 1)
276 Invitation.objects.get(code=code)
277 # An invitation with this code already exists, try again
278 except Invitation.DoesNotExist:
281 def get_latest_terms():
283 term = ApprovalTerms.objects.order_by('-id')[0]
289 class EmailChangeManager(models.Manager):
290 @transaction.commit_on_success
291 def change_email(self, activation_key):
293 Validate an activation key and change the corresponding
296 If the key is valid and has not expired, return the ``User``
299 If the key is not valid or has expired, return ``None``.
301 If the key is valid but the ``User`` is already active,
304 After successful email change the activation record is deleted.
306 Throws ValueError if there is already
309 email_change = self.model.objects.get(activation_key=activation_key)
310 if email_change.activation_key_expired():
311 email_change.delete()
312 raise EmailChange.DoesNotExist
313 # is there an active user with this address?
315 AstakosUser.objects.get(email=email_change.new_email_address)
316 except AstakosUser.DoesNotExist:
319 raise ValueError(_('The new email address is reserved.'))
321 user = AstakosUser.objects.get(pk=email_change.user_id)
322 user.email = email_change.new_email_address
324 email_change.delete()
326 except EmailChange.DoesNotExist:
327 raise ValueError(_('Invalid activation key'))
329 class EmailChange(models.Model):
330 new_email_address = models.EmailField(_(u'new e-mail address'), help_text=_(u'Your old email address will be used until you verify your new one.'))
331 user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
332 requested_at = models.DateTimeField(default=datetime.now())
333 activation_key = models.CharField(max_length=40, unique=True, db_index=True)
335 objects = EmailChangeManager()
337 def activation_key_expired(self):
338 expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
339 return self.requested_at + expiration_date < datetime.now()
341 class Service(models.Model):
342 name = models.CharField('Name', max_length=255, unique=True)
343 url = models.FilePathField()
344 icon = models.FilePathField(blank=True)
345 auth_token = models.CharField('Authentication Token', max_length=32,
346 null=True, blank=True)
347 auth_token_created = models.DateTimeField('Token creation date', null=True)
348 auth_token_expires = models.DateTimeField('Token expiration date', null=True)
350 def save(self, **kwargs):
354 super(Service, self).save(**kwargs)
356 def renew_token(self):
358 md5.update(self.name.encode('ascii', 'ignore'))
359 md5.update(self.url.encode('ascii', 'ignore'))
360 md5.update(asctime())
362 self.auth_token = b64encode(md5.digest())
363 self.auth_token_created = datetime.now()
364 self.auth_token_expires = self.auth_token_created + \
365 timedelta(hours=AUTH_TOKEN_DURATION)
367 class AdditionalMail(models.Model):
369 Model for registring invitations
371 owner = models.ForeignKey(AstakosUser)
372 email = models.EmailField(unique=True)
374 def create_astakos_user(u):
376 AstakosUser.objects.get(user_ptr=u.pk)
377 except AstakosUser.DoesNotExist:
378 extended_user = AstakosUser(user_ptr_id=u.pk)
379 extended_user.__dict__.update(u.__dict__)
380 extended_user.renew_token()
385 def superuser_post_syncdb(sender, **kwargs):
386 # if there was created a superuser
387 # associate it with an AstakosUser
388 admins = User.objects.filter(is_superuser=True)
390 create_astakos_user(u)
392 post_syncdb.connect(superuser_post_syncdb)
394 def superuser_post_save(sender, instance, **kwargs):
395 if instance.is_superuser:
396 create_astakos_user(instance)
398 post_save.connect(superuser_post_save, sender=User)