Coverage for website/members/models/membership.py: 46.15%

40 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2025-08-14 10:31 +0000

1import datetime 

2 

3from django.conf import settings 

4from django.core.exceptions import ValidationError 

5from django.db import models 

6from django.utils import timezone 

7from django.utils.translation import gettext_lazy as _ 

8from django.utils.translation import pgettext_lazy 

9from utils.snippets import overlaps 

10 

11 

12class Membership(models.Model): 

13 MEMBER = "member" 

14 BENEFACTOR = "benefactor" 

15 HONORARY = "honorary" 

16 

17 MEMBERSHIP_TYPES = ( 

18 (MEMBER, _("Member")), 

19 (BENEFACTOR, _("Benefactor")), 

20 (HONORARY, _("Honorary Member")), 

21 ) 

22 

23 type = models.CharField( 

24 max_length=40, 

25 choices=MEMBERSHIP_TYPES, 

26 verbose_name=_("Membership type"), 

27 ) 

28 

29 user = models.ForeignKey( 

30 settings.AUTH_USER_MODEL, 

31 on_delete=models.CASCADE, 

32 verbose_name=_("User"), 

33 ) 

34 

35 since = models.DateField( 

36 verbose_name=_("Membership since"), 

37 help_text=_("The date the member started holding this membership."), 

38 default=datetime.date.today, 

39 ) 

40 

41 until = models.DateField( 

42 verbose_name=_("Membership until"), 

43 help_text=_("The date the member stops holding this membership."), 

44 blank=True, 

45 null=True, # This is only for honorary members 

46 ) 

47 

48 study_long = models.BooleanField( 

49 verbose_name=_("Study long"), 

50 help_text="Whether the member has paid to be member throughout their studies.", 

51 default=False, 

52 ) 

53 

54 def __str__(self): 

55 s = _("Membership of type {} for {} ({}) starting {}").format( 

56 self.get_type_display(), 

57 self.user.get_full_name(), 

58 self.user.username, 

59 self.since, 

60 ) 

61 if self.until is not None: 

62 s += pgettext_lazy("Membership until x", " until {}").format(self.until) 

63 return s 

64 

65 def clean(self): 

66 super().clean() 

67 

68 errors = {} 

69 if self.until and (not self.since or self.until < self.since): 

70 raise ValidationError({"until": "End date can't be before start date"}) 

71 

72 memberships = self.user.membership_set.all() 

73 if self.since is not None and overlaps(self, memberships): 

74 errors.update( 

75 { 

76 "since": "A membership already exists for that period.", 

77 "until": "A membership already exists for that period.", 

78 } 

79 ) 

80 

81 if self.type != self.HONORARY and self.until is None: 

82 errors.update({"until": "A non-honorary membership must have an end date."}) 

83 

84 if self.type == self.BENEFACTOR and self.study_long: 

85 errors.update( 

86 {"study_long": "Benefactors cannot have a study long membership."} 

87 ) 

88 

89 if errors: 

90 raise ValidationError(errors) 

91 

92 def is_active(self): 

93 today = timezone.now().date() 

94 return self.since <= today and (not self.until or self.until > today)