models.py 9.39 KB
Newer Older
frekk's avatar
frekk committed
1
from django.db import models
2
from django.db.models import F
3
from django.core.validators import RegexValidator
4
from django.core.management.utils import get_random_string
frekk's avatar
frekk committed
5
from django.urls import reverse
6
from django.utils import timezone
frekk's avatar
frekk committed
7

8
from squarepay.dispense import get_item_price
Zack Wong's avatar
Zack Wong committed
9
import subprocess
10

Zack Wong's avatar
Zack Wong committed
11
12
import ldap

13
14
15
"""
dictionary of membership types & descriptions, should be updated if these are changed in dispense.
"""
16
MEMBERSHIP_TYPES = {
Zack Wong's avatar
Zack Wong committed
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
	'oday': {
		'dispense':'pseudo:11',
		'desc':'O\' Day Special - first time members only',
		'is_guild':True,
		'is_student':True,
		'must_be_fresh':True,
	},
	'student_and_guild': {
		'dispense':'pseudo:10',
		'desc':'Student and UWA Guild member',
		'is_guild':True,
		'is_student':True,
		'must_be_fresh':False,
	},
	'student_only': {
		'dispense':'pseudo:9',
		'desc':'Student and not UWA Guild member',
		'is_guild':False,
		'is_student':True,
		'must_be_fresh':False,
	},
	'guild_only': {
		'dispense':'pseudo:8',
		'desc':'Non-Student and UWA Guild member',
		'is_guild':True,
		'is_student':False,
		'must_be_fresh':False,
	},
	'non_student': {
		'dispense':'pseudo:7',
		'desc':'Non-Student and not UWA Guild member',
		'is_guild':False,
		'is_student':False,
		'must_be_fresh':False,
	},
	'lifer': {
		'dispense':'',
		'desc':'Life member',
		'is_guild':False,
		'is_student':False,
		'must_be_fresh':False,
	}
59
60
61
}

def get_membership_choices(is_renew=None, get_prices=True):
Zack Wong's avatar
Zack Wong committed
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
	"""
	turn MEMBERSHIP_TYPES into a list of choices used by Django
	also dynamically fetch the prices from dispense (if possible)
	"""
	choices = []
	for key, val in MEMBERSHIP_TYPES.items():
		if (val['must_be_fresh'] and is_renew == True):
			# if you have an account already, you don't qualify for the fresher special
			continue
		if (val['dispense'] == '' and is_renew == False):
			# free memberships can only apply to life members, and they will have an existing membership somewhere
			# so this option is only displayed on the renewal form
			continue
		else:
			if get_prices:
				price = get_item_price(val['dispense'])
			else:
				price = None

			if price is not None:
				desc = "%s ($%1.2f)" % (val['desc'], price / 100.0)
				choices += [(key, desc)]
			else:
				# don't display the price
				choices += [(key, val['desc'])]

	return choices
89

90
def get_membership_type(member):
Zack Wong's avatar
Zack Wong committed
91
92
93
94
95
96
97
98
99
100
	best = 'non_student'
	is_fresh = member.memberships.all().count() == 0
	for i, t in MEMBERSHIP_TYPES.items():
		if (t['must_be_fresh'] == is_fresh and t['is_student'] == member.is_student and t['is_guild'] == member.is_guild):
			best = i
			break
		elif (t['is_student'] == member.is_student and t['is_guild'] == member.is_guild):
			best = i
			break
	return best
101

102
def make_pending_membership(member):
103
104
105
106
107
108
109
110
111
112
113
114
	""" creates or updates and returns a pending membership for the given member """
	latest = member.get_last_renewal()
	if latest is None or latest.date_submitted.year != timezone.now().year:
		# create a Membership if none exists already for this year
		latest = Membership(member=member)
		latest.membership_type = get_membership_type(member)

	# otherwise update the existing membership and mark as pending
	latest.approved = False
	latest.date_submitted = timezone.now()

	return latest
115
116

def make_token():
Zack Wong's avatar
Zack Wong committed
117
	return get_random_string(128)
118

119
PAYMENT_METHODS = [
Zack Wong's avatar
Zack Wong committed
120
121
122
123
124
125
	('dispense', 'Existing dispense credit'),
	('cash', 'Cash (in person)'),
	('card', 'Tap-n-Go via Square (in person)'),
	('online', 'Online payment via Square'),
	('eft', 'Bank transfer'),
	('', 'No payment')
126
]
frekk's avatar
frekk committed
127

Zack Wong's avatar
Zack Wong committed
128
129
130
131
132
133
134
ID_TYPES = [
	('student', 'Student ID'),
	('drivers', 'Drivers licence'),
	('passport', 'Passport'),
	('Other', 'Other ID'),
]

Zack Wong's avatar
Zack Wong committed
135
136
137
138
139
ACCOUNT_STATUS = [
		'enabled',
		'disabled',
		'no account'
		]
Zack Wong's avatar
Zack Wong committed
140
141
142



143
class IncAssocMember (models.Model):
Zack Wong's avatar
Zack Wong committed
144
145
146
147
148
	"""
	Member record for data we are legally required to keep under Incorporations Act (and make available to members upon request)
	Note: these data should only be changed administratively or with suitable validation since it must be up to date & accurate.
	"""

Zack Wong's avatar
Zack Wong committed
149
150
151
	first_name      = models.CharField ('Given Name', max_length=200)
	last_name       = models.CharField ('Other Names', max_length=200)
	email_address   = models.EmailField ('Contact email', blank=False)
Zack Wong's avatar
Zack Wong committed
152
153
154
155
156
	updated         = models.DateTimeField ('IncA. info last updated', auto_now=True)
	created         = models.DateTimeField ('When created', auto_now_add=True)

	def __str__ (self):
		return "%s %s <%s>" % (self.first_name, self.last_name, self.email_address)
Zack Wong's avatar
Zack Wong committed
157

Zack Wong's avatar
Zack Wong committed
158
159
160
	class Meta:
		verbose_name = "Incorporations Act member data"
		verbose_name_plural = verbose_name
frekk's avatar
frekk committed
161

162
class Member (IncAssocMember):
Zack Wong's avatar
Zack Wong committed
163
164
165
166
167
168
169
170
171
	"""
	Member table: only latest information, one record per member
	Some of this data may be required by the UWA Student Guild. Other stuff is just good to know,
	and we don't _need_ to keep historical data for every current/past member.
	Note: Privacy laws are a thing, unless people allow it then we cannot provide this info to members.
	"""

	# data to be entered by user and validated (mostly) manually
	display_name    = models.CharField ('Display name', max_length=200)
root's avatar
root committed
172
	username        = models.SlugField ('Username', max_length=32, null=True, blank=True, unique=False, validators=[RegexValidator(regex='^[a-z0-9._-]*$')])
Zack Wong's avatar
Zack Wong committed
173
174
175
	phone_number    = models.CharField ('Phone number', max_length=20, blank=False, validators=[RegexValidator(regex='^\+?[0-9() -]+$')])
	is_student      = models.BooleanField ('Student', default=True, blank=True, help_text="Tick this box if you are a current student at a secondary or tertiary institution in WA")
	is_guild        = models.BooleanField ('UWA Guild member', default=True, blank=True)
Zack Wong's avatar
Zack Wong committed
176
177
	id_number       = models.CharField ('Student number or other ID', max_length=255, blank=False, help_text="")
	id_desc			= models.CharField  ('Form of ID provided', max_length=255, blank=False, help_text="Please describe the type of identification provided", choices=ID_TYPES, default="student")
Zack Wong's avatar
Zack Wong committed
178
179
180
181
182
183
184
185

	# data used internally by the system, not to be touched, seen or heard (except when it is)
	member_updated  = models.DateTimeField ('Internal UCC info last updated', auto_now=True)
	login_token     = models.CharField ('Temporary access key', max_length=128, null=True, editable=False, default=make_token)
	email_confirm   = models.BooleanField ('Email address confirmed', null=False, editable=False, default=False)
	studnt_confirm  = models.BooleanField ('Student status confirmed', null=False, editable=False, default=False)
	guild_confirm   = models.BooleanField ('Guild status confirmed', null=False, editable=False, default=False)

Zack Wong's avatar
Zack Wong committed
186
187
	has_account		= models.BooleanField ('Has AD account', null=False, editable=False, default=False)

188
189
190
191
	def get_last_renewal(self):
		""" returns the most recently submitted Membership object """
		return self.memberships.order_by('-date_submitted').first()

Zack Wong's avatar
Zack Wong committed
192
	# account info
Zack Wong's avatar
Zack Wong committed
193
194
195
196
197
198
	def get_uid(self):
		result, uid = subprocess.getstatusoutput(["id", "-u", self.username])
		if (result == 0):
			return uid;
		else:
			return None;
Zack Wong's avatar
Zack Wong committed
199
200
201
202
203
204
205
206
207
208

	def __str__ (self):
		if (self.display_name != "%s %s" % (self.first_name, self.last_name)):
			name = "%s (%s %s)" % (self.display_name, self.first_name, self.last_name)
		else:
			name = self.display_name
		return "[%s] %s" % (self.username, name)

	class Meta:
		verbose_name = "Internal UCC member record"
209

210
class Membership (models.Model):
Zack Wong's avatar
Zack Wong committed
211
212
213
	"""
	Membership table: store information related to individual (successful/accepted) signups/renewals
	"""
frekk's avatar
frekk committed
214

Zack Wong's avatar
Zack Wong committed
215
216
217
218
219
	member          = models.ForeignKey (Member, on_delete=models.CASCADE, related_name='memberships')
	membership_type = models.CharField ('Membership type', max_length=20, blank=True, null=False, choices=get_membership_choices(get_prices=False))
	payment_method  = models.CharField ('Payment method', max_length=10, blank=True, null=True, choices=PAYMENT_METHODS, default=None)
	approved        = models.BooleanField ('Membership approved', default=False)
	approver        = models.ForeignKey (Member, on_delete=models.SET_NULL, null=True, blank=True, related_name='approved_memberships')
220
	date_submitted  = models.DateTimeField ('Date signed up', default=timezone.now)
Zack Wong's avatar
Zack Wong committed
221
222
	date_paid       = models.DateTimeField ('Date of payment', blank=True, null=True)
	date_approved   = models.DateTimeField ('Date approved', blank=True, null=True)
223

Zack Wong's avatar
Zack Wong committed
224
225
	def __str__ (self):
		return "Member [%s] (%s) renewed membership on %s" % (self.member.username, self.member.display_name, self.date_submitted.strftime("%Y-%m-%d"))
226

Zack Wong's avatar
Zack Wong committed
227
228
	def get_dispense_item(self):
		return MEMBERSHIP_TYPES[self.membership_type]['dispense']
229

230
231
232
	def get_pretty_type(self):
		return MEMBERSHIP_TYPES[self.membership_type]['desc']

Zack Wong's avatar
Zack Wong committed
233
234
235
	class Meta:
		verbose_name = "Membership renewal record"
		ordering = ['approved', '-date_submitted']
frekk's avatar
frekk committed
236
237

class TokenConfirmation(models.Model):
Zack Wong's avatar
Zack Wong committed
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
	""" keep track of email confirmation tokens etc. and which field to update """
	member          = models.ForeignKey (Member, on_delete=models.CASCADE, related_name='token_confirmations')
	confirm_token   = models.CharField ('unique confirmation URL token', max_length=128, null=False, default=make_token)
	model_field     = models.CharField ('name of BooleanField to update on parent when confirmed', max_length=40, null=False, blank=False)
	created         = models.DateTimeField ('creation date', auto_now_add=True)

	def mark_confirmed(self):
		""" try to mark as confirmed, if error then silently fail """
		try:
			m = self.member
			setattr(m, self.model_field)
			m.save()
			self.delete()
		except Member.DoesNotExist as e:
			pass

	def get_absolute_url(self):
		return reverse('memberdb:email_confirm', kwargs={'pk': self.id, 'token': self.confirm_token})