models.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  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.utils.translation import ugettext_lazy as _
  10. from django.utils.text import capfirst
  11. from django.dispatch import receiver
  12. from django.contrib.auth.models import AbstractUser
  13. from django.conf import settings
  14. from django.core.validators import RegexValidator
  15. from django.core.exceptions import ValidationError
  16. from ldapdb.models.fields import CharField, IntegerField, ListField
  17. from coin.offers.models import OfferSubscription
  18. from coin.mixins import CoinLdapSyncMixin
  19. from coin import utils
  20. class Member(CoinLdapSyncMixin, AbstractUser):
  21. # USERNAME_FIELD = 'login'
  22. REQUIRED_FIELDS = ['first_name', 'last_name', 'email', ]
  23. MEMBER_TYPE_CHOICES = (
  24. ('natural_person', _("Individual")),
  25. ('legal_entity', _("Organisation")),
  26. )
  27. MEMBER_STATUS_CHOICES = (
  28. ('member', _("Member")),
  29. ('not_member', _("Not a member")),
  30. ('pending', _("Pending membership")),
  31. )
  32. status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
  33. default='member', verbose_name=_('status'))
  34. type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
  35. default='natural_person', verbose_name=_("type"))
  36. nickname = models.CharField(max_length=64, blank=True,
  37. verbose_name=_("nickname"),
  38. help_text=_("Pseudonym, …"))
  39. organization_name = models.CharField(max_length=200, blank=True,
  40. verbose_name=_("organisation name"),
  41. help_text=_("For organisations"))
  42. home_phone_number = models.CharField(max_length=25, blank=True,
  43. verbose_name=_("home phone number"))
  44. mobile_phone_number = models.CharField(max_length=25, blank=True,
  45. verbose_name=_("mobile phone number"))
  46. # TODO: use a django module that provides an address model? (would
  47. # support more countries and address types)
  48. address = models.TextField(
  49. verbose_name=_("physical address"), blank=True, null=True)
  50. validator = RegexValidator(regex=r'^\d{5}$',
  51. message=_("Invalid postal code."))
  52. postal_code = models.CharField(max_length=5, blank=True, null=True,
  53. validators=[validator],
  54. verbose_name=_("postal code"))
  55. city = models.CharField(max_length=200, blank=True, null=True,
  56. verbose_name=_("city"))
  57. country = models.CharField(max_length=200, blank=True, null=True,
  58. default='France',
  59. verbose_name=_("country"))
  60. resign_date = models.DateField(null=True, blank=True,
  61. verbose_name=_("resign date of membership"),
  62. help_text=_("In case of premature leaving"))
  63. comments = models.TextField(blank=True, verbose_name=_("comments"),
  64. help_text=_("Free-form comments (specific"
  65. " membership information, reason"
  66. " for leaving, etc)"))
  67. # Following fields are managed by the parent class AbstractUser :
  68. # username, first_name, last_name, email
  69. # However we hack the model to force theses fields to be required. (see
  70. # below)
  71. # This property is used to change password in LDAP. Used in sync_to_ldap.
  72. # Should not be defined manually. Prefer use set_password method that hash
  73. # passwords for both ldap and local db
  74. _password_ldap = None
  75. def clean(self):
  76. from django.utils.translation import ugettext as _
  77. if self.type == 'legal_entity':
  78. if not self.organization_name:
  79. raise ValidationError(_("An organisation name is mandatory for"
  80. " organisations."))
  81. elif self.type == 'natural_person':
  82. if not (self.first_name and self.last_name):
  83. raise ValidationError(_("First name and last name are mandatory"
  84. " for individuals."))
  85. def __unicode__(self):
  86. from django.utils.translation import ugettext as _
  87. if self.type == 'legal_entity':
  88. return self.organization_name
  89. elif self.nickname:
  90. return self.nickname
  91. else:
  92. return _("{first_name} {last_name}").format(
  93. first_name=self.first_name,
  94. last_name=self.last_name)
  95. def get_full_name(self):
  96. return str(self)
  97. def get_short_name(self):
  98. return self.username
  99. # Renvoie la date de fin de la dernière cotisation du membre
  100. def end_date_of_membership(self):
  101. x = self.membership_fees.order_by('-end_date')
  102. if x:
  103. return self.membership_fees.order_by('-end_date')[0].end_date
  104. end_date_of_membership.short_description = _("End date of membership")
  105. def is_paid_up(self):
  106. """
  107. True si le membre est à jour de cotisation. False sinon
  108. """
  109. if self.end_date_of_membership() \
  110. and self.end_date_of_membership() >= datetime.date.today():
  111. return True
  112. else:
  113. return False
  114. def set_password(self, new_password, *args, **kwargs):
  115. """
  116. Définit le mot de passe a sauvegarder en base et dans le LDAP
  117. """
  118. super(Member, self).set_password(new_password, *args, **kwargs)
  119. self._password_ldap = utils.ldap_hash(new_password)
  120. def get_active_subscriptions(self, date=None):
  121. """
  122. Return list of OfferSubscription which are active today
  123. """
  124. if date is None:
  125. date = datetime.date.today()
  126. return OfferSubscription.objects.filter(
  127. Q(member__exact=self.pk),
  128. Q(subscription_date__lte=date),
  129. Q(resign_date__isnull=True) | Q(resign_date__gte=date))
  130. def get_inactive_subscriptions(self, date=None):
  131. """
  132. Return list of OfferSubscription which are not active today
  133. """
  134. if date is None:
  135. date = datetime.date.today()
  136. return OfferSubscription.objects.filter(
  137. Q(member__exact=self.pk),
  138. Q(subscription_date__gt=date) |
  139. Q(resign_date__lt=date))
  140. def get_ssh_keys(self):
  141. # Quick & dirty, ensure that keys are unique (otherwise, LDAP complains)
  142. return list({k.key for k in self.cryptokey_set.filter(type='RSA')})
  143. def sync_ssh_keys(self):
  144. """
  145. Called whenever a SSH key is saved
  146. """
  147. ldap_user = LdapUser.objects.get(pk=self.username)
  148. ldap_user.sshPublicKey = self.get_ssh_keys()
  149. ldap_user.save()
  150. def sync_to_ldap(self, creation, update_fields, *args, **kwargs):
  151. """
  152. Update LDAP data when a member is saved
  153. """
  154. from django.utils.translation import ugettext as _
  155. # Do not perform LDAP query if no usefull fields to update are specified
  156. # in update_fields
  157. # Ex : at login, last_login field is updated by django auth module.
  158. if update_fields and set(['username', 'last_name', 'first_name']).isdisjoint(set(update_fields)):
  159. return
  160. # Fail if no username specified
  161. assert self.username, _("Can't sync with LDAP because missing username "
  162. "value for Member <{name}>").format(name=str(self))
  163. # If try to sync a superuser in creation mode
  164. # Try to retrieve the user in ldap. If exists, switch to update mode
  165. # This allow to create a superuser without having to delete corresponding
  166. # username in LDAP
  167. if self.is_superuser and creation:
  168. try:
  169. ldap_user = LdapUser.objects.get(pk=self.username)
  170. creation = False
  171. except LdapUser.DoesNotExist:
  172. pass
  173. if not creation:
  174. ldap_user = LdapUser.objects.get(pk=self.username)
  175. if creation:
  176. users = LdapUser.objects
  177. if users.exists():
  178. uid_number = users.order_by('-uidNumber')[0].uidNumber + 1
  179. else:
  180. uid_number = settings.LDAP_USER_FIRST_UID
  181. ldap_user = LdapUser()
  182. ldap_user.pk = self.username
  183. ldap_user.uid = self.username
  184. ldap_user.nick_name = self.username
  185. ldap_user.uidNumber = uid_number
  186. ldap_user.homeDirectory = '/home/' + self.username
  187. if self.type == 'natural_person':
  188. ldap_user.last_name = self.last_name
  189. ldap_user.first_name = self.first_name
  190. elif self.type == 'legal_entity':
  191. ldap_user.last_name = self.organization_name
  192. ldap_user.first_name = ""
  193. # If a password is definied in _password_ldap, change it in LDAP
  194. if self._password_ldap:
  195. # Make sure password is hashed
  196. ldap_user.password = utils.ldap_hash(self._password_ldap)
  197. # Store SSH keys
  198. ldap_user.sshPublicKey = self.get_ssh_keys()
  199. ldap_user.save()
  200. # if creation:
  201. # ldap_group = LdapGroup.objects.get(pk='coin')
  202. # ldap_group.members.append(ldap_user.pk)
  203. # ldap_group.save()
  204. def delete_from_ldap(self):
  205. """
  206. Delete member from the LDAP
  207. """
  208. from django.utils.translation import ugettext as _
  209. assert self.username, _("Can't delete from LDAP because missing username "
  210. "value for Member <{name}>").format(name=str(self))
  211. # Delete user from LDAP
  212. ldap_user = LdapUser.objects.get(pk=self.username)
  213. ldap_user.delete()
  214. # Lorsqu'un membre est supprimé du SI, son utilisateur LDAP
  215. # correspondant est sorti du groupe "coin" afin qu'il n'ait plus
  216. # accès au SI
  217. # ldap_group = LdapGroup.objects.get(pk='coin')
  218. # if self.username in ldap_group.members:
  219. # ldap_group.members.remove(self.username)
  220. # ldap_group.save()
  221. def send_welcome_email(self):
  222. """ Envoie le courriel de bienvenue à ce membre """
  223. from coin.isp_database.models import ISPInfo
  224. utils.send_templated_email(to=self.email,
  225. subject_template='members/emails/welcome_email_subject.txt',
  226. body_template='members/emails/welcome_email.html',
  227. context={'member': self, 'branding':ISPInfo.objects.first()})
  228. class Meta:
  229. verbose_name = _('member')
  230. verbose_name_plural = _('members')
  231. # Hack to force email to be required by Member model
  232. Member._meta.get_field('email')._unique = True
  233. Member._meta.get_field('email').blank = False
  234. Member._meta.get_field('email').null = False
  235. def count_active_members():
  236. return Member.objects.filter(status='member').count()
  237. def get_automatic_username(member):
  238. """
  239. Calcul le username automatiquement en fonction
  240. du nom et du prénom
  241. """
  242. # S'il s'agit d'une entreprise, utilise son nom:
  243. if member.type == 'legal_entity' and member.organization_name:
  244. username = member.organization_name
  245. # Sinon, si un pseudo est définit, l'utilise
  246. elif member.nickname:
  247. username = member.nickname
  248. # Sinon, utilise nom et prenom
  249. elif member.first_name and member.last_name:
  250. # Première lettre de chaque partie du prénom
  251. first_name_letters = ''.join(
  252. [c[0] for c in member.first_name.split('-')]
  253. )
  254. # Concaténer avec nom de famille
  255. username = ('%s%s' % (first_name_letters, member.last_name))
  256. else:
  257. raise Exception(_("Not enough information provided to generate "
  258. "a login for the member."))
  259. # Remplacer ou enlever les caractères non ascii
  260. username = unicodedata.normalize('NFD', username)\
  261. .encode('ascii', 'ignore')
  262. # Enlever ponctuation (sauf _-.) et espace
  263. punctuation = ('!"#$%&\'()*+,/:;<=>?@[\\]^`{|}~ ').encode('ascii')
  264. username = username.translate(None, punctuation)
  265. # En minuscule
  266. username = username.lower()
  267. # Maximum de 30 char
  268. username = username[:30]
  269. # Recherche dans les membres existants un username identique
  270. member = Member.objects.filter(username=username)
  271. base_username = username
  272. incr = 2
  273. # Tant qu'un membre est trouvé, incrémente un entier à la fin
  274. while member:
  275. if len(base_username) >= 30:
  276. username = base_username[30 - len(str(incr)):]
  277. else:
  278. username = base_username
  279. username = username + str(incr)
  280. member = Member.objects.filter(username=username)
  281. incr += 1
  282. return username
  283. class CryptoKey(CoinLdapSyncMixin, models.Model):
  284. KEY_TYPE_CHOICES = (('RSA', _('RSA')), ('GPG', _('GPG')))
  285. type = models.CharField(max_length=3, choices=KEY_TYPE_CHOICES,
  286. verbose_name=_('type'))
  287. key = models.TextField(verbose_name=_('key'))
  288. member = models.ForeignKey('Member', verbose_name=_('member'))
  289. def sync_to_ldap(self, creation, *args, **kwargs):
  290. """Simply tell the member object to resync all its SSH keys to LDAP"""
  291. self.member.sync_ssh_keys()
  292. def delete_from_ldap(self, *args, **kwargs):
  293. self.member.sync_ssh_keys()
  294. def __unicode__(self):
  295. from django.utils.translation import ugettext as _
  296. readable_type = capfirst(self.get_type_display())
  297. return _('{type} key of {user}').format(type=readable_type,
  298. user=self.member)
  299. class Meta:
  300. verbose_name = _('key')
  301. verbose_name_plural = _('keys')
  302. class MembershipFee(models.Model):
  303. PAYMENT_METHOD_CHOICES = (
  304. ('cash', _('Cash')),
  305. ('check', _('Check')),
  306. ('transfer', _('Transfer')),
  307. ('other', _('Other'))
  308. )
  309. member = models.ForeignKey('Member', related_name='membership_fees',
  310. verbose_name=_('member'))
  311. amount = models.DecimalField(null=False, max_digits=5, decimal_places=2,
  312. default=settings.MEMBER_DEFAULT_COTISATION,
  313. verbose_name=_('amount'),
  314. help_text=_('in €'))
  315. start_date = models.DateField(
  316. null=False,
  317. blank=False,
  318. verbose_name=_('start date of membership fee'))
  319. end_date = models.DateField(
  320. null=False,
  321. blank=True,
  322. verbose_name=_('end date of membership fee'),
  323. help_text=_('By default, a membership fee covers one year'))
  324. payment_method = models.CharField(max_length=100, null=True, blank=True,
  325. choices=PAYMENT_METHOD_CHOICES,
  326. verbose_name=_('payment method'))
  327. reference = models.CharField(max_length=125, null=True, blank=True,
  328. verbose_name=_('payment reference'),
  329. help_text=_('Check number, '
  330. 'reference of bank transfer, '
  331. 'comment…'))
  332. payment_date = models.DateField(null=True, blank=True,
  333. verbose_name=_('date of payment'))
  334. def clean(self):
  335. if self.end_date is None:
  336. self.end_date = self.start_date + datetime.timedelta(364)
  337. def __unicode__(self):
  338. return '%s - %s - %i€' % (self.member, self.start_date, self.amount)
  339. class Meta:
  340. verbose_name = _('membership fee')
  341. verbose_name_plural = _('membership fees')
  342. class LdapUser(ldapdb.models.Model):
  343. # "ou=users,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  344. base_dn = settings.LDAP_USER_BASE_DN
  345. object_classes = [b'inetOrgPerson', b'organizationalPerson', b'person',
  346. b'top', b'posixAccount', b'ldapPublicKey']
  347. uid = CharField(db_column=b'uid', unique=True, max_length=255)
  348. nick_name = CharField(db_column=b'cn', unique=True, primary_key=True,
  349. max_length=255)
  350. first_name = CharField(db_column=b'givenName', max_length=255)
  351. last_name = CharField(db_column=b'sn', max_length=255)
  352. display_name = CharField(db_column=b'displayName', max_length=255,
  353. blank=True)
  354. password = CharField(db_column=b'userPassword', max_length=255)
  355. uidNumber = IntegerField(db_column=b'uidNumber', unique=True)
  356. gidNumber = IntegerField(db_column=b'gidNumber', default=2000)
  357. homeDirectory = CharField(db_column=b'homeDirectory', max_length=255,
  358. default='/tmp')
  359. loginShell = CharField(db_column=b'loginShell', max_length=255,
  360. default='/bin/bash')
  361. sshPublicKey = ListField(db_column=b'sshPublicKey', default=[])
  362. def __unicode__(self):
  363. return self.display_name
  364. class Meta:
  365. managed = False # Indique à Django de ne pas intégrer ce model en base
  366. # class LdapGroup(ldapdb.models.Model):
  367. # "ou=groups,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  368. # base_dn = settings.LDAP_GROUP_BASE_DN
  369. # object_classes = [b'posixGroup']
  370. # gid = IntegerField(db_column=b'gidNumber', unique=True)
  371. # name = CharField(db_column=b'cn', max_length=200, primary_key=True)
  372. # members = ListField(db_column=b'memberUid')
  373. # def __unicode__(self):
  374. # return self.name
  375. # class Meta:
  376. # managed = False # Indique à Django de ne pas intégrer ce model en base
  377. @receiver(pre_save, sender=Member)
  378. def define_username(sender, instance, **kwargs):
  379. """
  380. Lors de la sauvegarde d'un membre. Si le champ username n'est pas définit,
  381. le calcul automatiquement en fonction du nom et du prénom
  382. """
  383. if not instance.username and not instance.pk:
  384. instance.username = get_automatic_username(instance)
  385. @receiver(pre_save, sender=LdapUser)
  386. def define_display_name(sender, instance, **kwargs):
  387. """
  388. Lors de la sauvegarde d'un utilisateur Ldap, le champ display_name est la
  389. concaténation de first_name et last_name
  390. """
  391. if not instance.display_name:
  392. instance.display_name = '%s %s' % (instance.first_name,
  393. instance.last_name)