models.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  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 AbstractBaseUser, PermissionsMixin, BaseUserManager
  11. from ldapdb.models.fields import CharField, IntegerField, ListField
  12. from south.modelsinspector import add_ignored_fields
  13. from coin.offers.models import OfferSubscription
  14. from coin.mixins import CoinLdapSyncMixin
  15. from coin import utils
  16. from django.contrib.auth.signals import user_logged_in
  17. from django.conf import settings
  18. class CoinUserManager(BaseUserManager):
  19. def create_user(self, login, first_name, last_name, email, password=None):
  20. """
  21. """
  22. if not login:
  23. raise ValueError('Users must have a login')
  24. if not email:
  25. raise ValueError('Users must have an email address')
  26. if not first_name:
  27. raise ValueError('Users must have a first name')
  28. if not last_name:
  29. raise ValueError('Users must have a last name')
  30. user = self.model(
  31. login=login,
  32. email=self.normalize_email(email),
  33. first_name=first_name,
  34. last_name=last_name,
  35. )
  36. user.set_password(password, ldap=False)
  37. user.save(using=self._db)
  38. user.set_ldap_password(password)
  39. return user
  40. def create_superuser(self, login, first_name, last_name, email, password):
  41. """
  42. Creates and saves a superuser
  43. """
  44. user = self.create_user(
  45. login=login,
  46. email=email,
  47. first_name=first_name,
  48. last_name=last_name,
  49. password=password
  50. )
  51. user.status = 'member'
  52. user.is_superuser = True
  53. user.is_staff = True
  54. user.save(using=self._db)
  55. return user
  56. class Member(CoinLdapSyncMixin, AbstractBaseUser, PermissionsMixin):
  57. USERNAME_FIELD = 'login'
  58. REQUIRED_FIELDS = ['first_name', 'last_name', 'email', ]
  59. MEMBER_TYPE_CHOICES = (
  60. ('natural_person', 'Personne physique'),
  61. ('legal_entity', 'Personne morale'),
  62. )
  63. MEMBER_STATUS_CHOICES = (
  64. ('member', 'Adhérent'),
  65. ('not_member', 'Non adhérent'),
  66. ('pending', "Demande d'adhésion"),
  67. )
  68. objects = CoinUserManager()
  69. is_active = models.BooleanField(default=True)
  70. is_staff = models.BooleanField(default=False)
  71. status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
  72. default='pending')
  73. type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
  74. default='natural_person')
  75. first_name = models.CharField(max_length=200, verbose_name=u'Prénom')
  76. last_name = models.CharField(max_length=200, verbose_name=u'Nom')
  77. login = models.CharField(max_length=200, unique=True, null=True,
  78. blank=True,
  79. verbose_name='login',
  80. help_text='Login. Clé avec le LDAP. Laisser vide pour '
  81. 'le générer automatiquement')
  82. organization_name = models.CharField(max_length=200, blank=True,
  83. verbose_name='Nom de l\'organisme',
  84. help_text='Pour une personne morale')
  85. email = models.EmailField(max_length=254, verbose_name=u'Courriel')
  86. home_phone_number = models.CharField(max_length=25, blank=True,
  87. verbose_name=u'Téléphone fixe')
  88. mobile_phone_number = models.CharField(max_length=25, blank=True,
  89. verbose_name=u'Téléphone mobile')
  90. # TODO: use a django module that provides an address model? (would
  91. # support more countries and address types)
  92. address = models.TextField(verbose_name=u'Adresse', blank=True, null=True)
  93. postal_code = models.CharField(max_length=15, blank=True, null=True,
  94. verbose_name=u'Code postal')
  95. city = models.CharField(max_length=200, blank=True, null=True,
  96. verbose_name=u'Commune')
  97. country = models.CharField(max_length=200, blank=True, null=True,
  98. default='France',
  99. verbose_name=u'Pays')
  100. entry_date = models.DateField(null=False,
  101. blank=False,
  102. default=datetime.date.today,
  103. verbose_name='Date de première adhésion')
  104. # TODO: for data retention, prevent deletion of a user object while
  105. # the resign date is recent enough (e.g. one year in France).
  106. resign_date = models.DateField(null=True, blank=True,
  107. verbose_name='Date de départ de '
  108. 'l\'association')
  109. def __unicode__(self):
  110. name = self.first_name + ' ' + self.last_name
  111. if self.organization_name:
  112. name += ' (%s)' % self.organization_name
  113. return name
  114. def get_full_name(self):
  115. return '%s %s' % (self.first_name, self.last_name)
  116. def get_short_name(self):
  117. return '%s' % self.login
  118. # Renvoie la date de fin de la dernière cotisation du membre
  119. def end_date_of_membership(self):
  120. try:
  121. return self.membership_fees.order_by('-end_date')[0].end_date
  122. #TODO: bad practice de tout matcher comme ca
  123. except:
  124. return None
  125. def is_paid_up(self):
  126. """
  127. True si le membre est à jour de cotisation. False sinon
  128. """
  129. if self.end_date_of_membership() \
  130. and self.end_date_of_membership() >= datetime.date.today():
  131. return True
  132. else:
  133. return False
  134. def set_password(self, new_password, ldap=True, *args, **kwargs):
  135. """
  136. Override set_password in order to change password in ldap too
  137. """
  138. super(Member, self).set_password(new_password, *args, **kwargs)
  139. if ldap:
  140. self.set_ldap_password(new_password)
  141. def set_ldap_password(self, new_password):
  142. """
  143. Change password in LDAP
  144. """
  145. ldap_user = LdapUser.objects.get(pk=self.login)
  146. ldap_user.password = new_password
  147. ldap_user.save()
  148. def get_active_subscriptions(self, date=datetime.date.today()):
  149. """
  150. Return list of OfferSubscription which are active today
  151. """
  152. return OfferSubscription.objects.filter(
  153. Q(member__exact=self.pk),
  154. Q(subscription_date__lte=date),
  155. Q(resign_date__isnull=True) | Q(resign_date__gte=date))
  156. def get_automatic_login(self):
  157. """
  158. Calcul le login / ldap cn automatiquement en fonction
  159. du nom et du prénom
  160. """
  161. # Première lettre de chaque partie du prénom
  162. first_name_letters = ''.join(
  163. [c[0] for c in self.first_name.split('-')]
  164. )
  165. # Concaténer avec nom de famille
  166. login = ('%s%s' % (first_name_letters, self.last_name))
  167. # Remplacer ou enlever les caractères non ascii
  168. login = unicodedata.normalize('NFD', login)\
  169. .encode('ascii', 'ignore')
  170. # Enlever ponctuation et espace
  171. login = login.translate(None, string.punctuation + ' ')
  172. # En minuscule
  173. login = login.lower()
  174. return login
  175. def sync_to_ldap(self, creation):
  176. """
  177. Update LDAP data when a member is saved
  178. """
  179. assert self.login, ('Can\'t sync with LDAP because missing login '
  180. 'value for the Member : %s' % self)
  181. if not creation:
  182. ldap_user = LdapUser.objects.get(pk=self.login)
  183. if creation:
  184. max_uid_number = LdapUser.objects.order_by('-uidNumber')[0].uidNumber
  185. ldap_user = LdapUser()
  186. ldap_user.pk = self.login
  187. ldap_user.uid = self.login
  188. ldap_user.nick_name = self.login
  189. ldap_user.uidNumber = max_uid_number + 1
  190. ldap_user.last_name = self.last_name
  191. ldap_user.first_name = self.first_name
  192. ldap_user.save()
  193. if creation:
  194. ldap_group = LdapGroup.objects.get(pk='coin')
  195. ldap_group.members.append(ldap_user.pk)
  196. ldap_group.save()
  197. def delete_from_ldap(self):
  198. """
  199. Delete member from the LDAP
  200. """
  201. assert self.login, ('Can\'t delete from LDAP because missing '
  202. 'login value for the Member : %s' % self)
  203. # Lorsqu'un membre est supprimé du SI, son utilisateur LDAP
  204. # correspondant est sorti du groupe "coin" afin qu'il n'ait plus
  205. # accès au SI
  206. ldap_group = LdapGroup.objects.get(pk='coin')
  207. if self.login in ldap_group.members:
  208. ldap_group.members.remove(self.login)
  209. ldap_group.save()
  210. def has_perm(self, perm, obj=None):
  211. "Does the user have a specific permission?"
  212. # Simplest possible answer: Yes, always
  213. return True
  214. def has_module_perms(self, app_label):
  215. "Does the user have permissions to view the app `app_label`?"
  216. # Simplest possible answer: Yes, always
  217. return True
  218. class Meta:
  219. verbose_name = 'membre'
  220. class CryptoKey(models.Model):
  221. KEY_TYPE_CHOICES = (('RSA', 'RSA'), ('GPG', 'GPG'))
  222. type = models.CharField(max_length=3, choices=KEY_TYPE_CHOICES)
  223. key = models.TextField(verbose_name=u'Clé')
  224. member = models.ForeignKey('Member', verbose_name=u'Membre')
  225. def __unicode__(self):
  226. return u'Clé %s de %s' % (self.type, self.member)
  227. class Meta:
  228. verbose_name = 'clé'
  229. class MembershipFee(models.Model):
  230. member = models.ForeignKey('Member', related_name='membership_fees',
  231. verbose_name=u'Membre')
  232. #TODO: config: valeur par défaut à externaliser dans la configuration
  233. amount = models.IntegerField(null=False, default='20', help_text='en €',
  234. verbose_name=u'Montant')
  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=datetime.date.today() + datetime.timedelta(365),
  244. verbose_name='Date de fin de cotisation')
  245. def __unicode__(self):
  246. return u'%s - %s - %i€' % (self.member, self.start_date, self.amount)
  247. class Meta:
  248. verbose_name = 'cotisation'
  249. class LdapUser(ldapdb.models.Model):
  250. # TODO: déplacer ligne suivante dans settings.py
  251. base_dn = "ou=users,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  252. object_classes = ['inetOrgPerson', 'organizationalPerson', 'person',
  253. 'top', 'posixAccount']
  254. uid = CharField(db_column='uid', unique=True, max_length=255)
  255. nick_name = CharField(db_column='cn', unique=True, primary_key=True,
  256. max_length=255)
  257. first_name = CharField(db_column='givenName', max_length=255)
  258. last_name = CharField(db_column='sn', max_length=255)
  259. display_name = CharField(db_column='displayName', max_length=255,
  260. blank=True)
  261. password = CharField(db_column='userPassword', max_length=255)
  262. uidNumber = IntegerField(db_column='uidNumber', unique=True)
  263. gidNumber = IntegerField(db_column='gidNumber', default=2000)
  264. homeDirectory = CharField(db_column='homeDirectory', max_length=255,
  265. default='/tmp')
  266. def __unicode__(self):
  267. return self.display_name
  268. class Meta:
  269. managed = False # Indique à South de ne pas gérer le model LdapUser
  270. class LdapGroup(ldapdb.models.Model):
  271. """
  272. Class for representing an LDAP group entry.
  273. """
  274. #TODO: config à externaliser
  275. # LDAP meta-data
  276. base_dn = "ou=groups,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  277. object_classes = ['posixGroup']
  278. # posixGroup attributes
  279. gid = IntegerField(db_column='gidNumber', unique=True)
  280. name = CharField(db_column='cn', max_length=200, primary_key=True)
  281. members = ListField(db_column='memberUid')
  282. def __unicode__(self):
  283. return self.name
  284. class Meta:
  285. managed = False # Indique à South de ne pas gérer le model LdapGroup
  286. # Indique à South de ne pas gérer les models LdapUser et LdapGroup
  287. add_ignored_fields(["^ldapdb\.models\.fields"])
  288. @receiver(pre_save, sender=Member)
  289. def define_login(sender, instance, **kwargs):
  290. """
  291. Lors de la sauvegarde d'un membre. Si le champ login n'est pas définit,
  292. le calcul automatiquement en fonction du nom et du prénom
  293. """
  294. if not instance.login and not instance.pk:
  295. instance.login = instance.get_automatic_login()
  296. @receiver(pre_save, sender=LdapUser)
  297. def change_password(sender, instance, **kwargs):
  298. """
  299. Lors de la sauvegarde d'un utilisateur Ldap, cette fonction est exécutée
  300. avant la sauvegarde pour chiffrer le mot de passe s'il est définit
  301. et s'il n'est pas déjà chiffré
  302. """
  303. instance.password = utils.ldap_hash(instance.password)
  304. @receiver(pre_save, sender=LdapUser)
  305. def define_display_name(sender, instance, **kwargs):
  306. """
  307. Lors de la sauvegarde d'un utilisateur Ldap, le champ display_name est la
  308. concaténation de first_name et last_name
  309. """
  310. if not instance.display_name:
  311. instance.display_name = '%s %s' % (instance.first_name,
  312. instance.last_name)
  313. # @receiver(user_logged_in)
  314. # def define_member_user(sender, request, user, **kwargs):
  315. # """
  316. # Lorsqu'un utilisateur se connect avec succes, fait le lien entre le membre
  317. # et l'utilisateur en définissant le champ user du model membre ayant le
  318. # ldap_cn utilisé pour la connexion
  319. # """
  320. # try:
  321. # member = Member.objects.get(ldap_cn=user.username)
  322. # if not member.user:
  323. # member.user = user
  324. # member.save()
  325. # elif member.user.username != user.username:
  326. # raise Exception('Un membre avec cet ldap_cn existe en base de '
  327. # 'donnée mais l\'utilisateur auquel il est rattaché '
  328. # 'ne correspond pas.')
  329. # except Member.DoesNotExist:
  330. # if not user.is_superuser:
  331. # raise
  332. #==============================================================================
  333. # @receiver(pre_save, sender = LdapUser)
  334. # def ssha_password(sender, **kwargs):
  335. # if not kwargs['instance'].password.startswith('{SSHA}'):
  336. # salt = os.urandom(8).encode('hex')
  337. # kwargs['instance'].password = '{SSHA}' + base64.b64encode(
  338. # hashlib.sha1(obj.password + salt).digest() + salt)
  339. #==============================================================================