models.py 13 KB

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