models.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import ldapdb.models
  4. import unicodedata
  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 django.core.exceptions import ValidationError
  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='member', verbose_name='statut')
  32. type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
  33. default='natural_person', verbose_name='type')
  34. nickname = models.CharField(max_length=64, blank=True,
  35. verbose_name="nom d'usage",
  36. help_text='Pseudonyme, …')
  37. organization_name = models.CharField(max_length=200, blank=True,
  38. verbose_name="nom de l'organisme",
  39. help_text='Pour une personne morale')
  40. home_phone_number = models.CharField(max_length=25, blank=True,
  41. verbose_name='téléphone fixe')
  42. mobile_phone_number = models.CharField(max_length=25, blank=True,
  43. verbose_name='téléphone mobile')
  44. # TODO: use a django module that provides an address model? (would
  45. # support more countries and address types)
  46. address = models.TextField(
  47. verbose_name='adresse postale', blank=True, null=True)
  48. postal_code = models.CharField(max_length=5, blank=True, null=True,
  49. validators=[RegexValidator(regex=r'^\d{5}$',
  50. message='Code postal non valide.')],
  51. verbose_name='code postal')
  52. city = models.CharField(max_length=200, blank=True, null=True,
  53. verbose_name='commune')
  54. country = models.CharField(max_length=200, blank=True, null=True,
  55. default='France',
  56. verbose_name='pays')
  57. resign_date = models.DateField(null=True, blank=True,
  58. verbose_name="date de départ de "
  59. "l'association",
  60. help_text="En cas de départ prématuré")
  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
  64. # below)
  65. # This property is used to change password in LDAP. Used in sync_to_ldap.
  66. # Should not be defined manually. Prefer use set_password method that hash
  67. # passwords for both ldap and local db
  68. _password_ldap = None
  69. def clean(self):
  70. if self.type == 'legal_entity':
  71. if not self.organization_name:
  72. raise ValidationError("Le nom de l'organisme est obligatoire "
  73. "pour une personne morale")
  74. elif self.type == 'natural_person':
  75. if not (self.first_name and self.last_name):
  76. raise ValidationError("Le nom et prénom sont obligatoires "
  77. "pour une personne physique")
  78. def __unicode__(self):
  79. if self.type == 'legal_entity':
  80. return self.organization_name
  81. elif self.nickname:
  82. return self.nickname
  83. else:
  84. return self.first_name + ' ' + self.last_name
  85. def get_full_name(self):
  86. return str(self)
  87. def get_short_name(self):
  88. return self.username
  89. # Renvoie la date de fin de la dernière cotisation du membre
  90. def end_date_of_membership(self):
  91. try:
  92. return self.membership_fees.order_by('-end_date')[0].end_date
  93. # TODO: bad practice de tout matcher comme ca
  94. except:
  95. return None
  96. end_date_of_membership.short_description = "Date de fin d'adhésion"
  97. def is_paid_up(self):
  98. """
  99. True si le membre est à jour de cotisation. False sinon
  100. """
  101. if self.end_date_of_membership() \
  102. and self.end_date_of_membership() >= datetime.date.today():
  103. return True
  104. else:
  105. return False
  106. def set_password(self, new_password, *args, **kwargs):
  107. """
  108. Définit le mot de passe a sauvegarder en base et dans le LDAP
  109. """
  110. super(Member, self).set_password(new_password, *args, **kwargs)
  111. self._password_ldap = utils.ldap_hash(new_password)
  112. def get_active_subscriptions(self, date=datetime.date.today()):
  113. """
  114. Return list of OfferSubscription which are active today
  115. """
  116. return OfferSubscription.objects.filter(
  117. Q(member__exact=self.pk),
  118. Q(subscription_date__lte=date),
  119. Q(resign_date__isnull=True) | Q(resign_date__gte=date))
  120. def get_inactive_subscriptions(self, date=datetime.date.today()):
  121. """
  122. Return list of OfferSubscription which are not active today
  123. """
  124. return OfferSubscription.objects.filter(
  125. Q(member__exact=self.pk),
  126. Q(subscription_date__gt=date) |
  127. Q(resign_date__lt=date))
  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. users = LdapUser.objects
  154. if users.exists():
  155. uid_number = users.order_by('-uidNumber')[0].uidNumber + 1
  156. else:
  157. uid_number = settings.LDAP_USER_FIRST_UID
  158. ldap_user = LdapUser()
  159. ldap_user.pk = self.username
  160. ldap_user.uid = self.username
  161. ldap_user.nick_name = self.username
  162. ldap_user.uidNumber = uid_number
  163. if self.type == 'natural_person':
  164. ldap_user.last_name = self.last_name
  165. ldap_user.first_name = self.first_name
  166. elif self.type == 'legal_entity':
  167. ldap_user.last_name = self.organization_name
  168. ldap_user.first_name = ""
  169. # If a password is definied in _password_ldap, change it in LDAP
  170. if self._password_ldap:
  171. # Make sure password is hashed
  172. ldap_user.password = utils.ldap_hash(self._password_ldap)
  173. ldap_user.save()
  174. # if creation:
  175. # ldap_group = LdapGroup.objects.get(pk='coin')
  176. # ldap_group.members.append(ldap_user.pk)
  177. # ldap_group.save()
  178. def delete_from_ldap(self):
  179. """
  180. Delete member from the LDAP
  181. """
  182. assert self.username, ('Can\'t delete from LDAP because missing '
  183. 'username value for the Member : %s' % self)
  184. # Delete user from LDAP
  185. ldap_user = LdapUser.objects.get(pk=self.username)
  186. ldap_user.delete()
  187. # Lorsqu'un membre est supprimé du SI, son utilisateur LDAP
  188. # correspondant est sorti du groupe "coin" afin qu'il n'ait plus
  189. # accès au SI
  190. # ldap_group = LdapGroup.objects.get(pk='coin')
  191. # if self.username in ldap_group.members:
  192. # ldap_group.members.remove(self.username)
  193. # ldap_group.save()
  194. def send_welcome_email(self):
  195. """ Envoie le courriel de bienvenue à ce membre """
  196. utils.send_templated_email(to=self.email,
  197. subject_template='members/emails/welcome_email_subject.txt',
  198. body_template='members/emails/welcome_email.html',
  199. context={'member': self})
  200. class Meta:
  201. verbose_name = 'membre'
  202. # Hack to force email to be required by Member model
  203. Member._meta.get_field('email')._unique = True
  204. Member._meta.get_field('email').blank = False
  205. Member._meta.get_field('email').null = False
  206. def count_active_members():
  207. return Member.objects.filter(status='member').count()
  208. def get_automatic_username(member):
  209. """
  210. Calcul le username automatiquement en fonction
  211. du nom et du prénom
  212. """
  213. # S'il s'agit d'une entreprise, utilise son nom:
  214. if member.type == 'legal_entity' and member.organization_name:
  215. username = member.organization_name
  216. # Sinon, si un pseudo est définit, l'utilise
  217. elif member.nickname:
  218. username = member.nickname
  219. # Sinon, utilise nom et prenom
  220. elif member.first_name and member.last_name:
  221. # Première lettre de chaque partie du prénom
  222. first_name_letters = ''.join(
  223. [c[0] for c in member.first_name.split('-')]
  224. )
  225. # Concaténer avec nom de famille
  226. username = ('%s%s' % (first_name_letters, member.last_name))
  227. else:
  228. raise Exception('Il n\'y a pas sufissement d\'informations pour déterminer un login automatiquement')
  229. # Remplacer ou enlever les caractères non ascii
  230. username = unicodedata.normalize('NFD', username)\
  231. .encode('ascii', 'ignore')
  232. # Enlever ponctuation (sauf _-.) et espace
  233. punctuation = ('!"#$%&\'()*+,/:;<=>?@[\\]^`{|}~ ').encode('ascii')
  234. username = username.translate(None, punctuation)
  235. # En minuscule
  236. username = username.lower()
  237. # Maximum de 30 char
  238. username = username[:30]
  239. # Recherche dans les membres existants un username identique
  240. member = Member.objects.filter(username=username)
  241. base_username = username
  242. incr = 2
  243. # Tant qu'un membre est trouvé, incrémente un entier à la fin
  244. while member:
  245. if len(base_username) >= 30:
  246. username = base_username[30 - len(str(incr)):]
  247. else:
  248. username = base_username
  249. username = username + str(incr)
  250. member = Member.objects.filter(username=username)
  251. incr += 1
  252. return username
  253. class CryptoKey(models.Model):
  254. KEY_TYPE_CHOICES = (('RSA', 'RSA'), ('GPG', 'GPG'))
  255. type = models.CharField(max_length=3, choices=KEY_TYPE_CHOICES,
  256. verbose_name='type')
  257. key = models.TextField(verbose_name='clé')
  258. member = models.ForeignKey('Member', verbose_name='membre')
  259. def __unicode__(self):
  260. return 'Clé %s de %s' % (self.type, self.member)
  261. class Meta:
  262. verbose_name = 'clé'
  263. class MembershipFee(models.Model):
  264. member = models.ForeignKey('Member', related_name='membership_fees',
  265. verbose_name='membre')
  266. amount = models.DecimalField(null=False, max_digits=5, decimal_places=2,
  267. default=settings.MEMBER_DEFAULT_COTISATION,
  268. verbose_name='montant', help_text='en €')
  269. start_date = models.DateField(
  270. null=False,
  271. blank=False,
  272. verbose_name='date de début de cotisation')
  273. end_date = models.DateField(
  274. null=False,
  275. blank=True,
  276. verbose_name='date de fin de cotisation',
  277. help_text='par défaut, la cotisation dure un an')
  278. payment_method = models.ForeignKey('billing.PaymentMethod', null=True,
  279. verbose_name='moyen de paiment')
  280. reference = models.CharField(max_length=125, null=True, blank=True,
  281. verbose_name='référence du paiement',
  282. help_text='numéro de chèque, '
  283. 'référence de virement, commentaire...')
  284. payment_date = models.DateField(null=True, blank=True,
  285. verbose_name='date du paiement')
  286. def clean(self):
  287. if self.end_date is None:
  288. self.end_date = self.start_date + datetime.timedelta(364)
  289. def __unicode__(self):
  290. return '%s - %s - %i€' % (self.member, self.start_date, self.amount)
  291. class Meta:
  292. verbose_name = 'cotisation'
  293. class LdapUser(ldapdb.models.Model):
  294. # "ou=users,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  295. base_dn = settings.LDAP_USER_BASE_DN
  296. object_classes = [b'inetOrgPerson', b'organizationalPerson', b'person',
  297. b'top', b'posixAccount']
  298. uid = CharField(db_column=b'uid', unique=True, max_length=255)
  299. nick_name = CharField(db_column=b'cn', unique=True, primary_key=True,
  300. max_length=255)
  301. first_name = CharField(db_column=b'givenName', max_length=255)
  302. last_name = CharField(db_column=b'sn', max_length=255)
  303. display_name = CharField(db_column=b'displayName', max_length=255,
  304. blank=True)
  305. password = CharField(db_column=b'userPassword', max_length=255)
  306. uidNumber = IntegerField(db_column=b'uidNumber', unique=True)
  307. gidNumber = IntegerField(db_column=b'gidNumber', default=2000)
  308. homeDirectory = CharField(db_column=b'homeDirectory', max_length=255,
  309. default='/tmp')
  310. def __unicode__(self):
  311. return self.display_name
  312. class Meta:
  313. managed = False # Indique à Django de ne pas intégrer ce model en base
  314. # class LdapGroup(ldapdb.models.Model):
  315. # "ou=groups,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  316. # base_dn = settings.LDAP_GROUP_BASE_DN
  317. # object_classes = [b'posixGroup']
  318. # gid = IntegerField(db_column=b'gidNumber', unique=True)
  319. # name = CharField(db_column=b'cn', max_length=200, primary_key=True)
  320. # members = ListField(db_column=b'memberUid')
  321. # def __unicode__(self):
  322. # return self.name
  323. # class Meta:
  324. # managed = False # Indique à Django de ne pas intégrer ce model en base
  325. @receiver(pre_save, sender=Member)
  326. def define_username(sender, instance, **kwargs):
  327. """
  328. Lors de la sauvegarde d'un membre. Si le champ username n'est pas définit,
  329. le calcul automatiquement en fonction du nom et du prénom
  330. """
  331. if not instance.username and not instance.pk:
  332. instance.username = get_automatic_username(instance)
  333. @receiver(pre_save, sender=LdapUser)
  334. def define_display_name(sender, instance, **kwargs):
  335. """
  336. Lors de la sauvegarde d'un utilisateur Ldap, le champ display_name est la
  337. concaténation de first_name et last_name
  338. """
  339. if not instance.display_name:
  340. instance.display_name = '%s %s' % (instance.first_name,
  341. instance.last_name)