models.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. # -*- coding: utf-8 -*-
  2. import ldapdb.models
  3. import unicodedata
  4. import string
  5. import datetime
  6. from django.db import models
  7. from django.db.models import Q
  8. from django.db.models.signals import pre_save
  9. from django.dispatch import receiver
  10. from django.contrib.auth.models import AbstractUser
  11. from ldapdb.models.fields import CharField, IntegerField, ListField
  12. from south.modelsinspector import add_ignored_fields
  13. from coin.offers.models import OfferSubscription
  14. from coin.mixins import CoinLdapSyncMixin
  15. from coin import utils
  16. from django.contrib.auth.signals import user_logged_in
  17. from django.conf import settings
  18. class Member(CoinLdapSyncMixin, AbstractUser):
  19. # USERNAME_FIELD = 'login'
  20. REQUIRED_FIELDS = ['first_name', 'last_name', 'email', ]
  21. MEMBER_TYPE_CHOICES = (
  22. ('natural_person', 'Personne physique'),
  23. ('legal_entity', 'Personne morale'),
  24. )
  25. MEMBER_STATUS_CHOICES = (
  26. ('member', 'Adhérent'),
  27. ('not_member', 'Non adhérent'),
  28. ('pending', "Demande d'adhésion"),
  29. )
  30. status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
  31. default='pending')
  32. type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
  33. default='natural_person')
  34. organization_name = models.CharField(max_length=200, blank=True,
  35. verbose_name='Nom de l\'organisme',
  36. help_text='Pour une personne morale')
  37. home_phone_number = models.CharField(max_length=25, blank=True,
  38. verbose_name=u'Téléphone fixe')
  39. mobile_phone_number = models.CharField(max_length=25, blank=True,
  40. verbose_name=u'Téléphone mobile')
  41. # TODO: use a django module that provides an address model? (would
  42. # support more countries and address types)
  43. address = models.TextField(verbose_name=u'Adresse', blank=True, null=True)
  44. postal_code = models.CharField(max_length=15, blank=True, null=True,
  45. verbose_name=u'Code postal')
  46. city = models.CharField(max_length=200, blank=True, null=True,
  47. verbose_name=u'Commune')
  48. country = models.CharField(max_length=200, blank=True, null=True,
  49. default='France',
  50. verbose_name=u'Pays')
  51. entry_date = models.DateField(null=False,
  52. blank=False,
  53. default=datetime.date.today,
  54. verbose_name='Date de première adhésion')
  55. # TODO: for data retention, prevent deletion of a user object while
  56. # the resign date is recent enough (e.g. one year in France).
  57. resign_date = models.DateField(null=True, blank=True,
  58. verbose_name='Date de départ de '
  59. 'l\'association')
  60. # Following fields are managed by the parent class AbstractUser :
  61. # username, first_name, last_name, email
  62. # This property is used to change password in LDAP. Used in sync_to_ldap.
  63. # Should not be defined manually. Prefer use set_password method that hash
  64. # passwords for both ldap and local db
  65. _password_ldap = None
  66. def __unicode__(self):
  67. name = self.first_name + ' ' + self.last_name
  68. if self.organization_name:
  69. name += ' (%s)' % self.organization_name
  70. return name
  71. def get_full_name(self):
  72. return '%s %s' % (self.first_name, self.last_name)
  73. def get_short_name(self):
  74. return '%s' % self.username
  75. # Renvoie la date de fin de la dernière cotisation du membre
  76. def end_date_of_membership(self):
  77. try:
  78. return self.membership_fees.order_by('-end_date')[0].end_date
  79. #TODO: bad practice de tout matcher comme ca
  80. except:
  81. return None
  82. def is_paid_up(self):
  83. """
  84. True si le membre est à jour de cotisation. False sinon
  85. """
  86. if self.end_date_of_membership() \
  87. and self.end_date_of_membership() >= datetime.date.today():
  88. return True
  89. else:
  90. return False
  91. def set_password(self, new_password, *args, **kwargs):
  92. """
  93. Définit le mot de passe a sauvegarder en base et dans le LDAP
  94. """
  95. super(Member, self).set_password(new_password, *args, **kwargs)
  96. self._password_ldap = utils.ldap_hash(new_password)
  97. def get_active_subscriptions(self, date=datetime.date.today()):
  98. """
  99. Return list of OfferSubscription which are active today
  100. """
  101. return OfferSubscription.objects.filter(
  102. Q(member__exact=self.pk),
  103. Q(subscription_date__lte=date),
  104. Q(resign_date__isnull=True) | Q(resign_date__gte=date))
  105. def get_automatic_username(self):
  106. """
  107. Calcul le username / ldap cn automatiquement en fonction
  108. du nom et du prénom
  109. """
  110. # Première lettre de chaque partie du prénom
  111. first_name_letters = ''.join(
  112. [c[0] for c in self.first_name.split('-')]
  113. )
  114. # Concaténer avec nom de famille
  115. username = ('%s%s' % (first_name_letters, self.last_name))
  116. # Remplacer ou enlever les caractères non ascii
  117. username = unicodedata.normalize('NFD', username)\
  118. .encode('ascii', 'ignore')
  119. # Enlever ponctuation et espace
  120. username = username.translate(None, string.punctuation + ' ')
  121. # En minuscule
  122. username = username.lower()
  123. return username
  124. def sync_to_ldap(self, creation, update_fields, *args, **kwargs):
  125. """
  126. Update LDAP data when a member is saved
  127. """
  128. # Do not perform LDAP query if no usefull fields to update are specified
  129. # in update_fields
  130. # Ex : at login, last_login field is updated by django auth module.
  131. if update_fields and set(['username', 'last_name', 'first_name']).isdisjoint(set(update_fields)):
  132. return
  133. # Fail if no username specified
  134. assert self.username, ('Can\'t sync with LDAP because missing username '
  135. 'value for the Member : %s' % self)
  136. if not creation:
  137. ldap_user = LdapUser.objects.get(pk=self.username)
  138. if creation:
  139. max_uid_number = LdapUser.objects.order_by('-uidNumber')[0].uidNumber
  140. ldap_user = LdapUser()
  141. ldap_user.pk = self.username
  142. ldap_user.uid = self.username
  143. ldap_user.nick_name = self.username
  144. ldap_user.uidNumber = max_uid_number + 1
  145. ldap_user.last_name = self.last_name
  146. ldap_user.first_name = self.first_name
  147. # If a password is definied in _password_ldap, change it in LDAP
  148. if self._password_ldap:
  149. # Make sure password is hashed
  150. ldap_user.password = utils.ldap_hash(self._password_ldap)
  151. ldap_user.save()
  152. if creation:
  153. ldap_group = LdapGroup.objects.get(pk='coin')
  154. ldap_group.members.append(ldap_user.pk)
  155. ldap_group.save()
  156. def delete_from_ldap(self):
  157. """
  158. Delete member from the LDAP
  159. """
  160. assert self.username, ('Can\'t delete from LDAP because missing '
  161. 'username value for the Member : %s' % self)
  162. # Lorsqu'un membre est supprimé du SI, son utilisateur LDAP
  163. # correspondant est sorti du groupe "coin" afin qu'il n'ait plus
  164. # accès au SI
  165. ldap_group = LdapGroup.objects.get(pk='coin')
  166. if self.username in ldap_group.members:
  167. ldap_group.members.remove(self.username)
  168. ldap_group.save()
  169. class Meta:
  170. verbose_name = 'membre'
  171. class CryptoKey(models.Model):
  172. KEY_TYPE_CHOICES = (('RSA', 'RSA'), ('GPG', 'GPG'))
  173. type = models.CharField(max_length=3, choices=KEY_TYPE_CHOICES)
  174. key = models.TextField(verbose_name=u'Clé')
  175. member = models.ForeignKey('Member', verbose_name=u'Membre')
  176. def __unicode__(self):
  177. return u'Clé %s de %s' % (self.type, self.member)
  178. class Meta:
  179. verbose_name = 'clé'
  180. class MembershipFee(models.Model):
  181. member = models.ForeignKey('Member', related_name='membership_fees',
  182. verbose_name=u'Membre')
  183. #TODO: config: valeur par défaut à externaliser dans la configuration
  184. amount = models.IntegerField(null=False, default='20', help_text='en €',
  185. verbose_name=u'Montant')
  186. start_date = models.DateField(
  187. null=False,
  188. blank=False,
  189. default=datetime.date.today,
  190. verbose_name='Date de début de cotisation')
  191. end_date = models.DateField(
  192. null=False,
  193. blank=False,
  194. default=datetime.date.today() + datetime.timedelta(365),
  195. verbose_name='Date de fin de cotisation')
  196. def __unicode__(self):
  197. return u'%s - %s - %i€' % (self.member, self.start_date, self.amount)
  198. class Meta:
  199. verbose_name = 'cotisation'
  200. class LdapUser(ldapdb.models.Model):
  201. # TODO: déplacer ligne suivante dans settings.py
  202. base_dn = "ou=users,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  203. object_classes = ['inetOrgPerson', 'organizationalPerson', 'person',
  204. 'top', 'posixAccount']
  205. uid = CharField(db_column='uid', unique=True, max_length=255)
  206. nick_name = CharField(db_column='cn', unique=True, primary_key=True,
  207. max_length=255)
  208. first_name = CharField(db_column='givenName', max_length=255)
  209. last_name = CharField(db_column='sn', max_length=255)
  210. display_name = CharField(db_column='displayName', max_length=255,
  211. blank=True)
  212. password = CharField(db_column='userPassword', max_length=255)
  213. uidNumber = IntegerField(db_column='uidNumber', unique=True)
  214. gidNumber = IntegerField(db_column='gidNumber', default=2000)
  215. homeDirectory = CharField(db_column='homeDirectory', max_length=255,
  216. default='/tmp')
  217. def __unicode__(self):
  218. return self.display_name
  219. class Meta:
  220. managed = False # Indique à South de ne pas gérer le model LdapUser
  221. class LdapGroup(ldapdb.models.Model):
  222. """
  223. Class for representing an LDAP group entry.
  224. """
  225. #TODO: config à externaliser
  226. # LDAP meta-data
  227. base_dn = "ou=groups,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  228. object_classes = ['posixGroup']
  229. # posixGroup attributes
  230. gid = IntegerField(db_column='gidNumber', unique=True)
  231. name = CharField(db_column='cn', max_length=200, primary_key=True)
  232. members = ListField(db_column='memberUid')
  233. def __unicode__(self):
  234. return self.name
  235. class Meta:
  236. managed = False # Indique à South de ne pas gérer le model LdapGroup
  237. # Indique à South de ne pas gérer les models LdapUser et LdapGroup
  238. add_ignored_fields(["^ldapdb\.models\.fields"])
  239. @receiver(pre_save, sender=Member)
  240. def define_username(sender, instance, **kwargs):
  241. """
  242. Lors de la sauvegarde d'un membre. Si le champ username n'est pas définit,
  243. le calcul automatiquement en fonction du nom et du prénom
  244. """
  245. if not instance.username and not instance.pk:
  246. instance.username = instance.get_automatic_username()
  247. @receiver(pre_save, sender=LdapUser)
  248. def define_display_name(sender, instance, **kwargs):
  249. """
  250. Lors de la sauvegarde d'un utilisateur Ldap, le champ display_name est la
  251. concaténation de first_name et last_name
  252. """
  253. if not instance.display_name:
  254. instance.display_name = '%s %s' % (instance.first_name,
  255. instance.last_name)