joeni

new_roster.py

1
""" This is a module dedicated to collecting all functionality regarding the
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