123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468 |
- # -*- coding: utf-8 -*-
- from __future__ import unicode_literals
- import ldapdb.models
- import unicodedata
- import datetime
- from django.db import models
- from django.db.models import Q
- from django.db.models.signals import pre_save
- from django.utils.translation import ugettext_lazy as _
- from django.utils.text import capfirst
- from django.dispatch import receiver
- from django.contrib.auth.models import AbstractUser
- from django.conf import settings
- from django.core.validators import RegexValidator
- from django.core.exceptions import ValidationError
- from ldapdb.models.fields import CharField, IntegerField, ListField
- from coin.offers.models import OfferSubscription
- from coin.mixins import CoinLdapSyncMixin
- from coin import utils
- class Member(CoinLdapSyncMixin, AbstractUser):
- # USERNAME_FIELD = 'login'
- REQUIRED_FIELDS = ['first_name', 'last_name', 'email', ]
- MEMBER_TYPE_CHOICES = (
- ('natural_person', _("Individual")),
- ('legal_entity', _("Organisation")),
- )
- MEMBER_STATUS_CHOICES = (
- ('member', _("Member")),
- ('not_member', _("Not a member")),
- ('pending', _("Pending membership")),
- )
- status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
- default='member', verbose_name=_('status'))
- type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
- default='natural_person', verbose_name=_("type"))
- nickname = models.CharField(max_length=64, blank=True,
- verbose_name=_("nickname"),
- help_text=_("Pseudonym, …"))
- organization_name = models.CharField(max_length=200, blank=True,
- verbose_name=_("organisation name"),
- help_text=_("For organisations"))
- home_phone_number = models.CharField(max_length=25, blank=True,
- verbose_name=_("home phone number"))
- mobile_phone_number = models.CharField(max_length=25, blank=True,
- verbose_name=_("mobile phone number"))
- # TODO: use a django module that provides an address model? (would
- # support more countries and address types)
- address = models.TextField(
- verbose_name=_("physical address"), blank=True, null=True)
- validator = RegexValidator(regex=r'^\d{5}$',
- message=_("Invalid postal code."))
- postal_code = models.CharField(max_length=5, blank=True, null=True,
- validators=[validator],
- verbose_name=_("postal code"))
- city = models.CharField(max_length=200, blank=True, null=True,
- verbose_name=_("city"))
- country = models.CharField(max_length=200, blank=True, null=True,
- default='France',
- verbose_name=_("country"))
- resign_date = models.DateField(null=True, blank=True,
- verbose_name=_("resign date of membership"),
- help_text=_("In case of premature leaving"))
- comments = models.TextField(blank=True, verbose_name=_("comments"),
- help_text=_("Free-form comments (specific"
- " membership information, reason"
- " for leaving, etc)"))
- # Following fields are managed by the parent class AbstractUser :
- # username, first_name, last_name, email
- # However we hack the model to force theses fields to be required. (see
- # below)
- # This property is used to change password in LDAP. Used in sync_to_ldap.
- # Should not be defined manually. Prefer use set_password method that hash
- # passwords for both ldap and local db
- _password_ldap = None
- def clean(self):
- from django.utils.translation import ugettext as _
- if self.type == 'legal_entity':
- if not self.organization_name:
- raise ValidationError(_("An organisation name is mandatory for"
- " organisations."))
- elif self.type == 'natural_person':
- if not (self.first_name and self.last_name):
- raise ValidationError(_("First name and last name are mandatory"
- " for individuals."))
- def __unicode__(self):
- from django.utils.translation import ugettext as _
- if self.type == 'legal_entity':
- return self.organization_name
- elif self.nickname:
- return self.nickname
- else:
- return _("{first_name} {last_name}").format(
- first_name=self.first_name,
- last_name=self.last_name)
- def get_full_name(self):
- return str(self)
- def get_short_name(self):
- return self.username
- # Renvoie la date de fin de la dernière cotisation du membre
- def end_date_of_membership(self):
- x = self.membership_fees.order_by('-end_date')
- if x:
- return self.membership_fees.order_by('-end_date')[0].end_date
- end_date_of_membership.short_description = _("End date of membership")
- def is_paid_up(self):
- """
- True si le membre est à jour de cotisation. False sinon
- """
- if self.end_date_of_membership() \
- and self.end_date_of_membership() >= datetime.date.today():
- return True
- else:
- return False
- def set_password(self, new_password, *args, **kwargs):
- """
- Définit le mot de passe a sauvegarder en base et dans le LDAP
- """
- super(Member, self).set_password(new_password, *args, **kwargs)
- self._password_ldap = utils.ldap_hash(new_password)
- def get_active_subscriptions(self, date=None):
- """
- Return list of OfferSubscription which are active today
- """
- if date is None:
- date = datetime.date.today()
- return OfferSubscription.objects.filter(
- Q(member__exact=self.pk),
- Q(subscription_date__lte=date),
- Q(resign_date__isnull=True) | Q(resign_date__gte=date))
- def get_inactive_subscriptions(self, date=None):
- """
- Return list of OfferSubscription which are not active today
- """
- if date is None:
- date = datetime.date.today()
- return OfferSubscription.objects.filter(
- Q(member__exact=self.pk),
- Q(subscription_date__gt=date) |
- Q(resign_date__lt=date))
- def get_ssh_keys(self):
- # Quick & dirty, ensure that keys are unique (otherwise, LDAP complains)
- return list({k.key for k in self.cryptokey_set.filter(type='RSA')})
- def sync_ssh_keys(self):
- """
- Called whenever a SSH key is saved
- """
- ldap_user = LdapUser.objects.get(pk=self.username)
- ldap_user.sshPublicKey = self.get_ssh_keys()
- ldap_user.save()
- def sync_to_ldap(self, creation, update_fields, *args, **kwargs):
- """
- Update LDAP data when a member is saved
- """
- from django.utils.translation import ugettext as _
- # Do not perform LDAP query if no usefull fields to update are specified
- # in update_fields
- # Ex : at login, last_login field is updated by django auth module.
- if update_fields and set(['username', 'last_name', 'first_name']).isdisjoint(set(update_fields)):
- return
- # Fail if no username specified
- assert self.username, _("Can't sync with LDAP because missing username "
- "value for Member <{name}>").format(name=str(self))
- # If try to sync a superuser in creation mode
- # Try to retrieve the user in ldap. If exists, switch to update mode
- # This allow to create a superuser without having to delete corresponding
- # username in LDAP
- if self.is_superuser and creation:
- try:
- ldap_user = LdapUser.objects.get(pk=self.username)
- creation = False
- except LdapUser.DoesNotExist:
- pass
- if not creation:
- ldap_user = LdapUser.objects.get(pk=self.username)
- if creation:
- users = LdapUser.objects
- if users.exists():
- uid_number = users.order_by('-uidNumber')[0].uidNumber + 1
- else:
- uid_number = settings.LDAP_USER_FIRST_UID
- ldap_user = LdapUser()
- ldap_user.pk = self.username
- ldap_user.uid = self.username
- ldap_user.nick_name = self.username
- ldap_user.uidNumber = uid_number
- ldap_user.homeDirectory = '/home/' + self.username
- if self.type == 'natural_person':
- ldap_user.last_name = self.last_name
- ldap_user.first_name = self.first_name
- elif self.type == 'legal_entity':
- ldap_user.last_name = self.organization_name
- ldap_user.first_name = ""
- # If a password is definied in _password_ldap, change it in LDAP
- if self._password_ldap:
- # Make sure password is hashed
- ldap_user.password = utils.ldap_hash(self._password_ldap)
- # Store SSH keys
- ldap_user.sshPublicKey = self.get_ssh_keys()
- ldap_user.save()
- # if creation:
- # ldap_group = LdapGroup.objects.get(pk='coin')
- # ldap_group.members.append(ldap_user.pk)
- # ldap_group.save()
- def delete_from_ldap(self):
- """
- Delete member from the LDAP
- """
- from django.utils.translation import ugettext as _
- assert self.username, _("Can't delete from LDAP because missing username "
- "value for Member <{name}>").format(name=str(self))
- # Delete user from LDAP
- ldap_user = LdapUser.objects.get(pk=self.username)
- ldap_user.delete()
- # Lorsqu'un membre est supprimé du SI, son utilisateur LDAP
- # correspondant est sorti du groupe "coin" afin qu'il n'ait plus
- # accès au SI
- # ldap_group = LdapGroup.objects.get(pk='coin')
- # if self.username in ldap_group.members:
- # ldap_group.members.remove(self.username)
- # ldap_group.save()
- def send_welcome_email(self):
- """ Envoie le courriel de bienvenue à ce membre """
- from coin.isp_database.models import ISPInfo
- utils.send_templated_email(to=self.email,
- subject_template='members/emails/welcome_email_subject.txt',
- body_template='members/emails/welcome_email.html',
- context={'member': self, 'branding':ISPInfo.objects.first()})
- class Meta:
- verbose_name = _('member')
- verbose_name_plural = _('members')
- # Hack to force email to be required by Member model
- Member._meta.get_field('email')._unique = True
- Member._meta.get_field('email').blank = False
- Member._meta.get_field('email').null = False
- def count_active_members():
- return Member.objects.filter(status='member').count()
- def get_automatic_username(member):
- """
- Calcul le username automatiquement en fonction
- du nom et du prénom
- """
- # S'il s'agit d'une entreprise, utilise son nom:
- if member.type == 'legal_entity' and member.organization_name:
- username = member.organization_name
- # Sinon, si un pseudo est définit, l'utilise
- elif member.nickname:
- username = member.nickname
- # Sinon, utilise nom et prenom
- elif member.first_name and member.last_name:
- # Première lettre de chaque partie du prénom
- first_name_letters = ''.join(
- [c[0] for c in member.first_name.split('-')]
- )
- # Concaténer avec nom de famille
- username = ('%s%s' % (first_name_letters, member.last_name))
- else:
- raise Exception(_("Not enough information provided to generate "
- "a login for the member."))
- # Remplacer ou enlever les caractères non ascii
- username = unicodedata.normalize('NFD', username)\
- .encode('ascii', 'ignore')
- # Enlever ponctuation (sauf _-.) et espace
- punctuation = ('!"#$%&\'()*+,/:;<=>?@[\\]^`{|}~ ').encode('ascii')
- username = username.translate(None, punctuation)
- # En minuscule
- username = username.lower()
- # Maximum de 30 char
- username = username[:30]
- # Recherche dans les membres existants un username identique
- member = Member.objects.filter(username=username)
- base_username = username
- incr = 2
- # Tant qu'un membre est trouvé, incrémente un entier à la fin
- while member:
- if len(base_username) >= 30:
- username = base_username[30 - len(str(incr)):]
- else:
- username = base_username
- username = username + str(incr)
- member = Member.objects.filter(username=username)
- incr += 1
- return username
- class CryptoKey(CoinLdapSyncMixin, models.Model):
- KEY_TYPE_CHOICES = (('RSA', _('RSA')), ('GPG', _('GPG')))
- type = models.CharField(max_length=3, choices=KEY_TYPE_CHOICES,
- verbose_name=_('type'))
- key = models.TextField(verbose_name=_('key'))
- member = models.ForeignKey('Member', verbose_name=_('member'))
- def sync_to_ldap(self, creation, *args, **kwargs):
- """Simply tell the member object to resync all its SSH keys to LDAP"""
- self.member.sync_ssh_keys()
- def delete_from_ldap(self, *args, **kwargs):
- self.member.sync_ssh_keys()
- def __unicode__(self):
- from django.utils.translation import ugettext as _
- readable_type = capfirst(self.get_type_display())
- return _('{type} key of {user}').format(type=readable_type,
- user=self.member)
- class Meta:
- verbose_name = _('key')
- verbose_name_plural = _('keys')
- class MembershipFee(models.Model):
- PAYMENT_METHOD_CHOICES = (
- ('cash', _('Cash')),
- ('check', _('Check')),
- ('transfer', _('Transfer')),
- ('other', _('Other'))
- )
- member = models.ForeignKey('Member', related_name='membership_fees',
- verbose_name=_('member'))
- amount = models.DecimalField(null=False, max_digits=5, decimal_places=2,
- default=settings.MEMBER_DEFAULT_COTISATION,
- verbose_name=_('amount'),
- help_text=_('in €'))
- start_date = models.DateField(
- null=False,
- blank=False,
- verbose_name=_('start date of membership fee'))
- end_date = models.DateField(
- null=False,
- blank=True,
- verbose_name=_('end date of membership fee'),
- help_text=_('By default, a membership fee covers one year'))
- payment_method = models.CharField(max_length=100, null=True, blank=True,
- choices=PAYMENT_METHOD_CHOICES,
- verbose_name=_('payment method'))
- reference = models.CharField(max_length=125, null=True, blank=True,
- verbose_name=_('payment reference'),
- help_text=_('Check number, '
- 'reference of bank transfer, '
- 'comment…'))
- payment_date = models.DateField(null=True, blank=True,
- verbose_name=_('date of payment'))
- def clean(self):
- if self.end_date is None:
- self.end_date = self.start_date + datetime.timedelta(364)
- def __unicode__(self):
- return '%s - %s - %i€' % (self.member, self.start_date, self.amount)
- class Meta:
- verbose_name = _('membership fee')
- verbose_name_plural = _('membership fees')
- class LdapUser(ldapdb.models.Model):
- # "ou=users,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
- base_dn = settings.LDAP_USER_BASE_DN
- object_classes = [b'inetOrgPerson', b'organizationalPerson', b'person',
- b'top', b'posixAccount', b'ldapPublicKey']
- uid = CharField(db_column=b'uid', unique=True, max_length=255)
- nick_name = CharField(db_column=b'cn', unique=True, primary_key=True,
- max_length=255)
- first_name = CharField(db_column=b'givenName', max_length=255)
- last_name = CharField(db_column=b'sn', max_length=255)
- display_name = CharField(db_column=b'displayName', max_length=255,
- blank=True)
- password = CharField(db_column=b'userPassword', max_length=255)
- uidNumber = IntegerField(db_column=b'uidNumber', unique=True)
- gidNumber = IntegerField(db_column=b'gidNumber', default=2000)
- homeDirectory = CharField(db_column=b'homeDirectory', max_length=255,
- default='/tmp')
- loginShell = CharField(db_column=b'loginShell', max_length=255,
- default='/bin/bash')
- sshPublicKey = ListField(db_column=b'sshPublicKey', default=[])
- def __unicode__(self):
- return self.display_name
- class Meta:
- managed = False # Indique à Django de ne pas intégrer ce model en base
- # class LdapGroup(ldapdb.models.Model):
- # "ou=groups,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
- # base_dn = settings.LDAP_GROUP_BASE_DN
- # object_classes = [b'posixGroup']
- # gid = IntegerField(db_column=b'gidNumber', unique=True)
- # name = CharField(db_column=b'cn', max_length=200, primary_key=True)
- # members = ListField(db_column=b'memberUid')
- # def __unicode__(self):
- # return self.name
- # class Meta:
- # managed = False # Indique à Django de ne pas intégrer ce model en base
- @receiver(pre_save, sender=Member)
- def define_username(sender, instance, **kwargs):
- """
- Lors de la sauvegarde d'un membre. Si le champ username n'est pas définit,
- le calcul automatiquement en fonction du nom et du prénom
- """
- if not instance.username and not instance.pk:
- instance.username = get_automatic_username(instance)
- @receiver(pre_save, sender=LdapUser)
- def define_display_name(sender, instance, **kwargs):
- """
- Lors de la sauvegarde d'un utilisateur Ldap, le champ display_name est la
- concaténation de first_name et last_name
- """
- if not instance.display_name:
- instance.display_name = '%s %s' % (instance.first_name,
- instance.last_name)
|