· 6 years ago · Apr 15, 2020, 11:16 PM
1import json
2import os.path
3import requests
4import urllib
5import copy
6import pytz
7from django.shortcuts import render, redirect
8from datetime import datetime, timedelta
9from django.shortcuts import render
10from django.http import HttpResponse
11from pprint import pprint as pp
12from ..forms import *
13from ..helpers.auth import *
14from ..helpers.helpers import *
15from django.contrib.auth.hashers import make_password
16from django.utils.dateparse import parse_date, parse_datetime
17
18from django.core.serializers.json import DjangoJSONEncoder
19
20BASE = os.path.dirname(os.path.abspath(__file__))
21
22'''
23This is the primary method to load the create_class page. All of the other methods in this file are helper methods to this method.
24This method has 4 primary cases. Case 1, the page is being loaded for the first time, do nothing. Case 2, the page is being loaded
25for the first time but has been passed a parameter in the URL, 'class_id'. The page should load with the data for this class
26pre-loaded. For case 3 and 4, the user has clicked the submit button on the page which has reloaded the page, all data from the form
27has been passed to us in 'request.POST'. Case 3, the passed form data can form a valid class, we will push it to the database. Case 4,
28the passed form data can NOT form a valid class, we will reload the page with the previous form data and give the necessary error
29messages.
30'''
31def create_class(request, class_id=None):
32
33 potential_locations = ["Room 1", "Room 2", "Room 3"]
34
35 initial_data = {'id': -1, 'creator': "", 'guild': "", 'prerequisites': "", 'acquired_certification': "", 'is_teaching_certs': False} # -1 and "" are signals the code and template are designed to recognize as meaning "create a new class instead of editing one" and "this is the default no data to preload case", respectively
36 post_data = request.POST
37 alerts = []
38
39 # Gathering API data
40
41 users = requests.get('http://root:8002/api/users/').json()
42 guilds = requests.get('http://root:8002/api/guilds/').json()
43 certifications = requests.get('http://root:8002/api/certifications/').json()
44 all_classes = requests.get('http://root:8002/api/classes/').json()
45
46 true_certs = [] # A certification that holds only one machine which has the same name as the cert name, is actually a 'machine_cert', effecitvely a placeholder to keep track of machines. Here we'll split our certs into true_certs and machine_certs.
47 machine_certs = []
48
49 for cert in certifications:
50 if len(cert['machines']) == 1 and cert['name'] == cert['machines'][0]:
51 machine_certs.append(cert)
52 else:
53 true_certs.append(cert)
54
55 required_certs = copy.deepcopy(true_certs)
56 will_gain_certs = copy.deepcopy(true_certs)
57
58 # Determine what case we're in
59
60 if (len(post_data) == 0 and class_id == None): # Case 1 : The page is being loaded for the first time, there is no data to preload.
61 temp = 0 # do nothing
62
63 elif (len(post_data) == 0 and class_id != None): # Case 2: The page is to be preloaded with the data of the class with the given class_id. When submitted, we should edit that class in the database rather then make a new class.
64 initial_data = prepare_given_id_form_data(class_id, alerts, users, guilds, all_classes, required_certs, will_gain_certs, true_certs)
65
66 else:
67
68 is_data_valid, data_for_database, data_id = check_validity(post_data, alerts)
69
70 if (is_data_valid): # Case 3: The user clicked submit on the previous form, and all data was valid. Push this class to the database.
71 # Create a new class or edit an existing one
72 if (data_id == -1): # Create a new class
73 url = 'http://root:8002/api/classes/'
74 response = requests.post(url, data_for_database)
75
76 else: # Edit the existing class with this data_id
77 json_data_for_database = json.dumps(data_for_database, cls=DjangoJSONEncoder) # BUG FIX: If a key in a dictionary only holds an empty list, python will drop it rather then send it along. But we sometimes want to overwrite manytomany fields in the model with an empty list. This transformation avoids the keys with empty lists from being dropped.
78 url = 'http://root:8002/api/classes/' + str(data_id) + '/'
79 response = requests.patch(url, json_data_for_database, headers={'Content-type': 'application/json'})
80
81 # Prepare success or error message for class creation
82 if (response.status_code < 300 and response.status_code >= 200):
83
84 if (data_id == -1):
85 alerts.append({'type': 'success', 'message': 'Class created!'})
86 else:
87 alerts.append({'type': 'success', 'message': 'Class edited!'})
88
89 else:
90 alerts.append({'type': 'error', 'message': 'Something went wrong when sending this class to the database, response status code: ' + str(response.status_code)})
91
92 # Reload all classes to include the new/updated class in the calendar
93 all_classes = requests.get('http://root:8002/api/classes/').json()
94
95 else: # Case 4: The user clicked submit on the previous form, BUT the data was invalid. Give the proper error messages and reload the previous data.
96 initial_data = prepare_passed_form_data(post_data, required_certs, will_gain_certs, users, guilds, true_certs)
97
98 # Handle features that exist in all cases
99
100 calendar_data = setup_calendar(all_classes)
101
102 # Create the page
103
104 return render(request, 'classes_pages/create_class.html', {'user': get_user(request), "calendar_data": calendar_data, "users": users, "guilds": guilds, "pre_certs": required_certs, "acq_certs": will_gain_certs, "initial_data": initial_data, "alerts": alerts, "potential_locations": potential_locations})
105
106'''
107This class will convert the data we've received from the previous form and convert it into 'initial_data', data set to be passed to
108the template to display
109Part of case 4
110'''
111def prepare_passed_form_data(form_data, remaining_required_certs, remaining_will_gain_certs, all_users, all_guilds, all_certifications):
112
113 list_holding_only_the_none_option = ['']
114
115 # Move data from form_data to variables
116
117 id = int(form_data['id'])
118 name = form_data['name']
119 description = form_data['description']
120 date = form_data['date']
121 start_time = form_data['start_time']
122 end_time = form_data['end_time']
123 location = form_data['location']
124 capacity = form_data['capacity']
125 teacher_id = form_data['teacher']
126 guild_id = form_data['guild']
127 required_certs = form_data.getlist('required_certs')
128 will_gain_certs = form_data.getlist('will_gain_certs')
129 due = form_data['due']
130
131 if 'is_teaching_certs' in form_data: # The value "true" will be passed if the checkbox associated with this is checked. But if it's left unchecked, the key doesn't appear in form_data.
132 is_teaching_certs = True
133 else:
134 is_teaching_certs = False
135
136 # Handle various data conversions
137
138 if (due != ""):
139 due = float(due)
140
141 if (capacity != ""):
142 capacity = int(capacity)
143
144 if (required_certs == list_holding_only_the_none_option): # Only the 'none' cert option was selected
145 required_certs = ""
146 else:
147 required_certs = convert_list_of_certs_to_list_of_dictionaries(required_certs, remaining_required_certs, all_certifications, True)
148
149 if (will_gain_certs == list_holding_only_the_none_option):
150 will_gain_certs = ""
151 else:
152 will_gain_certs = convert_list_of_certs_to_list_of_dictionaries(will_gain_certs, remaining_will_gain_certs, all_certifications, True)
153
154 if (guild_id != ""):
155
156 guild_id = int(guild_id)
157
158 # Convert guild_id to be a dictionary of the guilds id and name
159 for guild in all_guilds:
160 if guild['id'] == guild_id:
161
162 dict = {}
163 dict['id'] = guild_id
164 dict['name'] = guild['name']
165
166 guild_id = dict
167 all_guilds.remove(guild)
168
169 break
170
171 if (teacher_id != ""):
172
173 teacher_id = int(teacher_id)
174
175 # Convert teacher_id to be a dictionary of the users id, first name, and last name
176 for user in all_users:
177 if user['id'] == teacher_id:
178
179 dict = {}
180 dict['id'] = teacher_id
181 dict['first_name'] = user['first_name']
182 dict['last_name'] = user['last_name']
183
184 teacher_id = dict
185 all_users.remove(user)
186
187 break
188
189 initial_data = {'id': -1,
190 'name': name,
191 'description': description,
192 'date': date,
193 'start_time': start_time,
194 'end_time': end_time,
195 'guild': guild_id,
196 'cost': due,
197 'prerequisites': required_certs,
198 'acquired_certification': will_gain_certs,
199 'is_teaching_certs': is_teaching_certs,
200 'capacity': capacity,
201 'creator': teacher_id}
202
203 return initial_data
204
205'''
206This is a helper method to prepare_passed_form_data() and prepare_given_id_form_data()
207This method will take in a list of certification ids, convert them into a dictionary object that the template can use to list off all
208relevant information on the certification, and then return that dictionary object. At the same time, it will remove any certs it adds
209to the dictionary from the 'cert_list_to_remove_elements_from' object. It does this to avoid these certifications being listed by the
210templace twice
211Parameters
212 'string_list': A list that holds a list of certification ids, but all of the certification ids are a string object rather then an int
213 'list_is_of_strings': A boolean variable. If true, the passed list holds numbers as strings, like ['1']. If false, it just holds number, like [1]
214Part of case 2 and 4
215'''
216def convert_list_of_certs_to_list_of_dictionaries(string_list, cert_list_to_remove_elements_from, all_certifications, list_is_of_strings):
217
218 ids_list = []
219 dict_list = []
220
221 # Convert our list of strings, 'string_list", to a list of ints
222 if (list_is_of_strings):
223 for index in range(len(string_list)):
224 if string_list[index] != '': # If this element is not the none option which has value ''
225 ids_list.append(int(string_list[index]))
226 else:
227 ids_list = string_list
228
229 # Use 'ids_list' to form 'dict_list', a list of dictionaries containing a cert id and its associated name
230 for id in ids_list:
231 for cert in all_certifications:
232 if cert['id'] == id:
233
234 dict = {}
235 dict['id'] = id
236 dict['name'] = cert['name']
237
238 dict_list.append(dict)
239
240 cert_list_to_remove_elements_from.remove(cert) # On the page we'll list every cert in the prerequisite/acquired cert list, and in the passed prerequisite/acquired cert list. To keep from listing the cert twice, any added to the latter is removed from the former here.
241
242
243 return dict_list
244from pprint import pprint as pp
245
246
247'''
248This class will take in a 'class_id', and will then prepare all of the data from that class to be send to the template and displayed
249Part of case 2
250'''
251def prepare_given_id_form_data(class_id, alerts, all_users, all_guilds, all_classes, remaining_required_certs, remaining_will_gain_certs, all_certifications):
252
253 edit_class = None
254 guild_dict = None
255 creator_dict = None
256 capacity = ""
257
258 # Find this class
259 for this_class in all_classes:
260 if this_class['id'] == class_id:
261 edit_class = this_class
262
263 # If no class with this id exists...
264 if edit_class == None:
265 alerts.append({'type': 'error', 'message': 'The class you are trying to edit does not exist'})
266 return
267
268 ## Start handling various data conversions
269
270 if (edit_class['capacity'] == None):
271 capacity = ""
272 elif (edit_class['capacity'] == 10000): # If the teacher didn't provide a capacity (assumed due to having no capacity limit) we round that up to 10000. So 10000 should typically be autp-generated, make it appear blank (as the creator put it in as) if this shows up.
273 capacity = ""
274 else:
275 capacity = edit_class['capacity']
276
277 # Create a dictionary to represent this classes guild
278 if edit_class['guild'] == None:
279 guild_dict = "" # The template is designed to recognize "" as meaning no data
280 else:
281 for guild in all_guilds:
282 if guild['id'] == edit_class['guild']:
283
284 guild_dict = {}
285 guild_dict['id'] = guild['id']
286 guild_dict['name'] = guild['name']
287
288 all_guilds.remove(guild)
289
290 break
291
292 # Create a dictionary to represent this classes creator
293 if edit_class['creator'] == None:
294 creator_dict = ""
295 else:
296 for user in all_users:
297 if user['id'] == edit_class['creator']:
298
299 creator_dict = {}
300 creator_dict['id'] = user['id']
301 creator_dict['first_name'] = user['first_name']
302 creator_dict['last_name'] = user['last_name']
303
304 all_users.remove(user)
305
306 break
307
308 ## Handle date and times
309
310 date_str_time = parse_datetime(edit_class['start_time'])
311 date_end_time = parse_datetime(edit_class['end_time'])
312
313 ''' This will take a number that can be two digits, ##, or 1 digit, #. If it's only one digit, it converts it to 0# '''
314 # Why does this exist? If the time out of my date is 07:03, i get hour=7 and minute=3, which will produce the time 7:3. I need to add 0s here to get 07:03.
315 def x_to_0x_converter(x):
316 x = str(x)
317
318 if len(x) == 1:
319 x = '0'+x
320
321 return x
322
323 date = str(date_str_time.year) + '-' + x_to_0x_converter(date_str_time.month) + '-' + x_to_0x_converter(date_str_time.day)
324
325 date_str_time = x_to_0x_converter(date_str_time.hour) + ':' + x_to_0x_converter(date_str_time.minute)
326 date_end_time = x_to_0x_converter(date_end_time.hour) + ':' + x_to_0x_converter(date_end_time.minute)
327
328 ## Handle certs
329
330 required_certs = edit_class['prerequisites']
331 will_gain_certs = edit_class['acquired_certifications']
332 is_teaching_certs = False
333
334 if (len(edit_class['teaching_certs']) > 0): # Every class will either have "will_gain_certs" or "teaching_certs", and the other will be empty. Both are the same, but the latter authorizes students to teach this cert as well.
335 will_gain_certs = edit_class['teaching_certs']
336 is_teaching_certs = True
337
338 required_certs = convert_list_of_certs_to_list_of_dictionaries(required_certs, remaining_required_certs, all_certifications, False)
339 will_gain_certs = convert_list_of_certs_to_list_of_dictionaries(will_gain_certs, remaining_will_gain_certs, all_certifications, False)
340
341 ## Prepare initial data to return
342
343 initial_data = {'id': class_id,
344 'name': edit_class['name'],
345 'description': edit_class['description'],
346 'date': date,
347 'start_time': date_str_time,
348 'end_time': date_end_time,
349 'guild': guild_dict,
350 'cost': edit_class['cost'],
351 'prerequisites': required_certs,
352 'acquired_certification': will_gain_certs,
353 'is_teaching_certs': is_teaching_certs,
354 'capacity': capacity,
355 'creator': creator_dict}
356
357 return initial_data
358
359'''
360Will check that the passed 'form_data' is valid
361Data requirements
362 'name': Exists and is non-blank
363 'description': Exists, is non-blank, and is not purely composed of spaces
364 'date': Exists and is non-blank
365 * For 'data', 'start_time', and 'end_time', it's impossible for the form to send half-complete data or data that includes letters.
366 'start_time': Exists and is non-blank
367 'end_time': Exists and is non-blank
368Parameters
369 'alerts': A list that will hold dictionary objects defining alerts for the template. Will not be returned, instead is a pointer to the same object as what caller method sent.
370Part of case 3 and 4
371'''
372def check_validity(form_data, alerts):
373
374 this_data_is_valid = True
375
376 ## Moving form_data into variables
377
378 id = int(form_data['id'])
379 name = form_data['name']
380 description = form_data['description']
381 date = form_data['date'] # Note: date, start_time, and end_time have all either been entered not at all and are "", or are fully entered. There is no middle ground of being half-filled in.
382 start_time = form_data['start_time']
383 end_time = form_data['end_time']
384 location = form_data['location']
385 capacity = form_data['capacity']
386 teacher = form_data['teacher']
387 guild = form_data['guild']
388 required_certs = form_data.getlist('required_certs')
389 will_gain_certs = form_data.getlist('will_gain_certs')
390 due = form_data['due']
391 location = form_data['location']
392
393 # Prepping some variabiables
394
395 if (teacher != ''):
396 teacher_id = int(teacher)
397 else:
398 teacher_id = None
399
400 date_str_time = parse_datetime(date + "T" + start_time + ":00Z")
401 date_end_time = parse_datetime(date + "T" + end_time + ":00Z")
402
403 timezone = pytz.timezone("UTC")
404 date_current = timezone.localize(datetime.now())
405
406 if 'is_teaching_certs' in form_data: # The value "true" will be passed if the checkbox associated with this is checked. But if it's left unchecked, the key doesn't appear in form_data.
407 is_teaching_certs = True
408 else:
409 is_teaching_certs = False
410
411 ## Checking validity
412
413 if (name == "" or name.strip() == ""):
414 this_data_is_valid = False
415 alerts.append({'type': 'error', 'message': 'A class name is required'})
416
417 if (description == "" or description.strip() == ""):
418 this_data_is_valid = False
419 alerts.append({'type': 'error', 'message': 'A description is required'})
420
421 if (date == ""):
422 this_data_is_valid = False
423 alerts.append({'type': 'error', 'message': 'A class date is required'})
424
425 if (start_time == ""):
426 this_data_is_valid = False
427 alerts.append({'type': 'error', 'message': 'A class start time is required'})
428
429 if (end_time == ""):
430 this_data_is_valid = False
431 alerts.append({'type': 'error', 'message': 'A class end time is required'})
432
433 if (date_str_time != None and date_end_time != None):
434
435 if (date_str_time < (date_current - timedelta(days=1))): # Subtracting 1 day from the current date to avoid any potential timezone issues
436 this_data_is_valid = False
437 alerts.append({'type': 'error', 'message': 'The class start time is in the past'})
438
439 if (date_end_time < date_str_time):
440 this_data_is_valid = False
441 alerts.append({'type': 'error', 'message': "The end time of this class is before the start time"})
442
443 if (will_gain_certs != [''] and will_gain_certs != []): # A cert other then none was selected
444
445 if (teacher_id == None):
446 this_data_is_valid = False
447 alerts.append({'type': 'error', 'message': "No teacher was selected for this class"})
448
449 else:
450
451 url = 'http://root:8002/api/users/' + str(teacher_id)
452 teacher_dict = requests.get(url).json()
453
454 if teacher_dict['teaching_machines'] != None:
455 can_teach_certs = set(teacher_dict['teaching_machines'])
456 else:
457 can_teach_certs = []
458
459 can_teach_machines = []
460 for mach_id in can_teach_certs:
461 url = 'http://root:8002/api/certifications/' + str(mach_id)
462 machines = requests.get(url).json()
463 if machines:
464 can_teach_machines = can_teach_machines + machines['machines']
465
466 for cert_id in will_gain_certs:
467
468 if cert_id == '':
469 continue
470
471 url = 'http://root:8002/api/certifications/' + str(cert_id)
472 cert = requests.get(url).json()
473
474 cert_name = cert['name']
475 cert_machines = cert['machines']
476
477 for machine in cert_machines:
478 if machine not in can_teach_machines:
479 this_data_is_valid = False
480 alerts.append({'type': 'error', 'message': "The teacher is not authorized to teach the certification " + cert_name})
481 break
482
483 # If atleast one of the above if-statements triggered
484 if (this_data_is_valid == False):
485 return False, 0, 0
486
487 ## Generating python datetime objects for start and end times
488
489 year = int(date[0:4])
490 month = int(date[5:7])
491 day = int(date[8:])
492
493 start_hour = int(start_time[0:2])
494 start_minute = int(start_time[3:])
495
496 end_hour = int(end_time[0:2])
497 end_minute = int(end_time[3:])
498
499 start_datetime = datetime(year, month, day, start_hour, start_minute)
500 end_datetime = datetime(year, month, day, end_hour, end_minute)
501
502 ## Prepare data into the form needed for the post to database
503
504 fixed_required_certs = []
505 fixed_will_gain_certs = []
506 fixed_teaching_certs = []
507
508 for element in required_certs: # Our certification lists are currently in the form ['#', '#'], and may contain the none option of ''. We need to correct those to hold only integer ids of certifications.
509 if element != '':
510 fixed_required_certs.append(int(element))
511
512 for element in will_gain_certs:
513 if element != '':
514 fixed_will_gain_certs.append(int(element))
515
516 if (is_teaching_certs):
517 fixed_teaching_certs = fixed_will_gain_certs
518 fixed_will_gain_certs = []
519
520 if capacity == '': # Other systems require that capacity exist. If the class creator states that there is no capacity limit, just treat it as a very large number
521 capacity = 10000
522
523 data = {"name": name,
524 "description": description,
525 "start_time": start_datetime,
526 "end_time": end_datetime,
527 "guild": guild,
528 "cost": due,
529 "prerequisites": fixed_required_certs,
530 "acquired_certifications": fixed_will_gain_certs,
531 "teaching_certs": fixed_teaching_certs,
532 "capacity": capacity,
533 "creator": teacher,
534 "location": location}
535
536 if location == '':
537 del data['location']
538
539 return True, data, id
540
541'''
542This class will prepare the data for the calendar component of the create_class template
543Return value
544 A dictionary with the following properties
545 * The dictionary holds 7 sub-dictionaries, each correlating to days of the week monday to sunday of the current week
546 * Each of these sub-dictionaries will contain a list of all classes from 'all_classes' that fall on that date
547Part of all cases
548'''
549def setup_calendar(all_classes):
550
551 calendar_data = []
552
553 today_weekday_num = datetime.today().weekday()
554 weekday_names = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
555
556 # Create the 7 code calendar objects, the 7 days of the week
557 for x in range(0,7):
558
559 this_data = {}
560
561 date = datetime.now() - timedelta(today_weekday_num - x)
562 matching_data = [date.year, date.month, date.day]
563
564 this_data["weekday"] = weekday_names[x]
565 this_data["classes"] = []
566 this_data["date_string"] = date.strftime('%b %d')
567 this_data["matching_data"] = matching_data #This exists purely to assist future calculations
568
569 calendar_data.append(this_data)
570
571 # Find any classes that fall on the dataes in calendar_data and add them in
572 for this_class in all_classes:
573
574 date = parse_datetime(this_class['start_time'])
575
576 year = date.year
577 month = date.month
578 day = date.day
579
580 matching_data = [year, month, day]
581
582 for day_of_the_week in calendar_data:
583 if (day_of_the_week["matching_data"] == matching_data):
584
585 modified_class_data = {}
586
587 modified_class_data["title"] = this_class["name"]
588 modified_class_data["id"] = this_class["id"]
589 modified_class_data["location"] = this_class["location"]
590
591 ## Converting our time notation from python format (24-hour clock) to AM, PM format
592
593 # Setup
594
595 date_start = parse_datetime(this_class['start_time'])
596 date_end = parse_datetime(this_class['end_time'])
597
598 start_hour = date_start.hour
599 start_minutes = date_start.minute
600 end_hour = date_end.hour
601 end_minutes = date_end.minute
602
603 # Handling minutes
604
605 def minutes_int_to_str(minutes):
606
607 if (minutes == 0):
608 return ""
609 elif (minutes < 10):
610 return ":0" + str(minutes)
611 else: #start_minutes >= 10
612 return ":" + str(minutes)
613
614 start_minutes_str = minutes_int_to_str(start_minutes)
615 end_minutes_str = minutes_int_to_str(end_minutes)
616
617 # Handling hour and AM, PM
618
619 def total_int_to_str(hour, minutes_str):
620
621 if (hour == 0):
622 return "12" + minutes_str + "am"
623 elif (hour < 12):
624 return str(hour) + minutes_str + "am"
625 elif (hour == 12):
626 return "12" + minutes_str + "pm"
627 elif (hour < 24):
628 return str(hour - 12) + minutes_str + "pm"
629
630 start_time_str = total_int_to_str(start_hour, start_minutes_str)
631 end_time_str = total_int_to_str(end_hour, end_minutes_str)
632
633 modified_class_data["start_time"] = start_time_str
634 modified_class_data["end_time"] = end_time_str
635
636 day_of_the_week["classes"].append(modified_class_data)
637
638 return calendar_data