models.py 14 KB

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