models.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  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, Max
  8. from django.db.models.signals import pre_save
  9. from django.dispatch import receiver
  10. from django.contrib.auth.models import AbstractUser, Permission, UserManager
  11. from django.conf import settings
  12. from django.core.validators import RegexValidator
  13. from django.core.exceptions import ValidationError
  14. from django.utils import timezone
  15. from django.utils.text import slugify
  16. from django.core.mail import send_mail
  17. from django.core.urlresolvers import reverse
  18. from ldapdb.models.fields import CharField, IntegerField, ListField
  19. from registration.signals import user_registered
  20. from coin.offers.models import Offer, OfferSubscription
  21. from coin.mixins import CoinLdapSyncMixin
  22. from coin import utils
  23. class MemberQuerySet(models.QuerySet):
  24. paidup_q = Q(
  25. # we have at least one fee
  26. membership_fees__isnull=False,
  27. # and it is still running
  28. membership_fees__end_date__gte=datetime.date.today)
  29. def paidup_fee(self):
  30. return self.filter(self.paidup_q)
  31. def no_fee_or_late(self):
  32. return self.exclude(self.paidup_q)
  33. def could_be_deleted(self):
  34. return self.exclude(
  35. # we have at least one subscription
  36. Q(offersubscription__isnull=False),
  37. # still running or resigned less than one year ago
  38. Q(offersubscription__resign_date__isnull=True)
  39. |
  40. Q(offersubscription__resign_date__gte=utils.one_year_ago())
  41. ).exclude(self.paidup_q)
  42. class MemberManager(UserManager):
  43. use_in_migrations = False
  44. def manageable_by(self, user):
  45. """" Renvoie la liste des members que l'utilisateur est autorisé à voir
  46. dans l'interface d'administration.
  47. """
  48. if user.is_superuser:
  49. return super(MemberManager, self).all()
  50. else:
  51. offers = Offer.objects.manageable_by(user)
  52. return super(MemberManager, self).filter(offersubscription__offer__in=offers).distinct()
  53. @receiver(user_registered)
  54. def send_registration_notification(sender, user, request=None, **kwargs):
  55. """
  56. Send a notification to the admin if a user subscribe
  57. """
  58. relative_link = reverse('admin:members_member_change', args=[user.id])
  59. edit_link = request.build_absolute_uri(relative_link)
  60. utils.send_templated_email(
  61. to=settings.NOTIFICATION_EMAILS,
  62. subject_template='members/emails/new_member_subject.txt',
  63. body_template='members/emails/new_member_email.html',
  64. context={'member': self, 'edit_link': edit_link},
  65. **kwargs)
  66. class Member(CoinLdapSyncMixin, AbstractUser):
  67. # USERNAME_FIELD = 'login'
  68. REQUIRED_FIELDS = ['first_name', 'last_name', 'email', ]
  69. MEMBER_TYPE_CHOICES = (
  70. ('natural_person', 'Personne physique'),
  71. ('legal_entity', 'Personne morale'),
  72. )
  73. MEMBER_STATUS_MEMBER = 'member'
  74. MEMBER_STATUS_NOT_MEMBER = 'not_member'
  75. MEMBER_STATUS_PENDING = 'pending'
  76. MEMBER_STATUS_CHOICES = (
  77. (MEMBER_STATUS_MEMBER, 'Adhérent'),
  78. (MEMBER_STATUS_NOT_MEMBER, 'Non adhérent'),
  79. (MEMBER_STATUS_PENDING, "Demande d'adhésion"),
  80. )
  81. status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
  82. default='member', verbose_name='statut')
  83. type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
  84. default='natural_person', verbose_name='type')
  85. nickname = models.CharField(max_length=64, blank=True,
  86. verbose_name="nom d'usage",
  87. help_text='Pseudonyme, …')
  88. organization_name = models.CharField(max_length=200, blank=True,
  89. verbose_name="nom de l'organisme",
  90. help_text='Pour une personne morale')
  91. home_phone_number = models.CharField(max_length=25, blank=True,
  92. verbose_name='téléphone fixe')
  93. mobile_phone_number = models.CharField(max_length=25, blank=True,
  94. verbose_name='téléphone mobile')
  95. # TODO: use a django module that provides an address model? (would
  96. # support more countries and address types)
  97. address = models.TextField(
  98. verbose_name='adresse postale', blank=True, null=True)
  99. postal_code = models.CharField(max_length=5, blank=True, null=True,
  100. validators=[RegexValidator(regex=r'^\d{5}$',
  101. message='Code postal non valide.')],
  102. verbose_name='code postal')
  103. city = models.CharField(max_length=200, blank=True, null=True,
  104. verbose_name='commune')
  105. country = models.CharField(max_length=200, blank=True, null=True,
  106. default='France',
  107. verbose_name='pays')
  108. resign_date = models.DateField(null=True, blank=True,
  109. verbose_name="date de départ de "
  110. "l'association",
  111. help_text="En cas de départ prématuré")
  112. comments = models.TextField(blank=True, verbose_name='commentaires',
  113. help_text="Commentaires libres (informations"
  114. " spécifiques concernant l'adhésion,"
  115. " raison du départ, etc)")
  116. date_last_call_for_membership_fees_email = models.DateTimeField(null=True,
  117. blank=True,
  118. verbose_name="Date du dernier email de relance de cotisation envoyé")
  119. send_membership_fees_email = models.BooleanField(
  120. default=True, verbose_name='relance de cotisation',
  121. help_text='Précise si l\'utilisateur doit recevoir des mails de relance pour la cotisation. Certains membres n\'ont pas à recevoir de relance (prélèvement automatique, membres d\'honneurs, etc.)')
  122. balance = models.DecimalField(max_digits=5, decimal_places=2, default=0,
  123. verbose_name='solde')
  124. objects = MemberManager.from_queryset(MemberQuerySet)()
  125. # Following fields are managed by the parent class AbstractUser :
  126. # username, first_name, last_name, email
  127. # However we hack the model to force theses fields to be required. (see
  128. # below)
  129. # This property is used to change password in LDAP. Used in sync_to_ldap.
  130. # Should not be defined manually. Prefer use set_password method that hash
  131. # passwords for both ldap and local db
  132. _password_ldap = None
  133. def clean(self):
  134. if self.type == 'legal_entity':
  135. if not self.organization_name:
  136. raise ValidationError("Le nom de l'organisme est obligatoire "
  137. "pour une personne morale")
  138. elif self.type == 'natural_person':
  139. if not (self.first_name and self.last_name):
  140. raise ValidationError("Le nom et prénom sont obligatoires "
  141. "pour une personne physique")
  142. def __unicode__(self):
  143. if self.type == 'legal_entity':
  144. return self.organization_name
  145. elif self.nickname:
  146. return self.nickname
  147. else:
  148. return self.first_name + ' ' + self.last_name
  149. def get_full_name(self):
  150. return str(self)
  151. def get_short_name(self):
  152. return self.username
  153. # Renvoie la date de fin de la dernière cotisation du membre
  154. def end_date_of_membership(self):
  155. aggregate = self.membership_fees.aggregate(end=Max('end_date'))
  156. return aggregate['end']
  157. end_date_of_membership.short_description = "Date de fin d'adhésion"
  158. def is_paid_up(self, date=None):
  159. """
  160. Teste si le membre est à jour de cotisation à la date donnée.
  161. """
  162. if date is None:
  163. date = datetime.date.today()
  164. end_date = self.end_date_of_membership()
  165. if end_date is None:
  166. return False
  167. return (end_date >= date)
  168. def set_password(self, new_password, *args, **kwargs):
  169. """
  170. Définit le mot de passe a sauvegarder en base et dans le LDAP
  171. """
  172. super(Member, self).set_password(new_password, *args, **kwargs)
  173. self._password_ldap = utils.ldap_hash(new_password)
  174. def get_active_subscriptions(self, date=None):
  175. """
  176. Return list of OfferSubscription which are active today
  177. """
  178. if date is None:
  179. date = datetime.date.today()
  180. return OfferSubscription.objects.filter(
  181. Q(member__exact=self.pk),
  182. Q(subscription_date__lte=date),
  183. Q(resign_date__isnull=True) | Q(resign_date__gte=date))
  184. def get_inactive_subscriptions(self, date=None):
  185. """
  186. Return list of OfferSubscription which are not active today
  187. """
  188. if date is None:
  189. date = datetime.date.today()
  190. return OfferSubscription.objects.filter(
  191. Q(member__exact=self.pk),
  192. Q(subscription_date__gt=date) |
  193. Q(resign_date__lt=date))
  194. def get_recent_inactive_subscriptions(self):
  195. """ Does this member has subscriptions that ended less than one year ago
  196. For French law requirements.
  197. """
  198. return self.get_inactive_subscriptions().filter(
  199. resign_date__gte=datetime.date.today() - datetime.timedelta(days=365))
  200. def get_ssh_keys(self):
  201. # Quick & dirty, ensure that keys are unique (otherwise, LDAP complains)
  202. return list({k.key for k in self.cryptokey_set.filter(type='RSA')})
  203. def sync_ssh_keys(self):
  204. """
  205. Called whenever a SSH key is saved
  206. """
  207. ldap_user = LdapUser.objects.get(pk=self.username)
  208. ldap_user.sshPublicKey = self.get_ssh_keys()
  209. ldap_user.save()
  210. def sync_to_ldap(self, creation, update_fields, *args, **kwargs):
  211. """
  212. Update LDAP data when a member is saved
  213. """
  214. # Do not perform LDAP query if no usefull fields to update are specified
  215. # in update_fields
  216. # Ex : at login, last_login field is updated by django auth module.
  217. relevant_fields = {'username', 'last_name', 'first_name',
  218. 'organization_name', 'email'}
  219. if update_fields and relevant_fields.isdisjoint(set(update_fields)):
  220. return
  221. # Fail if no username specified
  222. assert self.username, ('Can\'t sync with LDAP because missing username '
  223. 'value for the Member : %s' % self)
  224. # If try to sync a superuser in creation mode
  225. # Try to retrieve the user in ldap. If exists, switch to update mode
  226. # This allow to create a superuser without having to delete corresponding
  227. # username in LDAP
  228. if self.is_superuser and creation:
  229. try:
  230. ldap_user = LdapUser.objects.get(pk=self.username)
  231. creation = False
  232. except LdapUser.DoesNotExist:
  233. pass
  234. if not creation:
  235. ldap_user = LdapUser.objects.get(pk=self.username)
  236. if creation:
  237. users = LdapUser.objects
  238. if users.exists():
  239. uid_number = users.order_by('-uidNumber')[0].uidNumber + 1
  240. else:
  241. uid_number = settings.LDAP_USER_FIRST_UID
  242. ldap_user = LdapUser()
  243. ldap_user.pk = self.username
  244. ldap_user.uid = self.username
  245. ldap_user.nick_name = self.username
  246. ldap_user.uidNumber = uid_number
  247. ldap_user.homeDirectory = '/home/' + self.username
  248. if self.type == 'natural_person':
  249. ldap_user.last_name = self.last_name
  250. ldap_user.first_name = self.first_name
  251. elif self.type == 'legal_entity':
  252. ldap_user.last_name = self.organization_name
  253. ldap_user.first_name = ""
  254. # If a password is definied in _password_ldap, change it in LDAP
  255. if self._password_ldap:
  256. # Make sure password is hashed
  257. ldap_user.password = utils.ldap_hash(self._password_ldap)
  258. ldap_user.mail = self.email
  259. # Store SSH keys
  260. ldap_user.sshPublicKey = self.get_ssh_keys()
  261. ldap_user.save()
  262. # if creation:
  263. # ldap_group = LdapGroup.objects.get(pk='coin')
  264. # ldap_group.members.append(ldap_user.pk)
  265. # ldap_group.save()
  266. def delete_from_ldap(self):
  267. """
  268. Delete member from the LDAP
  269. """
  270. assert self.username, ('Can\'t delete from LDAP because missing '
  271. 'username value for the Member : %s' % self)
  272. # Delete user from LDAP
  273. ldap_user = LdapUser.objects.get(pk=self.username)
  274. ldap_user.delete()
  275. # Lorsqu'un membre est supprimé du SI, son utilisateur LDAP
  276. # correspondant est sorti du groupe "coin" afin qu'il n'ait plus
  277. # accès au SI
  278. # ldap_group = LdapGroup.objects.get(pk='coin')
  279. # if self.username in ldap_group.members:
  280. # ldap_group.members.remove(self.username)
  281. # ldap_group.save()
  282. def send_welcome_email(self):
  283. """ Envoie le courriel de bienvenue à ce membre """
  284. from coin.isp_database.models import ISPInfo
  285. isp_info = ISPInfo.objects.first()
  286. kwargs = {}
  287. if isp_info.administrative_email:
  288. kwargs['from_email'] = isp_info.administrative_email
  289. utils.send_templated_email(
  290. to=self.email,
  291. subject_template='members/emails/welcome_email_subject.txt',
  292. body_template='members/emails/welcome_email.html',
  293. context={'member': self, 'branding': isp_info},
  294. **kwargs)
  295. def send_call_for_membership_fees_email(self, auto=False):
  296. """ Envoie le courriel d'appel à cotisation du membre
  297. :param auto: is it an auto email? (changes slightly template content)
  298. """
  299. if auto and not self.send_membership_fees_email:
  300. return False
  301. from dateutil.relativedelta import relativedelta
  302. from coin.isp_database.models import ISPInfo
  303. isp_info = ISPInfo.objects.first()
  304. kwargs = {}
  305. # Il peut ne pas y avir d'ISPInfo, ou bien pas d'administrative_email
  306. if isp_info and isp_info.administrative_email:
  307. kwargs['from_email'] = isp_info.administrative_email
  308. # Si le dernier courriel de relance a été envoyé il y a moins de trois
  309. # semaines, n'envoi pas un nouveau courriel
  310. if (not self.date_last_call_for_membership_fees_email
  311. or (self.date_last_call_for_membership_fees_email
  312. <= timezone.now() + relativedelta(weeks=-3))):
  313. utils.send_templated_email(
  314. to=self.email,
  315. subject_template='members/emails/call_for_membership_fees_subject.txt',
  316. body_template='members/emails/call_for_membership_fees.html',
  317. context={'member': self, 'branding': isp_info,
  318. 'membership_info_url': settings.MEMBER_MEMBERSHIP_INFO_URL,
  319. 'today': datetime.date.today,
  320. 'auto_sent': auto},
  321. **kwargs)
  322. # Sauvegarde en base la date du dernier envoi de mail de relance
  323. self.date_last_call_for_membership_fees_email = timezone.now()
  324. self.save()
  325. return True
  326. return False
  327. class Meta:
  328. verbose_name = 'membre'
  329. # Hack to force email to be required by Member model
  330. Member._meta.get_field('email')._unique = True
  331. Member._meta.get_field('email').blank = False
  332. Member._meta.get_field('email').null = False
  333. def count_active_members():
  334. return Member.objects.filter(status='member').count()
  335. def get_automatic_username(member):
  336. """
  337. Calcul le username automatiquement en fonction
  338. du nom et du prénom
  339. """
  340. # S'il s'agit d'une entreprise, utilise son nom:
  341. if member.type == 'legal_entity' and member.organization_name:
  342. username = member.organization_name
  343. # Sinon, si un pseudo est définit, l'utilise
  344. elif member.nickname:
  345. username = member.nickname
  346. # Sinon, utilise nom et prenom
  347. elif member.first_name and member.last_name:
  348. # Première lettre de chaque partie du prénom
  349. first_name_letters = ''.join(
  350. [c[0] for c in member.first_name.split('-')]
  351. )
  352. # Concaténer avec nom de famille
  353. username = ('%s%s' % (first_name_letters, member.last_name))
  354. else:
  355. raise Exception('Il n\'y a pas sufissement d\'informations pour déterminer un login automatiquement')
  356. # Remplacer ou enlever les caractères non ascii
  357. username = unicodedata.normalize('NFD', username)\
  358. .encode('ascii', 'ignore')
  359. # Enlever ponctuation (sauf _-.) et espace
  360. punctuation = ('!"#$%&\'()*+,/:;<=>?@[\\]^`{|}~ ').encode('ascii')
  361. username = username.translate(None, punctuation)
  362. # En minuscule
  363. username = username.lower()
  364. # Maximum de 30 char
  365. username = username[:30]
  366. # Recherche dans les membres existants un username identique
  367. member = Member.objects.filter(username=username)
  368. base_username = username
  369. incr = 2
  370. # Tant qu'un membre est trouvé, incrémente un entier à la fin
  371. while member:
  372. if len(base_username) >= 30:
  373. username = base_username[30 - len(str(incr)):]
  374. else:
  375. username = base_username
  376. username = username + str(incr)
  377. member = Member.objects.filter(username=username)
  378. incr += 1
  379. return username
  380. class CryptoKey(CoinLdapSyncMixin, models.Model):
  381. KEY_TYPE_CHOICES = (('RSA', 'RSA'), ('GPG', 'GPG'))
  382. type = models.CharField(max_length=3, choices=KEY_TYPE_CHOICES,
  383. verbose_name='type')
  384. key = models.TextField(verbose_name='clé')
  385. member = models.ForeignKey('Member', verbose_name='membre')
  386. def sync_to_ldap(self, creation, *args, **kwargs):
  387. """Simply tell the member object to resync all its SSH keys to LDAP"""
  388. self.member.sync_ssh_keys()
  389. def delete_from_ldap(self, *args, **kwargs):
  390. self.member.sync_ssh_keys()
  391. def __unicode__(self):
  392. return 'Clé %s de %s' % (self.type, self.member)
  393. class Meta:
  394. verbose_name = 'clé'
  395. class MembershipFee(models.Model):
  396. PAYMENT_METHOD_CHOICES = (
  397. ('cash', 'Espèces'),
  398. ('check', 'Chèque'),
  399. ('transfer', 'Virement'),
  400. ('other', 'Autre')
  401. )
  402. member = models.ForeignKey('Member', related_name='membership_fees',
  403. verbose_name='membre')
  404. amount = models.DecimalField(null=False, max_digits=5, decimal_places=2,
  405. default=settings.DEFAULT_MEMBERSHIP_FEE,
  406. verbose_name='montant', help_text='en €')
  407. start_date = models.DateField(
  408. null=False,
  409. blank=False,
  410. verbose_name='date de début de cotisation')
  411. end_date = models.DateField(
  412. null=False,
  413. blank=True,
  414. verbose_name='date de fin de cotisation',
  415. help_text='par défaut, la cotisation dure un an')
  416. payment_method = models.CharField(max_length=100, null=True, blank=True,
  417. choices=PAYMENT_METHOD_CHOICES,
  418. verbose_name='moyen de paiement')
  419. reference = models.CharField(max_length=125, null=True, blank=True,
  420. verbose_name='référence du paiement',
  421. help_text='numéro de chèque, '
  422. 'référence de virement, commentaire...')
  423. payment_date = models.DateField(null=True, blank=True,
  424. verbose_name='date du paiement')
  425. def clean(self):
  426. if self.start_date is not None and self.end_date is None:
  427. self.end_date = self.start_date + datetime.timedelta(364)
  428. def save(self, *args, **kwargs):
  429. ret = super(MembershipFee, self).save(*args, **kwargs)
  430. today = datetime.date.today()
  431. if self.start_date <= today and today <= self.end_date:
  432. self.member.status = self.member.MEMBER_STATUS_MEMBER
  433. self.member.save()
  434. return ret
  435. def __unicode__(self):
  436. return '%s - %s - %i€' % (self.member, self.start_date, self.amount)
  437. class Meta:
  438. verbose_name = 'cotisation'
  439. class LdapUser(ldapdb.models.Model):
  440. # "ou=users,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  441. base_dn = settings.LDAP_USER_BASE_DN
  442. object_classes = [b'inetOrgPerson', b'organizationalPerson', b'person',
  443. b'top', b'posixAccount', b'ldapPublicKey']
  444. uid = CharField(db_column=b'uid', unique=True, max_length=255)
  445. nick_name = CharField(db_column=b'cn', unique=True, primary_key=True,
  446. max_length=255)
  447. first_name = CharField(db_column=b'givenName', max_length=255)
  448. last_name = CharField(db_column=b'sn', max_length=255)
  449. display_name = CharField(db_column=b'displayName', max_length=255,
  450. blank=True)
  451. password = CharField(db_column=b'userPassword', max_length=255)
  452. uidNumber = IntegerField(db_column=b'uidNumber', unique=True)
  453. gidNumber = IntegerField(db_column=b'gidNumber', default=2000)
  454. # Used by Sympa for logging in.
  455. mail = CharField(db_column=b'mail', max_length=255, blank=True,
  456. unique=True)
  457. homeDirectory = CharField(db_column=b'homeDirectory', max_length=255,
  458. default='/tmp')
  459. loginShell = CharField(db_column=b'loginShell', max_length=255,
  460. default='/bin/bash')
  461. sshPublicKey = ListField(db_column=b'sshPublicKey', default=[])
  462. def __unicode__(self):
  463. return self.display_name
  464. class Meta:
  465. managed = False # Indique à Django de ne pas intégrer ce model en base
  466. # class LdapGroup(ldapdb.models.Model):
  467. # "ou=groups,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  468. # base_dn = settings.LDAP_GROUP_BASE_DN
  469. # object_classes = [b'posixGroup']
  470. # gid = IntegerField(db_column=b'gidNumber', unique=True)
  471. # name = CharField(db_column=b'cn', max_length=200, primary_key=True)
  472. # members = ListField(db_column=b'memberUid')
  473. # def __unicode__(self):
  474. # return self.name
  475. # class Meta:
  476. # managed = False # Indique à Django de ne pas intégrer ce model en base
  477. @receiver(pre_save, sender=Member)
  478. def define_username(sender, instance, **kwargs):
  479. """
  480. Lors de la sauvegarde d'un membre. Si le champ username n'est pas définit,
  481. le calcul automatiquement en fonction du nom et du prénom
  482. """
  483. if not instance.username and not instance.pk:
  484. instance.username = get_automatic_username(instance)
  485. @receiver(pre_save, sender=LdapUser)
  486. def define_display_name(sender, instance, **kwargs):
  487. """
  488. Lors de la sauvegarde d'un utilisateur Ldap, le champ display_name est la
  489. concaténation de first_name et last_name
  490. """
  491. if not instance.display_name:
  492. instance.display_name = '%s %s' % (instance.first_name,
  493. instance.last_name)
  494. class RowLevelPermission(Permission):
  495. offer = models.ForeignKey(
  496. 'offers.Offer', null=True, verbose_name="Offre",
  497. help_text="Offre dont l'utilisateur est autorisé à voir et modifier les membres et les abonnements.")
  498. description = models.TextField(blank=True)
  499. def save(self, *args, **kwargs):
  500. """
  501. Lors de la sauvegarde d'une RowLevelPermission. Si le champ codename n'est pas définit,
  502. le calcul automatiquement.
  503. """
  504. if not self.codename:
  505. self.codename = self.generate_codename()
  506. return super(RowLevelPermission, self).save(*args, **kwargs)
  507. def generate_codename(self):
  508. """
  509. Calcule le codename automatiquement en fonction du name.
  510. """
  511. # Convertit en ASCII. Convertit les espaces en tirets. Enlève les caractères qui ne sont ni alphanumériques, ni soulignements, ni tirets. Convertit en minuscules. Les espaces en début et fin de chaîne sont aussi enlevés
  512. codename = slugify(self.name)
  513. # Maximum de 30 char
  514. codename = codename[:30]
  515. # Recherche dans les membres existants un codename identique
  516. perm = Permission.objects.filter(codename=codename)
  517. base_codename = codename
  518. incr = 2
  519. # Tant qu'une permission est trouvée, incrémente un entier à la fin
  520. while perm:
  521. codename = base_codename + str(incr)
  522. perm = Permission.objects.filter(codename=codename)
  523. incr += 1
  524. return codename
  525. class Meta:
  526. verbose_name = 'permission fine'
  527. verbose_name_plural = 'permissions fines'
  528. RowLevelPermission._meta.get_field('codename').blank = True
  529. RowLevelPermission._meta.get_field('codename').help_text = 'Laisser vide pour le générer automatiquement'
  530. RowLevelPermission._meta.get_field('content_type').help_text = "Garder 'abonnement' pour une utilisation normale"