models.py 12 KB

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