models.py 14 KB

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