new_roster.py
1 |
|
2 |
building of the roster. """ |
3 |
from django.shortcuts import render |
4 |
from collections import OrderedDict |
5 |
import datetime |
6 |
from django.urls import reverse |
7 |
from django.utils.translation import gettext as _ |
8 |
from .models import * |
9 |
import administration |
10 |
|
11 |
def same_day(datetime_a, datetime_b): |
12 |
"""True if both a and b are on the same day, false otherwise.""" |
13 |
return ( |
14 |
datetime_a.day == datetime_b.day and |
15 |
datetime_a.month == datetime_b.month and |
16 |
datetime_a.year == datetime_b.year) |
17 |
|
18 |
def same_daytime(moment, hour, minute): |
19 |
return (moment.minute == minute and moment.hour == hour) |
20 |
|
21 |
def conflicting_events(events): |
22 |
"""Finds conflicting events in the given set, and returns a list of all |
23 |
conflicting events, grouped according to their conflicts in lists.""" |
24 |
conflicts = list() |
25 |
test_conflict = list() |
26 |
#for event in events.order_by("begin_time"): |
27 |
for event in events: |
28 |
if len(test_conflict) == 0: |
29 |
test_conflict.append(event) |
30 |
else: |
31 |
possible_conflict = test_conflict.pop() |
32 |
#print("comparing" + str(possible_conflict) + " - " + str(event)) |
33 |
if possible_conflict.end_time > event.begin_time: # New conflict! |
34 |
test_conflict.append(possible_conflict) |
35 |
test_conflict.append(event) |
36 |
#print(test_conflict) |
37 |
elif len(test_conflict) == 0: # No conflict |
38 |
test_conflict.append(event) |
39 |
else: # No conflict, but previous conflicting events exist |
40 |
test_conflict.append(possible_conflict) |
41 |
conflicts.append(test_conflict.copy()) |
42 |
test_conflict.clear() |
43 |
if len(test_conflict) >= 2: |
44 |
conflicts.append(test_conflict.copy()) |
45 |
return conflicts |
46 |
|
47 |
def replace_conflict(events, conflicts): |
48 |
"""Removes the conflicts from events, and replaces them with a new |
49 |
event. Returns the key number for this conflict set. Expects the conflicts |
50 |
to be in chronological order based on begin_time.""" |
51 |
for conflict in conflicts: |
52 |
events.remove(conflict) |
53 |
conflict_number = 0 |
54 |
for event in events: |
55 |
if event.note == "Conflict " + str(conflict_number): |
56 |
conflict_number += 1 |
57 |
|
58 |
replacement = Event( |
59 |
begin_time = conflicts[0].begin_time, |
60 |
end_time = conflicts[-1].end_time, |
61 |
note = "Conflict " + str(conflict_number), |
62 |
) |
63 |
events.append(replacement) |
64 |
return conflict_number |
65 |
|
66 |
def replace_conflicts(events): |
67 |
"""Searches for all events that are in conflict regarding timespans, and |
68 |
replaces them with a general event. Returns a dictionary with keys referencing |
69 |
a specific conflict block, and the values being a list of the events that are |
70 |
in conflict with each other.""" |
71 |
all_conflicts = conflicting_events(events) |
72 |
conflict_dict = dict() |
73 |
for conflict_list in all_conflicts: |
74 |
conflict_number = replace_conflict(events, conflict_list) |
75 |
conflict_dict[conflict_number] = conflict_list |
76 |
return conflict_dict |
77 |
|
78 |
def create_roster_event(event_type, quarters, content, style="", note=""): |
79 |
return '<td class="{event_type}" style="{style}" {title} rowspan="{quarters}">{content}</td>'.format( |
80 |
event_type=event_type, |
81 |
style=style, |
82 |
quarters=quarters, |
83 |
content=content, |
84 |
title=note) |
85 |
|
86 |
def create_roster_conflict_event(event): |
87 |
quarters = (event.end_time - event.begin_time).seconds // 60 // 15 |
88 |
content = _('<strong>Conflicting events!<br />See <a href="#'+event.note+'">'+event.note+'</a> for more information.</strong>') |
89 |
return create_roster_event("event-conflict", quarters, content) |
90 |
|
91 |
def create_roster_study_event(event): |
92 |
pass # TODO |
93 |
def create_roster_university_event(event): |
94 |
pass # TODO |
95 |
def create_roster_course_event(event): |
96 |
# FIXME: Currently not all users are equipped with a user_data object. |
97 |
# Because of this, reversing the link to the docent's page may throw a |
98 |
# RelatedObjectDoesNotExist error. Until that's resolved, I've wrapped |
99 |
# creating that link in a try catch. |
100 |
docent_link = "" |
101 |
try: |
102 |
docent_link = reverse('administration-user', args=(event.docent.user_data.slug_name(),)) |
103 |
except : |
104 |
pass |
105 |
|
106 |
quarters = (event.end_time - event.begin_time).seconds // 60 // 15 |
107 |
course_link = reverse('courses-course-index', args=(event.course.course.slug_name,)) |
108 |
room_link = reverse('administration-room-detail', args=(str(event.room),)) |
109 |
event_type = "event" |
110 |
|
111 |
content = "{course}<br /> {docent}<br />{begin} - {end}<br /> {room} ({subject})".format( |
112 |
course = '<a href="'+course_link+'">'+str(event.course.course.name)+'</a>', |
113 |
docent = '<a href="'+docent_link+'">'+str(event.docent)+'</a>', |
114 |
begin = event.begin_time.strftime("%H:%M"), |
115 |
end = event.end_time.strftime("%H:%M"), |
116 |
room = '<a href="'+room_link+'">'+str(event.room)+'</a>', |
117 |
subject = event.subject, |
118 |
) |
119 |
|
120 |
style = "background-color: #"+event.course.course.color+"; color: white;" |
121 |
|
122 |
if event.recently_created(): |
123 |
event_type = "event-new" |
124 |
style = "" |
125 |
elif event.recently_updated(): |
126 |
event_type = "event-update" |
127 |
style = "" |
128 |
elif event.note != "": |
129 |
event_type = "event-note" |
130 |
|
131 |
return create_roster_event(event_type, quarters, content, style, event.note) |
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
def first_last_day(events): |
138 |
"""Returns the first and last day of the starting times of the given events.""" |
139 |
earliest = events[0].begin_time |
140 |
latest = events[0].begin_time |
141 |
for event in events: |
142 |
if event.begin_time < earliest: |
143 |
earliest = event.begin_time |
144 |
if event.begin_time > latest: |
145 |
latest = event.begin_time |
146 |
first = datetime.date(earliest.year, earliest.month, earliest.day) |
147 |
last = datetime.date(latest.year, latest.month, latest.day) |
148 |
return first, last |
149 |
|
150 |
|
151 |
|
152 |
|
153 |
def create_roster_for_event(event, conflict=False): |
154 |
"""Determines which function to call to build the roster entry for the given event.""" |
155 |
if conflict and isinstance(event, Event): |
156 |
return create_roster_conflict_event(event) |
157 |
elif isinstance(event, CourseEvent): |
158 |
return create_roster_course_event(event) |
159 |
elif isinstance(event, UniversityEvent): |
160 |
return create_roster_university_event(event) |
161 |
elif isinstance(event, StudyEvent): |
162 |
return create_roster_study_event(event) |
163 |
elif isinstance(event, Event): |
164 |
return create_roster_event(event) |
165 |
else: |
166 |
raise TypeError("Given object is not of any Event type") |
167 |
|
168 |
def make_first_quarter_column(hour, quarter): |
169 |
"""Creates and returns the first part of the quarter row, which is the |
170 |
column with the current time in it.""" |
171 |
quarter_line = "<tr><td style='font-size:" |
172 |
if quarter != 0: |
173 |
quarter_line += "xx-small; color: grey;" |
174 |
else: |
175 |
quarter_line += "x-small;" |
176 |
quarter_line += "'>" |
177 |
if hour < 10: |
178 |
quarter_line += "0" |
179 |
quarter_line += str(hour) +":" |
180 |
if quarter == 0: |
181 |
quarter_line += "0" |
182 |
quarter_line += str(quarter) |
183 |
quarter_line += "</td>" |
184 |
return quarter_line |
185 |
|
186 |
|
187 |
def make_quarter_row(events, hour, quarter): |
188 |
"""Creates an HTML row for a quarter. Expects *NO conflicts!*""" |
189 |
if len(conflicting_events(events)) != 0: |
190 |
print(conflicting_events(events)) |
191 |
raise ValueError("events contains conflicts!") |
192 |
|
193 |
quarter_line = make_first_quarter_column(hour, quarter) |
194 |
|
195 |
first_day, last_day = first_last_day(events) |
196 |
for i in range((first_day - last_day).days, 1): |
197 |
column_added = False |
198 |
current_day = (last_day + datetime.timedelta(days=i)) |
199 |
current_daytime = datetime.datetime( |
200 |
current_day.year, |
201 |
current_day.month, |
202 |
current_day.day, |
203 |
hour=hour, |
204 |
minute=quarter, |
205 |
tzinfo = datetime.timezone.utc) |
206 |
for event in events: |
207 |
if (same_day(event.begin_time, current_day) and |
208 |
same_daytime(event.begin_time, hour, quarter)): # Event starts on this quarter |
209 |
quarter_line += create_roster_for_event( |
210 |
event, |
211 |
conflict = (event.note.startswith("Conflict "))) |
212 |
column_added = True |
213 |
elif (same_day(event.begin_time, current_day) and |
214 |
event.begin_time < current_daytime and |
215 |
event.end_time >= current_daytime): |
216 |
column_added = True |
217 |
break |
218 |
if not column_added: |
219 |
quarter_line += "<td></td>" |
220 |
quarter_line += "</tr>" |
221 |
return quarter_line |
222 |
|
223 |
|
224 |
|
225 |
|
226 |
def create_roster_rows(events): |
227 |
if len(events) == 0: |
228 |
return {}, [] |
229 |
events = list(events.order_by("begin_time")) |
230 |
conflict_dict = replace_conflicts(events) |
231 |
print(conflict_dict) |
232 |
for event in events: |
233 |
print(event.begin_time) |
234 |
table_code = [] |
235 |
current_quarter = datetime.datetime.now().replace(hour=8, minute=0) |
236 |
while current_quarter.hour != 20 or current_quarter.minute != 00: |
237 |
table_code.append(make_quarter_row(events, current_quarter.hour, current_quarter.minute)) |
238 |
current_quarter += datetime.timedelta(minutes=15) |
239 |
return conflict_dict, table_code |
240 |