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