models.py 9.42 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
	""" 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
112
	latest.date_approved = None
113
114
115
	latest.date_submitted = timezone.now()

	return latest
116
117

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

120
PAYMENT_METHODS = [
Zack Wong's avatar
Zack Wong committed
121
122
123
124
125
126
	('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')
127
]
frekk's avatar
frekk committed
128

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

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



144
class IncAssocMember (models.Model):
Zack Wong's avatar
Zack Wong committed
145
146
147
148
149
	"""
	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
150
151
152
	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
153
154
155
156
157
	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
158

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

163
class Member (IncAssocMember):
Zack Wong's avatar
Zack Wong committed
164
165
166
167
168
169
170
171
172
	"""
	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
173
	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
174
175
176
	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
177
178
	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
179
180
181
182
183
184
185
186

	# 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
187
188
	has_account		= models.BooleanField ('Has AD account', null=False, editable=False, default=False)

189
190
191
192
	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
193
	# account info
Zack Wong's avatar
Zack Wong committed
194
195
196
197
198
199
	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
200
201
202
203
204
205
206
207
208
209

	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"
210

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

Zack Wong's avatar
Zack Wong committed
216
217
218
219
220
	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')
221
	date_submitted  = models.DateTimeField ('Date signed up', default=timezone.now)
Zack Wong's avatar
Zack Wong committed
222
223
	date_paid       = models.DateTimeField ('Date of payment', blank=True, null=True)
	date_approved   = models.DateTimeField ('Date approved', blank=True, null=True)
224

Zack Wong's avatar
Zack Wong committed
225
226
	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"))
227

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

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

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

class TokenConfirmation(models.Model):
Zack Wong's avatar
Zack Wong committed
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
	""" 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})