Update snf-manage commands in email templates. Fix authentication token renewal.
[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             self.username = self.email
502         
503         self.validate_unique_email_isactive()
504         if self.is_active and self.activation_sent:
505             # reset the activation sent
506             self.activation_sent = None
507
508         super(AstakosUser, self).save(**kwargs)
509
510     def renew_token(self, flush_sessions=False, current_key=None):
511         md5 = hashlib.md5()
512         md5.update(settings.SECRET_KEY)
513         md5.update(self.username)
514         md5.update(self.realname.encode('ascii', 'ignore'))
515         md5.update(asctime())
516
517         self.auth_token = b64encode(md5.digest())
518         self.auth_token_created = datetime.now()
519         self.auth_token_expires = self.auth_token_created + \
520                                   timedelta(hours=AUTH_TOKEN_DURATION)
521         if flush_sessions:
522             self.flush_sessions(current_key)
523         msg = 'Token renewed for %s' % self.email
524         logger.log(LOGGING_LEVEL, msg)
525
526     def flush_sessions(self, current_key=None):
527         q = self.sessions
528         if current_key:
529             q = q.exclude(session_key=current_key)
530
531         keys = q.values_list('session_key', flat=True)
532         if keys:
533             msg = 'Flushing sessions: %s' % ','.join(keys)
534             logger.log(LOGGING_LEVEL, msg, [])
535         engine = import_module(settings.SESSION_ENGINE)
536         for k in keys:
537             s = engine.SessionStore(k)
538             s.flush()
539
540     def __unicode__(self):
541         return '%s (%s)' % (self.realname, self.email)
542
543     def conflicting_email(self):
544         q = AstakosUser.objects.exclude(username=self.username)
545         q = q.filter(email__iexact=self.email)
546         if q.count() != 0:
547             return True
548         return False
549
550     def validate_unique_email_isactive(self):
551         """
552         Implements a unique_together constraint for email and is_active fields.
553         """
554         q = AstakosUser.objects.all()
555         q = q.filter(email = self.email)
556         q = q.filter(is_active = self.is_active)
557         if self.id:
558             q = q.filter(~Q(id = self.id))
559         if q.count() != 0:
560             raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
561
562     @property
563     def signed_terms(self):
564         term = get_latest_terms()
565         if not term:
566             return True
567         if not self.has_signed_terms:
568             return False
569         if not self.date_signed_terms:
570             return False
571         if self.date_signed_terms < term.date:
572             self.has_signed_terms = False
573             self.date_signed_terms = None
574             self.save()
575             return False
576         return True
577
578     def set_invitations_level(self):
579         """
580         Update user invitation level
581         """
582         level = self.invitation.inviter.level + 1
583         self.level = level
584         self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
585
586     def can_login_with_auth_provider(self, provider):
587         if not self.has_auth_provider(provider):
588             return False
589         else:
590             return auth_providers.get_provider(provider).is_available_for_login()
591
592     def can_add_auth_provider(self, provider, **kwargs):
593         provider_settings = auth_providers.get_provider(provider)
594         if not provider_settings.is_available_for_login():
595             return False
596
597         if self.has_auth_provider(provider) and \
598            provider_settings.one_per_user:
599             return False
600
601         if 'identifier' in kwargs:
602             try:
603                 # provider with specified params already exist
604                 existing_user = AstakosUser.objects.get_auth_provider_user(provider,
605                                                                    **kwargs)
606             except AstakosUser.DoesNotExist:
607                 return True
608             else:
609                 return False
610
611         return True
612
613     def can_remove_auth_provider(self, provider):
614         if len(self.get_active_auth_providers()) <= 1:
615             return False
616         return True
617
618     def can_change_password(self):
619         return self.has_auth_provider('local', auth_backend='astakos')
620
621     def has_auth_provider(self, provider, **kwargs):
622         return bool(self.auth_providers.filter(module=provider,
623                                                **kwargs).count())
624
625     def add_auth_provider(self, provider, **kwargs):
626         if self.can_add_auth_provider(provider, **kwargs):
627             self.auth_providers.create(module=provider, active=True, **kwargs)
628         else:
629             raise Exception('Cannot add provider')
630
631     def add_pending_auth_provider(self, pending):
632         """
633         Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
634         the current user.
635         """
636         if not isinstance(pending, PendingThirdPartyUser):
637             pending = PendingThirdPartyUser.objects.get(token=pending)
638
639         provider = self.add_auth_provider(pending.provider,
640                                identifier=pending.third_party_identifier)
641
642         if email_re.match(pending.email) and pending.email != self.email:
643             self.additionalmail_set.get_or_create(email=pending.email)
644
645         pending.delete()
646         return provider
647
648     def remove_auth_provider(self, provider, **kwargs):
649         self.auth_providers.get(module=provider, **kwargs).delete()
650
651     # user urls
652     def get_resend_activation_url(self):
653         return reverse('send_activation', {'user_id': self.pk})
654
655     def get_activation_url(self, nxt=False):
656         url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
657                                  quote(self.auth_token))
658         if nxt:
659             url += "&next=%s" % quote(nxt)
660         return url
661
662     def get_password_reset_url(self, token_generator=default_token_generator):
663         return reverse('django.contrib.auth.views.password_reset_confirm',
664                           kwargs={'uidb36':int_to_base36(self.id),
665                                   'token':token_generator.make_token(self)})
666
667     def get_auth_providers(self):
668         return self.auth_providers.all()
669
670     def get_available_auth_providers(self):
671         """
672         Returns a list of providers available for user to connect to.
673         """
674         providers = []
675         for module, provider_settings in auth_providers.PROVIDERS.iteritems():
676             if self.can_add_auth_provider(module):
677                 providers.append(provider_settings(self))
678
679         return providers
680
681     def get_active_auth_providers(self):
682         providers = []
683         for provider in self.auth_providers.active():
684             if auth_providers.get_provider(provider.module).is_available_for_login():
685                 providers.append(provider)
686         return providers
687
688     @property
689     def auth_providers_display(self):
690         return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
691
692
693 class AstakosUserAuthProviderManager(models.Manager):
694
695     def active(self):
696         return self.filter(active=True)
697
698
699 class AstakosUserAuthProvider(models.Model):
700     """
701     Available user authentication methods.
702     """
703     affiliation = models.CharField('Affiliation', max_length=255, blank=True,
704                                    null=True, default=None)
705     user = models.ForeignKey(AstakosUser, related_name='auth_providers')
706     module = models.CharField('Provider', max_length=255, blank=False,
707                                 default='local')
708     identifier = models.CharField('Third-party identifier',
709                                               max_length=255, null=True,
710                                               blank=True)
711     active = models.BooleanField(default=True)
712     auth_backend = models.CharField('Backend', max_length=255, blank=False,
713                                    default='astakos')
714
715     objects = AstakosUserAuthProviderManager()
716
717     class Meta:
718         unique_together = (('identifier', 'module', 'user'), )
719
720     @property
721     def settings(self):
722         return auth_providers.get_provider(self.module)
723
724     @property
725     def details_display(self):
726         return self.settings.details_tpl % self.__dict__
727
728     def can_remove(self):
729         return self.user.can_remove_auth_provider(self.module)
730
731     def delete(self, *args, **kwargs):
732         ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
733         if self.module == 'local':
734             self.user.set_unusable_password()
735             self.user.save()
736         return ret
737
738     def __repr__(self):
739         return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
740
741     def __unicode__(self):
742         if self.identifier:
743             return "%s:%s" % (self.module, self.identifier)
744         if self.auth_backend:
745             return "%s:%s" % (self.module, self.auth_backend)
746         return self.module
747
748
749
750 class Membership(models.Model):
751     person = models.ForeignKey(AstakosUser)
752     group = models.ForeignKey(AstakosGroup)
753     date_requested = models.DateField(default=datetime.now(), blank=True)
754     date_joined = models.DateField(null=True, db_index=True, blank=True)
755
756     class Meta:
757         unique_together = ("person", "group")
758
759     def save(self, *args, **kwargs):
760         if not self.id:
761             if not self.group.moderation_enabled:
762                 self.date_joined = datetime.now()
763         super(Membership, self).save(*args, **kwargs)
764
765     @property
766     def is_approved(self):
767         if self.date_joined:
768             return True
769         return False
770
771     def approve(self):
772         if self.is_approved:
773             return
774         if self.group.max_participants:
775             assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
776             'Maximum participant number has been reached.'
777         self.date_joined = datetime.now()
778         self.save()
779         quota_disturbed.send(sender=self, users=(self.person,))
780
781     def disapprove(self):
782         self.delete()
783         quota_disturbed.send(sender=self, users=(self.person,))
784
785 class AstakosQuotaManager(models.Manager):
786     def _update_or_create(self, **kwargs):
787         assert kwargs, \
788             'update_or_create() must be passed at least one keyword argument'
789         obj, created = self.get_or_create(**kwargs)
790         defaults = kwargs.pop('defaults', {})
791         if created:
792             return obj, True, False
793         else:
794             try:
795                 params = dict(
796                     [(k, v) for k, v in kwargs.items() if '__' not in k])
797                 params.update(defaults)
798                 for attr, val in params.items():
799                     if hasattr(obj, attr):
800                         setattr(obj, attr, val)
801                 sid = transaction.savepoint()
802                 obj.save(force_update=True)
803                 transaction.savepoint_commit(sid)
804                 return obj, False, True
805             except IntegrityError, e:
806                 transaction.savepoint_rollback(sid)
807                 try:
808                     return self.get(**kwargs), False, False
809                 except self.model.DoesNotExist:
810                     raise e
811
812     update_or_create = _update_or_create
813
814 class AstakosGroupQuota(models.Model):
815     objects = AstakosQuotaManager()
816     limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
817     uplimit = models.BigIntegerField('Up limit', null=True)
818     resource = models.ForeignKey(Resource)
819     group = models.ForeignKey(AstakosGroup, blank=True)
820
821     class Meta:
822         unique_together = ("resource", "group")
823
824 class AstakosUserQuota(models.Model):
825     objects = AstakosQuotaManager()
826     limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
827     uplimit = models.BigIntegerField('Up limit', null=True)
828     resource = models.ForeignKey(Resource)
829     user = models.ForeignKey(AstakosUser)
830
831     class Meta:
832         unique_together = ("resource", "user")
833
834
835 class ApprovalTerms(models.Model):
836     """
837     Model for approval terms
838     """
839
840     date = models.DateTimeField(
841         'Issue date', db_index=True, default=datetime.now())
842     location = models.CharField('Terms location', max_length=255)
843
844
845 class Invitation(models.Model):
846     """
847     Model for registring invitations
848     """
849     inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
850                                 null=True)
851     realname = models.CharField('Real name', max_length=255)
852     username = models.CharField('Unique ID', max_length=255, unique=True)
853     code = models.BigIntegerField('Invitation code', db_index=True)
854     is_consumed = models.BooleanField('Consumed?', default=False)
855     created = models.DateTimeField('Creation date', auto_now_add=True)
856     consumed = models.DateTimeField('Consumption date', null=True, blank=True)
857
858     def __init__(self, *args, **kwargs):
859         super(Invitation, self).__init__(*args, **kwargs)
860         if not self.id:
861             self.code = _generate_invitation_code()
862
863     def consume(self):
864         self.is_consumed = True
865         self.consumed = datetime.now()
866         self.save()
867
868     def __unicode__(self):
869         return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
870
871
872 class EmailChangeManager(models.Manager):
873     @transaction.commit_on_success
874     def change_email(self, activation_key):
875         """
876         Validate an activation key and change the corresponding
877         ``User`` if valid.
878
879         If the key is valid and has not expired, return the ``User``
880         after activating.
881
882         If the key is not valid or has expired, return ``None``.
883
884         If the key is valid but the ``User`` is already active,
885         return ``None``.
886
887         After successful email change the activation record is deleted.
888
889         Throws ValueError if there is already
890         """
891         try:
892             email_change = self.model.objects.get(
893                 activation_key=activation_key)
894             if email_change.activation_key_expired():
895                 email_change.delete()
896                 raise EmailChange.DoesNotExist
897             # is there an active user with this address?
898             try:
899                 AstakosUser.objects.get(email__iexact=email_change.new_email_address)
900             except AstakosUser.DoesNotExist:
901                 pass
902             else:
903                 raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
904             # update user
905             user = AstakosUser.objects.get(pk=email_change.user_id)
906             user.email = email_change.new_email_address
907             user.save()
908             email_change.delete()
909             return user
910         except EmailChange.DoesNotExist:
911             raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
912
913
914 class EmailChange(models.Model):
915     new_email_address = models.EmailField(_(u'new e-mail address'),
916                                           help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
917     user = models.ForeignKey(
918         AstakosUser, unique=True, related_name='emailchange_user')
919     requested_at = models.DateTimeField(default=datetime.now())
920     activation_key = models.CharField(
921         max_length=40, unique=True, db_index=True)
922
923     objects = EmailChangeManager()
924
925     def activation_key_expired(self):
926         expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
927         return self.requested_at + expiration_date < datetime.now()
928
929
930 class AdditionalMail(models.Model):
931     """
932     Model for registring invitations
933     """
934     owner = models.ForeignKey(AstakosUser)
935     email = models.EmailField()
936
937
938 def _generate_invitation_code():
939     while True:
940         code = randint(1, 2L ** 63 - 1)
941         try:
942             Invitation.objects.get(code=code)
943             # An invitation with this code already exists, try again
944         except Invitation.DoesNotExist:
945             return code
946
947
948 def get_latest_terms():
949     try:
950         term = ApprovalTerms.objects.order_by('-id')[0]
951         return term
952     except IndexError:
953         pass
954     return None
955
956 class PendingThirdPartyUser(models.Model):
957     """
958     Model for registring successful third party user authentications
959     """
960     third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
961     provider = models.CharField('Provider', max_length=255, blank=True)
962     email = models.EmailField(_('e-mail address'), blank=True, null=True)
963     first_name = models.CharField(_('first name'), max_length=30, blank=True)
964     last_name = models.CharField(_('last name'), max_length=30, blank=True)
965     affiliation = models.CharField('Affiliation', max_length=255, blank=True)
966     username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
967     token = models.CharField('Token', max_length=255, null=True, blank=True)
968     created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
969
970     class Meta:
971         unique_together = ("provider", "third_party_identifier")
972
973     @property
974     def realname(self):
975         return '%s %s' %(self.first_name, self.last_name)
976
977     @realname.setter
978     def realname(self, value):
979         parts = value.split(' ')
980         if len(parts) == 2:
981             self.first_name = parts[0]
982             self.last_name = parts[1]
983         else:
984             self.last_name = parts[0]
985
986     def save(self, **kwargs):
987         if not self.id:
988             # set username
989             while not self.username:
990                 username =  uuid.uuid4().hex[:30]
991                 try:
992                     AstakosUser.objects.get(username = username)
993                 except AstakosUser.DoesNotExist, e:
994                     self.username = username
995         super(PendingThirdPartyUser, self).save(**kwargs)
996
997     def generate_token(self):
998         self.password = self.third_party_identifier
999         self.last_login = datetime.now()
1000         self.token = default_token_generator.make_token(self)
1001
1002 class SessionCatalog(models.Model):
1003     session_key = models.CharField(_('session key'), max_length=40)
1004     user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1005
1006
1007 def create_astakos_user(u):
1008     try:
1009         AstakosUser.objects.get(user_ptr=u.pk)
1010     except AstakosUser.DoesNotExist:
1011         extended_user = AstakosUser(user_ptr_id=u.pk)
1012         extended_user.__dict__.update(u.__dict__)
1013         extended_user.save()
1014     except BaseException, e:
1015         logger.exception(e)
1016
1017
1018 def fix_superusers(sender, **kwargs):
1019     # Associate superusers with AstakosUser
1020     admins = User.objects.filter(is_superuser=True)
1021     for u in admins:
1022         create_astakos_user(u)
1023
1024
1025 def user_post_save(sender, instance, created, **kwargs):
1026     if not created:
1027         return
1028     create_astakos_user(instance)
1029
1030
1031 def set_default_group(user):
1032     try:
1033         default = AstakosGroup.objects.get(name='default')
1034         Membership(
1035             group=default, person=user, date_joined=datetime.now()).save()
1036     except AstakosGroup.DoesNotExist, e:
1037         logger.exception(e)
1038
1039
1040 def astakosuser_pre_save(sender, instance, **kwargs):
1041     instance.aquarium_report = False
1042     instance.new = False
1043     try:
1044         db_instance = AstakosUser.objects.get(id=instance.id)
1045     except AstakosUser.DoesNotExist:
1046         # create event
1047         instance.aquarium_report = True
1048         instance.new = True
1049     else:
1050         get = AstakosUser.__getattribute__
1051         l = filter(lambda f: get(db_instance, f) != get(instance, f),
1052                    BILLING_FIELDS)
1053         instance.aquarium_report = True if l else False
1054
1055
1056 def astakosuser_post_save(sender, instance, created, **kwargs):
1057     if instance.aquarium_report:
1058         report_user_event(instance, create=instance.new)
1059     if not created:
1060         return
1061     set_default_group(instance)
1062     # TODO handle socket.error & IOError
1063     register_users((instance,))
1064
1065
1066 def resource_post_save(sender, instance, created, **kwargs):
1067     if not created:
1068         return
1069     register_resources((instance,))
1070
1071
1072 def send_quota_disturbed(sender, instance, **kwargs):
1073     users = []
1074     extend = users.extend
1075     if sender == Membership:
1076         if not instance.group.is_enabled:
1077             return
1078         extend([instance.person])
1079     elif sender == AstakosUserQuota:
1080         extend([instance.user])
1081     elif sender == AstakosGroupQuota:
1082         if not instance.group.is_enabled:
1083             return
1084         extend(instance.group.astakosuser_set.all())
1085     elif sender == AstakosGroup:
1086         if not instance.is_enabled:
1087             return
1088     quota_disturbed.send(sender=sender, users=users)
1089
1090
1091 def on_quota_disturbed(sender, users, **kwargs):
1092 #     print '>>>', locals()
1093     if not users:
1094         return
1095     send_quota(users)
1096
1097 def renew_token(sender, instance, **kwargs):
1098     if not instance.auth_token:
1099         instance.renew_token()
1100
1101 post_syncdb.connect(fix_superusers)
1102 post_save.connect(user_post_save, sender=User)
1103 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1104 post_save.connect(astakosuser_post_save, sender=AstakosUser)
1105 post_save.connect(resource_post_save, sender=Resource)
1106
1107 quota_disturbed = Signal(providing_args=["users"])
1108 quota_disturbed.connect(on_quota_disturbed)
1109
1110 post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1111 post_delete.connect(send_quota_disturbed, sender=Membership)
1112 post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1113 post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1114 post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1115 post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1116
1117 pre_save.connect(renew_token, sender=AstakosUser)
1118 pre_save.connect(renew_token, sender=Service)