cbd568bcbb84bcf9720a73520beed5f99c233ab9
[astakos] / snf-astakos-app / astakos / im / models.py
1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
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.
15 #
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.
28 #
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.
33
34 import hashlib
35 import uuid
36 import logging
37 import json
38
39 from time import asctime
40 from datetime import datetime, timedelta
41 from base64 import b64encode
42 from urlparse import urlparse
43 from urllib import quote
44 from random import randint
45 from collections import defaultdict
46
47 from django.db import models, IntegrityError
48 from django.contrib.auth.models import User, UserManager, Group, Permission
49 from django.utils.translation import ugettext as _
50 from django.db import transaction
51 from django.core.exceptions import ValidationError
52 from django.db.models.signals import (
53     pre_save, post_save, post_syncdb, post_delete
54 )
55 from django.contrib.contenttypes.models import ContentType
56
57 from django.dispatch import Signal
58 from django.db.models import Q
59 from django.core.urlresolvers import reverse
60 from django.utils.http import int_to_base36
61 from django.contrib.auth.tokens import default_token_generator
62 from django.conf import settings
63 from django.utils.importlib import import_module
64 from django.utils.safestring import mark_safe
65 from django.core.validators import email_re
66
67 from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
68                                  AUTH_TOKEN_DURATION, BILLING_FIELDS,
69                                  EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
70 from astakos.im import settings as astakos_settings
71 from astakos.im.endpoints.qh import (
72     register_users, send_quota, register_resources
73 )
74 from astakos.im import auth_providers
75 from astakos.im.endpoints.aquarium.producer import report_user_event
76 from astakos.im.functions import send_invitation
77 #from astakos.im.tasks import propagate_groupmembers_quota
78
79 import astakos.im.messages as astakos_messages
80
81 logger = logging.getLogger(__name__)
82
83 DEFAULT_CONTENT_TYPE = None
84 try:
85     content_type = ContentType.objects.get(app_label='im', model='astakosuser')
86 except:
87     content_type = DEFAULT_CONTENT_TYPE
88
89 RESOURCE_SEPARATOR = '.'
90
91 inf = float('inf')
92
93 class Service(models.Model):
94     name = models.CharField('Name', max_length=255, unique=True, db_index=True)
95     url = models.FilePathField()
96     icon = models.FilePathField(blank=True)
97     auth_token = models.CharField('Authentication Token', max_length=32,
98                                   null=True, blank=True)
99     auth_token_created = models.DateTimeField('Token creation date', null=True)
100     auth_token_expires = models.DateTimeField(
101         'Token expiration date', null=True)
102
103     def renew_token(self):
104         md5 = hashlib.md5()
105         md5.update(self.name.encode('ascii', 'ignore'))
106         md5.update(self.url.encode('ascii', 'ignore'))
107         md5.update(asctime())
108
109         self.auth_token = b64encode(md5.digest())
110         self.auth_token_created = datetime.now()
111         self.auth_token_expires = self.auth_token_created + \
112             timedelta(hours=AUTH_TOKEN_DURATION)
113
114     def __str__(self):
115         return self.name
116
117     @property
118     def resources(self):
119         return self.resource_set.all()
120
121     @resources.setter
122     def resources(self, resources):
123         for s in resources:
124             self.resource_set.create(**s)
125
126     def add_resource(self, service, resource, uplimit, update=True):
127         """Raises ObjectDoesNotExist, IntegrityError"""
128         resource = Resource.objects.get(service__name=service, name=resource)
129         if update:
130             AstakosUserQuota.objects.update_or_create(user=self,
131                                                       resource=resource,
132                                                       defaults={'uplimit': uplimit})
133         else:
134             q = self.astakosuserquota_set
135             q.create(resource=resource, uplimit=uplimit)
136
137
138 class ResourceMetadata(models.Model):
139     key = models.CharField('Name', max_length=255, unique=True, db_index=True)
140     value = models.CharField('Value', max_length=255)
141
142
143 class Resource(models.Model):
144     name = models.CharField('Name', max_length=255, unique=True, db_index=True)
145     meta = models.ManyToManyField(ResourceMetadata)
146     service = models.ForeignKey(Service)
147     desc = models.TextField('Description', null=True)
148     unit = models.CharField('Name', null=True, max_length=255)
149     group = models.CharField('Group', null=True, max_length=255)
150
151     def __str__(self):
152         return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
153
154
155 class GroupKind(models.Model):
156     name = models.CharField('Name', max_length=255, unique=True, db_index=True)
157
158     def __str__(self):
159         return self.name
160
161
162 class AstakosGroup(Group):
163     kind = models.ForeignKey(GroupKind)
164     homepage = models.URLField(
165         'Homepage Url', max_length=255, null=True, blank=True)
166     desc = models.TextField('Description', null=True)
167     policy = models.ManyToManyField(
168         Resource,
169         null=True,
170         blank=True,
171         through='AstakosGroupQuota'
172     )
173     creation_date = models.DateTimeField(
174         'Creation date',
175         default=datetime.now()
176     )
177     issue_date = models.DateTimeField('Start date', null=True)
178     expiration_date = models.DateTimeField(
179         'End date',
180          null=True
181     )
182     moderation_enabled = models.BooleanField(
183         'Moderated membership?',
184         default=True
185     )
186     approval_date = models.DateTimeField(
187         'Activation date',
188         null=True,
189         blank=True
190     )
191     estimated_participants = models.PositiveIntegerField(
192         'Estimated #members',
193         null=True,
194         blank=True,
195     )
196     max_participants = models.PositiveIntegerField(
197         'Maximum numder of participants',
198         null=True,
199         blank=True
200     )
201
202     @property
203     def is_disabled(self):
204         if not self.approval_date:
205             return True
206         return False
207
208     @property
209     def is_enabled(self):
210         if self.is_disabled:
211             return False
212         if not self.issue_date:
213             return False
214         if not self.expiration_date:
215             return True
216         now = datetime.now()
217         if self.issue_date > now:
218             return False
219         if now >= self.expiration_date:
220             return False
221         return True
222
223     def enable(self):
224         if self.is_enabled:
225             return
226         self.approval_date = datetime.now()
227         self.save()
228         quota_disturbed.send(sender=self, users=self.approved_members)
229         #propagate_groupmembers_quota.apply_async(
230         #    args=[self], eta=self.issue_date)
231         #propagate_groupmembers_quota.apply_async(
232         #    args=[self], eta=self.expiration_date)
233
234     def disable(self):
235         if self.is_disabled:
236             return
237         self.approval_date = None
238         self.save()
239         quota_disturbed.send(sender=self, users=self.approved_members)
240
241     @transaction.commit_manually
242     def approve_member(self, person):
243         m, created = self.membership_set.get_or_create(person=person)
244         try:
245             m.approve()
246         except:
247             transaction.rollback()
248             raise
249         else:
250             transaction.commit()
251
252 #     def disapprove_member(self, person):
253 #         self.membership_set.remove(person=person)
254
255     @property
256     def members(self):
257         q = self.membership_set.select_related().all()
258         return [m.person for m in q]
259
260     @property
261     def approved_members(self):
262         q = self.membership_set.select_related().all()
263         return [m.person for m in q if m.is_approved]
264
265     @property
266     def quota(self):
267         d = defaultdict(int)
268         for q in self.astakosgroupquota_set.select_related().all():
269             d[q.resource] += q.uplimit or inf
270         return d
271
272     def add_policy(self, service, resource, uplimit, update=True):
273         """Raises ObjectDoesNotExist, IntegrityError"""
274         resource = Resource.objects.get(service__name=service, name=resource)
275         if update:
276             AstakosGroupQuota.objects.update_or_create(
277                 group=self,
278                 resource=resource,
279                 defaults={'uplimit': uplimit}
280             )
281         else:
282             q = self.astakosgroupquota_set
283             q.create(resource=resource, uplimit=uplimit)
284
285     @property
286     def policies(self):
287         return self.astakosgroupquota_set.select_related().all()
288
289     @policies.setter
290     def policies(self, policies):
291         for p in policies:
292             service = p.get('service', None)
293             resource = p.get('resource', None)
294             uplimit = p.get('uplimit', 0)
295             update = p.get('update', True)
296             self.add_policy(service, resource, uplimit, update)
297
298     @property
299     def owners(self):
300         return self.owner.all()
301
302     @property
303     def owner_details(self):
304         return self.owner.select_related().all()
305
306     @owners.setter
307     def owners(self, l):
308         self.owner = l
309         map(self.approve_member, l)
310
311
312
313 class AstakosUserManager(UserManager):
314
315     def get_auth_provider_user(self, provider, **kwargs):
316         """
317         Retrieve AstakosUser instance associated with the specified third party
318         id.
319         """
320         kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
321                           kwargs.iteritems()))
322         return self.get(auth_providers__module=provider, **kwargs)
323
324     def get_by_email(self, email):
325         return self.get(email=email)
326
327     def get_by_identifier(self, email_or_username, **kwargs):
328         try:
329             return self.get(email__iexact=email_or_username, **kwargs)
330         except AstakosUser.DoesNotExist:
331             return self.get(username__iexact=email_or_username, **kwargs)
332
333     def user_exists(self, email_or_username, **kwargs):
334         qemail = Q(email__iexact=email_or_username)
335         qusername = Q(username__iexact=email_or_username)
336         return self.filter(qemail | qusername).exists()
337
338
339 class AstakosUser(User):
340     """
341     Extends ``django.contrib.auth.models.User`` by defining additional fields.
342     """
343     affiliation = models.CharField('Affiliation', max_length=255, blank=True,
344                                    null=True)
345
346     # DEPRECATED FIELDS: provider, third_party_identifier moved in
347     #                    AstakosUserProvider model.
348     provider = models.CharField('Provider', max_length=255, blank=True,
349                                 null=True)
350     # ex. screen_name for twitter, eppn for shibboleth
351     third_party_identifier = models.CharField('Third-party identifier',
352                                               max_length=255, null=True,
353                                               blank=True)
354
355
356     #for invitations
357     user_level = DEFAULT_USER_LEVEL
358     level = models.IntegerField('Inviter level', default=user_level)
359     invitations = models.IntegerField(
360         'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
361
362     auth_token = models.CharField('Authentication Token', max_length=32,
363                                   null=True, blank=True)
364     auth_token_created = models.DateTimeField('Token creation date', null=True)
365     auth_token_expires = models.DateTimeField(
366         'Token expiration date', null=True)
367
368     updated = models.DateTimeField('Update date')
369     is_verified = models.BooleanField('Is verified?', default=False)
370
371     email_verified = models.BooleanField('Email verified?', default=False)
372
373     has_credits = models.BooleanField('Has credits?', default=False)
374     has_signed_terms = models.BooleanField(
375         'I agree with the terms', default=False)
376     date_signed_terms = models.DateTimeField(
377         'Signed terms date', null=True, blank=True)
378
379     activation_sent = models.DateTimeField(
380         'Activation sent data', null=True, blank=True)
381
382     policy = models.ManyToManyField(
383         Resource, null=True, through='AstakosUserQuota')
384
385     uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
386
387     astakos_groups = models.ManyToManyField(
388         AstakosGroup, verbose_name=_('agroups'), blank=True,
389         help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
390         through='Membership')
391
392     __has_signed_terms = False
393     disturbed_quota = models.BooleanField('Needs quotaholder syncing',
394                                            default=False, db_index=True)
395
396     objects = AstakosUserManager()
397
398
399     owner = models.ManyToManyField(
400         AstakosGroup, related_name='owner', null=True)
401
402     def __init__(self, *args, **kwargs):
403         super(AstakosUser, self).__init__(*args, **kwargs)
404         self.__has_signed_terms = self.has_signed_terms
405         if not self.id:
406             self.is_active = False
407
408     @property
409     def realname(self):
410         return '%s %s' % (self.first_name, self.last_name)
411
412     @realname.setter
413     def realname(self, value):
414         parts = value.split(' ')
415         if len(parts) == 2:
416             self.first_name = parts[0]
417             self.last_name = parts[1]
418         else:
419             self.last_name = parts[0]
420
421     def add_permission(self, pname):
422         if self.has_perm(pname):
423             return
424         p, created = Permission.objects.get_or_create(codename=pname,
425                                                       name=pname.capitalize(),
426                                                       content_type=content_type)
427         self.user_permissions.add(p)
428
429     def remove_permission(self, pname):
430         if self.has_perm(pname):
431             return
432         p = Permission.objects.get(codename=pname,
433                                    content_type=content_type)
434         self.user_permissions.remove(p)
435
436     @property
437     def invitation(self):
438         try:
439             return Invitation.objects.get(username=self.email)
440         except Invitation.DoesNotExist:
441             return None
442
443     def invite(self, email, realname):
444         inv = Invitation(inviter=self, username=email, realname=realname)
445         inv.save()
446         send_invitation(inv)
447         self.invitations = max(0, self.invitations - 1)
448         self.save()
449
450     @property
451     def quota(self):
452         """Returns a dict with the sum of quota limits per resource"""
453         d = defaultdict(int)
454         for q in self.policies:
455             d[q.resource] += q.uplimit or inf
456         for m in self.extended_groups:
457             if not m.is_approved:
458                 continue
459             g = m.group
460             if not g.is_enabled:
461                 continue
462             for r, uplimit in g.quota.iteritems():
463                 d[r] += uplimit or inf
464         # TODO set default for remaining
465         return d
466
467     @property
468     def policies(self):
469         return self.astakosuserquota_set.select_related().all()
470
471     @policies.setter
472     def policies(self, policies):
473         for p in policies:
474             service = policies.get('service', None)
475             resource = policies.get('resource', None)
476             uplimit = policies.get('uplimit', 0)
477             update = policies.get('update', True)
478             self.add_policy(service, resource, uplimit, update)
479
480     def add_policy(self, service, resource, uplimit, update=True):
481         """Raises ObjectDoesNotExist, IntegrityError"""
482         resource = Resource.objects.get(service__name=service, name=resource)
483         if update:
484             AstakosUserQuota.objects.update_or_create(user=self,
485                                                       resource=resource,
486                                                       defaults={'uplimit': uplimit})
487         else:
488             q = self.astakosuserquota_set
489             q.create(resource=resource, uplimit=uplimit)
490
491     def remove_policy(self, service, resource):
492         """Raises ObjectDoesNotExist, IntegrityError"""
493         resource = Resource.objects.get(service__name=service, name=resource)
494         q = self.policies.get(resource=resource).delete()
495
496     def update_uuid(self):
497         while not self.uuid:
498             uuid_val =  str(uuid.uuid4())
499             try:
500                 AstakosUser.objects.get(uuid=uuid_val)
501             except AstakosUser.DoesNotExist, e:
502                 self.uuid = uuid_val
503         return self.uuid
504
505     @property
506     def extended_groups(self):
507         return self.membership_set.select_related().all()
508
509     @extended_groups.setter
510     def extended_groups(self, groups):
511         #TODO exceptions
512         for name in (groups or ()):
513             group = AstakosGroup.objects.get(name=name)
514             self.membership_set.create(group=group)
515
516     def save(self, update_timestamps=True, **kwargs):
517         if update_timestamps:
518             if not self.id:
519                 self.date_joined = datetime.now()
520             self.updated = datetime.now()
521
522         # update date_signed_terms if necessary
523         if self.__has_signed_terms != self.has_signed_terms:
524             self.date_signed_terms = datetime.now()
525
526         self.update_uuid()
527
528         if self.username != self.email.lower():
529             # set username
530             self.username = self.email.lower()
531
532         self.validate_unique_email_isactive()
533
534         super(AstakosUser, self).save(**kwargs)
535
536     def renew_token(self, flush_sessions=False, current_key=None):
537         md5 = hashlib.md5()
538         md5.update(settings.SECRET_KEY)
539         md5.update(self.username)
540         md5.update(self.realname.encode('ascii', 'ignore'))
541         md5.update(asctime())
542
543         self.auth_token = b64encode(md5.digest())
544         self.auth_token_created = datetime.now()
545         self.auth_token_expires = self.auth_token_created + \
546                                   timedelta(hours=AUTH_TOKEN_DURATION)
547         if flush_sessions:
548             self.flush_sessions(current_key)
549         msg = 'Token renewed for %s' % self.email
550         logger.log(LOGGING_LEVEL, msg)
551
552     def flush_sessions(self, current_key=None):
553         q = self.sessions
554         if current_key:
555             q = q.exclude(session_key=current_key)
556
557         keys = q.values_list('session_key', flat=True)
558         if keys:
559             msg = 'Flushing sessions: %s' % ','.join(keys)
560             logger.log(LOGGING_LEVEL, msg, [])
561         engine = import_module(settings.SESSION_ENGINE)
562         for k in keys:
563             s = engine.SessionStore(k)
564             s.flush()
565
566     def __unicode__(self):
567         return '%s (%s)' % (self.realname, self.email)
568
569     def conflicting_email(self):
570         q = AstakosUser.objects.exclude(username=self.username)
571         q = q.filter(email__iexact=self.email)
572         if q.count() != 0:
573             return True
574         return False
575
576     def validate_unique_email_isactive(self):
577         """
578         Implements a unique_together constraint for email and is_active fields.
579         """
580         q = AstakosUser.objects.all()
581         q = q.filter(email = self.email)
582         if self.id:
583             q = q.filter(~Q(id = self.id))
584         if q.count() != 0:
585             raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
586
587     def email_change_is_pending(self):
588         return self.emailchanges.count() > 0
589
590     @property
591     def signed_terms(self):
592         term = get_latest_terms()
593         if not term:
594             return True
595         if not self.has_signed_terms:
596             return False
597         if not self.date_signed_terms:
598             return False
599         if self.date_signed_terms < term.date:
600             self.has_signed_terms = False
601             self.date_signed_terms = None
602             self.save()
603             return False
604         return True
605
606     def set_invitations_level(self):
607         """
608         Update user invitation level
609         """
610         level = self.invitation.inviter.level + 1
611         self.level = level
612         self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
613
614     def can_login_with_auth_provider(self, provider):
615         if not self.has_auth_provider(provider):
616             return False
617         else:
618             return auth_providers.get_provider(provider).is_available_for_login()
619
620     def can_add_auth_provider(self, provider, **kwargs):
621         provider_settings = auth_providers.get_provider(provider)
622         if not provider_settings.is_available_for_login():
623             return False
624
625         if self.has_auth_provider(provider) and \
626            provider_settings.one_per_user:
627             return False
628
629         if 'provider_info' in kwargs:
630             kwargs.pop('provider_info')
631
632         if 'identifier' in kwargs:
633             try:
634                 # provider with specified params already exist
635                 existing_user = AstakosUser.objects.get_auth_provider_user(provider,
636                                                                    **kwargs)
637             except AstakosUser.DoesNotExist:
638                 return True
639             else:
640                 return False
641
642         return True
643
644     def can_remove_auth_provider(self, provider):
645         if len(self.get_active_auth_providers()) <= 1:
646             return False
647         return True
648
649     def can_change_password(self):
650         return self.has_auth_provider('local', auth_backend='astakos')
651
652     def has_auth_provider(self, provider, **kwargs):
653         return bool(self.auth_providers.filter(module=provider,
654                                                **kwargs).count())
655
656     def add_auth_provider(self, provider, **kwargs):
657         info_data = ''
658         if 'provider_info' in kwargs:
659             info_data = kwargs.pop('provider_info')
660             if isinstance(info_data, dict):
661                 info_data = json.dumps(info_data)
662
663         if self.can_add_auth_provider(provider, **kwargs):
664             self.auth_providers.create(module=provider, active=True,
665                                        info_data=info_data,
666                                        **kwargs)
667         else:
668             raise Exception('Cannot add provider')
669
670     def add_pending_auth_provider(self, pending):
671         """
672         Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
673         the current user.
674         """
675         if not isinstance(pending, PendingThirdPartyUser):
676             pending = PendingThirdPartyUser.objects.get(token=pending)
677
678         provider = self.add_auth_provider(pending.provider,
679                                identifier=pending.third_party_identifier,
680                                 affiliation=pending.affiliation,
681                                           provider_info=pending.info)
682
683         if email_re.match(pending.email or '') and pending.email != self.email:
684             self.additionalmail_set.get_or_create(email=pending.email)
685
686         pending.delete()
687         return provider
688
689     def remove_auth_provider(self, provider, **kwargs):
690         self.auth_providers.get(module=provider, **kwargs).delete()
691
692     # user urls
693     def get_resend_activation_url(self):
694         return reverse('send_activation', kwargs={'user_id': self.pk})
695
696     def get_provider_remove_url(self, module, **kwargs):
697         return reverse('remove_auth_provider', kwargs={
698             'pk': self.auth_providers.get(module=module, **kwargs).pk})
699
700     def get_activation_url(self, nxt=False):
701         url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
702                                  quote(self.auth_token))
703         if nxt:
704             url += "&next=%s" % quote(nxt)
705         return url
706
707     def get_password_reset_url(self, token_generator=default_token_generator):
708         return reverse('django.contrib.auth.views.password_reset_confirm',
709                           kwargs={'uidb36':int_to_base36(self.id),
710                                   'token':token_generator.make_token(self)})
711
712     def get_auth_providers(self):
713         return self.auth_providers.all()
714
715     def get_available_auth_providers(self):
716         """
717         Returns a list of providers available for user to connect to.
718         """
719         providers = []
720         for module, provider_settings in auth_providers.PROVIDERS.iteritems():
721             if self.can_add_auth_provider(module):
722                 providers.append(provider_settings(self))
723
724         return providers
725
726     def get_active_auth_providers(self):
727         providers = []
728         for provider in self.auth_providers.active():
729             if auth_providers.get_provider(provider.module).is_available_for_login():
730                 providers.append(provider)
731         return providers
732
733     @property
734     def auth_providers_display(self):
735         return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
736
737     def get_inactive_message(self):
738         msg_extra = ''
739         message = ''
740         if self.activation_sent:
741             if self.email_verified:
742                 message = _(astakos_messages.ACCOUNT_INACTIVE)
743             else:
744                 message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
745                 if astakos_settings.MODERATION_ENABLED:
746                     msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
747                 else:
748                     url = self.get_resend_activation_url()
749                     msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
750                                 u' ' + \
751                                 _('<a href="%s">%s?</a>') % (url,
752                                 _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
753         else:
754             if astakos_settings.MODERATION_ENABLED:
755                 message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
756             else:
757                 message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
758                 url = self.get_resend_activation_url()
759                 msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
760                             _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
761
762         return mark_safe(message + u' '+ msg_extra)
763
764
765 class AstakosUserAuthProviderManager(models.Manager):
766
767     def active(self):
768         return self.filter(active=True)
769
770
771 class AstakosUserAuthProvider(models.Model):
772     """
773     Available user authentication methods.
774     """
775     affiliation = models.CharField('Affiliation', max_length=255, blank=True,
776                                    null=True, default=None)
777     user = models.ForeignKey(AstakosUser, related_name='auth_providers')
778     module = models.CharField('Provider', max_length=255, blank=False,
779                                 default='local')
780     identifier = models.CharField('Third-party identifier',
781                                               max_length=255, null=True,
782                                               blank=True)
783     active = models.BooleanField(default=True)
784     auth_backend = models.CharField('Backend', max_length=255, blank=False,
785                                    default='astakos')
786     info_data = models.TextField(default="", null=True, blank=True)
787     created = models.DateTimeField('Creation date', auto_now_add=True)
788
789     objects = AstakosUserAuthProviderManager()
790
791     class Meta:
792         unique_together = (('identifier', 'module', 'user'), )
793         ordering = ('module', 'created')
794
795     def __init__(self, *args, **kwargs):
796         super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
797         try:
798             self.info = json.loads(self.info_data)
799             if not self.info:
800                 self.info = {}
801         except Exception, e:
802             self.info = {}
803
804         for key,value in self.info.iteritems():
805             setattr(self, 'info_%s' % key, value)
806
807
808     @property
809     def settings(self):
810         return auth_providers.get_provider(self.module)
811
812     @property
813     def details_display(self):
814         try:
815           return self.settings.get_details_tpl_display % self.__dict__
816         except:
817           return ''
818
819     @property
820     def title_display(self):
821         title_tpl = self.settings.get_title_display
822         try:
823             if self.settings.get_user_title_display:
824                 title_tpl = self.settings.get_user_title_display
825         except Exception, e:
826             pass
827         try:
828           return title_tpl % self.__dict__
829         except:
830           return self.settings.get_title_display % self.__dict__
831
832     def can_remove(self):
833         return self.user.can_remove_auth_provider(self.module)
834
835     def delete(self, *args, **kwargs):
836         ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
837         if self.module == 'local':
838             self.user.set_unusable_password()
839             self.user.save()
840         return ret
841
842     def __repr__(self):
843         return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
844
845     def __unicode__(self):
846         if self.identifier:
847             return "%s:%s" % (self.module, self.identifier)
848         if self.auth_backend:
849             return "%s:%s" % (self.module, self.auth_backend)
850         return self.module
851
852     def save(self, *args, **kwargs):
853         self.info_data = json.dumps(self.info)
854         return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
855
856
857 class Membership(models.Model):
858     person = models.ForeignKey(AstakosUser)
859     group = models.ForeignKey(AstakosGroup)
860     date_requested = models.DateField(default=datetime.now(), blank=True)
861     date_joined = models.DateField(null=True, db_index=True, blank=True)
862
863     class Meta:
864         unique_together = ("person", "group")
865
866     def save(self, *args, **kwargs):
867         if not self.id:
868             if not self.group.moderation_enabled:
869                 self.date_joined = datetime.now()
870         super(Membership, self).save(*args, **kwargs)
871
872     @property
873     def is_approved(self):
874         if self.date_joined:
875             return True
876         return False
877
878     def approve(self):
879         if self.is_approved:
880             return
881         if self.group.max_participants:
882             assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
883             'Maximum participant number has been reached.'
884         self.date_joined = datetime.now()
885         self.save()
886         quota_disturbed.send(sender=self, users=(self.person,))
887
888     def disapprove(self):
889         self.delete()
890         quota_disturbed.send(sender=self, users=(self.person,))
891
892 class AstakosQuotaManager(models.Manager):
893     def _update_or_create(self, **kwargs):
894         assert kwargs, \
895             'update_or_create() must be passed at least one keyword argument'
896         obj, created = self.get_or_create(**kwargs)
897         defaults = kwargs.pop('defaults', {})
898         if created:
899             return obj, True, False
900         else:
901             try:
902                 params = dict(
903                     [(k, v) for k, v in kwargs.items() if '__' not in k])
904                 params.update(defaults)
905                 for attr, val in params.items():
906                     if hasattr(obj, attr):
907                         setattr(obj, attr, val)
908                 sid = transaction.savepoint()
909                 obj.save(force_update=True)
910                 transaction.savepoint_commit(sid)
911                 return obj, False, True
912             except IntegrityError, e:
913                 transaction.savepoint_rollback(sid)
914                 try:
915                     return self.get(**kwargs), False, False
916                 except self.model.DoesNotExist:
917                     raise e
918
919     update_or_create = _update_or_create
920
921 class AstakosGroupQuota(models.Model):
922     objects = AstakosQuotaManager()
923     limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
924     uplimit = models.BigIntegerField('Up limit', null=True)
925     resource = models.ForeignKey(Resource)
926     group = models.ForeignKey(AstakosGroup, blank=True)
927
928     class Meta:
929         unique_together = ("resource", "group")
930
931 class AstakosUserQuota(models.Model):
932     objects = AstakosQuotaManager()
933     limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
934     uplimit = models.BigIntegerField('Up limit', null=True)
935     resource = models.ForeignKey(Resource)
936     user = models.ForeignKey(AstakosUser)
937
938     class Meta:
939         unique_together = ("resource", "user")
940
941
942 class ApprovalTerms(models.Model):
943     """
944     Model for approval terms
945     """
946
947     date = models.DateTimeField(
948         'Issue date', db_index=True, default=datetime.now())
949     location = models.CharField('Terms location', max_length=255)
950
951
952 class Invitation(models.Model):
953     """
954     Model for registring invitations
955     """
956     inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
957                                 null=True)
958     realname = models.CharField('Real name', max_length=255)
959     username = models.CharField('Unique ID', max_length=255, unique=True)
960     code = models.BigIntegerField('Invitation code', db_index=True)
961     is_consumed = models.BooleanField('Consumed?', default=False)
962     created = models.DateTimeField('Creation date', auto_now_add=True)
963     consumed = models.DateTimeField('Consumption date', null=True, blank=True)
964
965     def __init__(self, *args, **kwargs):
966         super(Invitation, self).__init__(*args, **kwargs)
967         if not self.id:
968             self.code = _generate_invitation_code()
969
970     def consume(self):
971         self.is_consumed = True
972         self.consumed = datetime.now()
973         self.save()
974
975     def __unicode__(self):
976         return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
977
978
979 class EmailChangeManager(models.Manager):
980
981     @transaction.commit_on_success
982     def change_email(self, activation_key):
983         """
984         Validate an activation key and change the corresponding
985         ``User`` if valid.
986
987         If the key is valid and has not expired, return the ``User``
988         after activating.
989
990         If the key is not valid or has expired, return ``None``.
991
992         If the key is valid but the ``User`` is already active,
993         return ``None``.
994
995         After successful email change the activation record is deleted.
996
997         Throws ValueError if there is already
998         """
999         try:
1000             email_change = self.model.objects.get(
1001                 activation_key=activation_key)
1002             if email_change.activation_key_expired():
1003                 email_change.delete()
1004                 raise EmailChange.DoesNotExist
1005             # is there an active user with this address?
1006             try:
1007                 AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1008             except AstakosUser.DoesNotExist:
1009                 pass
1010             else:
1011                 raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
1012             # update user
1013             user = AstakosUser.objects.get(pk=email_change.user_id)
1014             old_email = user.email
1015             user.email = email_change.new_email_address
1016             user.save()
1017             email_change.delete()
1018             msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1019                                                           user.email)
1020             logger.log(LOGGING_LEVEL, msg)
1021             return user
1022         except EmailChange.DoesNotExist:
1023             raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
1024
1025
1026 class EmailChange(models.Model):
1027     new_email_address = models.EmailField(_(u'new e-mail address'),
1028                                           help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
1029     user = models.ForeignKey(
1030         AstakosUser, unique=True, related_name='emailchanges')
1031     requested_at = models.DateTimeField(default=datetime.now())
1032     activation_key = models.CharField(
1033         max_length=40, unique=True, db_index=True)
1034
1035     objects = EmailChangeManager()
1036
1037     def get_url(self):
1038         return reverse('email_change_confirm',
1039                       kwargs={'activation_key': self.activation_key})
1040
1041     def activation_key_expired(self):
1042         expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1043         return self.requested_at + expiration_date < datetime.now()
1044
1045
1046 class AdditionalMail(models.Model):
1047     """
1048     Model for registring invitations
1049     """
1050     owner = models.ForeignKey(AstakosUser)
1051     email = models.EmailField()
1052
1053
1054 def _generate_invitation_code():
1055     while True:
1056         code = randint(1, 2L ** 63 - 1)
1057         try:
1058             Invitation.objects.get(code=code)
1059             # An invitation with this code already exists, try again
1060         except Invitation.DoesNotExist:
1061             return code
1062
1063
1064 def get_latest_terms():
1065     try:
1066         term = ApprovalTerms.objects.order_by('-id')[0]
1067         return term
1068     except IndexError:
1069         pass
1070     return None
1071
1072 class PendingThirdPartyUser(models.Model):
1073     """
1074     Model for registring successful third party user authentications
1075     """
1076     third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
1077     provider = models.CharField('Provider', max_length=255, blank=True)
1078     email = models.EmailField(_('e-mail address'), blank=True, null=True)
1079     first_name = models.CharField(_('first name'), max_length=30, blank=True)
1080     last_name = models.CharField(_('last name'), max_length=30, blank=True)
1081     affiliation = models.CharField('Affiliation', max_length=255, blank=True)
1082     username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1083     token = models.CharField('Token', max_length=255, null=True, blank=True)
1084     created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1085     info = models.TextField(default="", null=True, blank=True)
1086
1087     class Meta:
1088         unique_together = ("provider", "third_party_identifier")
1089
1090     def get_user_instance(self):
1091         d = self.__dict__
1092         d.pop('_state', None)
1093         d.pop('id', None)
1094         d.pop('token', None)
1095         d.pop('created', None)
1096         d.pop('info', None)
1097         user = AstakosUser(**d)
1098
1099         return user
1100
1101     @property
1102     def realname(self):
1103         return '%s %s' %(self.first_name, self.last_name)
1104
1105     @realname.setter
1106     def realname(self, value):
1107         parts = value.split(' ')
1108         if len(parts) == 2:
1109             self.first_name = parts[0]
1110             self.last_name = parts[1]
1111         else:
1112             self.last_name = parts[0]
1113
1114     def save(self, **kwargs):
1115         if not self.id:
1116             # set username
1117             while not self.username:
1118                 username =  uuid.uuid4().hex[:30]
1119                 try:
1120                     AstakosUser.objects.get(username = username)
1121                 except AstakosUser.DoesNotExist, e:
1122                     self.username = username
1123         super(PendingThirdPartyUser, self).save(**kwargs)
1124
1125     def generate_token(self):
1126         self.password = self.third_party_identifier
1127         self.last_login = datetime.now()
1128         self.token = default_token_generator.make_token(self)
1129
1130 class SessionCatalog(models.Model):
1131     session_key = models.CharField(_('session key'), max_length=40)
1132     user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1133
1134
1135 def create_astakos_user(u):
1136     try:
1137         AstakosUser.objects.get(user_ptr=u.pk)
1138     except AstakosUser.DoesNotExist:
1139         extended_user = AstakosUser(user_ptr_id=u.pk)
1140         extended_user.__dict__.update(u.__dict__)
1141         extended_user.save()
1142         if not extended_user.has_auth_provider('local'):
1143             extended_user.add_auth_provider('local')
1144     except BaseException, e:
1145         logger.exception(e)
1146
1147
1148 def fix_superusers(sender, **kwargs):
1149     # Associate superusers with AstakosUser
1150     admins = User.objects.filter(is_superuser=True)
1151     for u in admins:
1152         create_astakos_user(u)
1153
1154
1155 def user_post_save(sender, instance, created, **kwargs):
1156     if not created:
1157         return
1158     create_astakos_user(instance)
1159
1160
1161 def set_default_group(user):
1162     try:
1163         default = AstakosGroup.objects.get(name='default')
1164         Membership(
1165             group=default, person=user, date_joined=datetime.now()).save()
1166     except AstakosGroup.DoesNotExist, e:
1167         logger.exception(e)
1168
1169
1170 def astakosuser_pre_save(sender, instance, **kwargs):
1171     instance.aquarium_report = False
1172     instance.new = False
1173     try:
1174         db_instance = AstakosUser.objects.get(id=instance.id)
1175     except AstakosUser.DoesNotExist:
1176         # create event
1177         instance.aquarium_report = True
1178         instance.new = True
1179     else:
1180         get = AstakosUser.__getattribute__
1181         l = filter(lambda f: get(db_instance, f) != get(instance, f),
1182                    BILLING_FIELDS)
1183         instance.aquarium_report = True if l else False
1184
1185
1186 def astakosuser_post_save(sender, instance, created, **kwargs):
1187     if instance.aquarium_report:
1188         report_user_event(instance, create=instance.new)
1189     if not created:
1190         return
1191     set_default_group(instance)
1192     # TODO handle socket.error & IOError
1193     register_users((instance,))
1194
1195
1196 def resource_post_save(sender, instance, created, **kwargs):
1197     if not created:
1198         return
1199     register_resources((instance,))
1200
1201
1202 def send_quota_disturbed(sender, instance, **kwargs):
1203     users = []
1204     extend = users.extend
1205     if sender == Membership:
1206         if not instance.group.is_enabled:
1207             return
1208         extend([instance.person])
1209     elif sender == AstakosUserQuota:
1210         extend([instance.user])
1211     elif sender == AstakosGroupQuota:
1212         if not instance.group.is_enabled:
1213             return
1214         extend(instance.group.astakosuser_set.all())
1215     elif sender == AstakosGroup:
1216         if not instance.is_enabled:
1217             return
1218     quota_disturbed.send(sender=sender, users=users)
1219
1220
1221 def on_quota_disturbed(sender, users, **kwargs):
1222 #     print '>>>', locals()
1223     if not users:
1224         return
1225     send_quota(users)
1226
1227 def renew_token(sender, instance, **kwargs):
1228     if not instance.auth_token:
1229         instance.renew_token()
1230
1231 post_syncdb.connect(fix_superusers)
1232 post_save.connect(user_post_save, sender=User)
1233 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1234 post_save.connect(astakosuser_post_save, sender=AstakosUser)
1235 post_save.connect(resource_post_save, sender=Resource)
1236
1237 quota_disturbed = Signal(providing_args=["users"])
1238 quota_disturbed.connect(on_quota_disturbed)
1239
1240 post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1241 post_delete.connect(send_quota_disturbed, sender=Membership)
1242 post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1243 post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1244 post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1245 post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1246
1247 pre_save.connect(renew_token, sender=AstakosUser)
1248 pre_save.connect(renew_token, sender=Service)