joeni

Improve roster by coloring conflicts

Author
Maarten Vangeneugden
Date
July 26, 2018, 9:53 p.m.
Hash
884292600fbfb4f1fc825646f85bf69fd48342e7
Parent
ed815ee93652b3db38a69833b17404be8b1eb389
Modified files
administration/new_roster.py
administration/templates/administration/roster_t.djhtml
static/css/base.scss

administration/new_roster.py

1 addition and 1 deletion.

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

administration/templates/administration/roster_t.djhtml

7 additions and 13 deletions.

View changes Hide changes
1
1
This is the roster template. You can use this to display a hourly roster,
2
2
spread over a series of days. To function properly, this template requires
3
3
the following variables:
4
4
- days :: A list of all days for which a column must be made.
5
5
- time_blocks :: A list of all rows with their respective timings, that must
6
6
                 be displayed in the roster. These elements must *not* contain
7
7
                 any unsafe elements!
8
8
- conflicts :: A dictionary with all conflicting events that need to be listed.
9
9
{% endcomment %}
10
10
11
11
<!--<style>
12
-
    table td {
13
-
        border-width: 0px 0px 1px 0px;
14
-
        border-bottom-style: solid;
15
-
        border-style: solid;
16
-
        border-color: red;
17
-
    }
18
-
</style>-->
19
-
<table>
20
12
    <th>
21
13
        {#<td></td> {# Empty row for hours #} {# Apparantly this isn't necessary with <th /> #}
22
14
        {% for day in days %}
23
15
            <td>{{ day|date:"l (d/m)" }}</td>
24
-
        {% endfor %}
+
16
        {% endfor %}
25
17
    </th>
26
18
    {% for element in time_blocks %}
27
19
        {{ element|safe }}
28
20
    {% endfor %}
29
21
</table>
30
22
{% for number, conflict_list in conflicts.items %}
31
23
    <h2 id="Conflict {{ number }}">Conflict {{ number }}</h2>
32
24
    {% for conflict in conflict_list %}
33
25
        {% if conflict.recently_created %}
34
26
            <div class="event-new">
35
-
        {% elif conflict.recently_updated %}
+
27
        {% elif conflict.recently_updated %}
36
28
            <div class="event-update">
37
-
        {% endif %}
+
29
        {% else %}
+
30
            <p class="conflict event" style="background-color: #{{ conflict.course.course.color }};">
+
31
        {% endif %}
38
32
        <a href="{% url "courses-course-index" conflict.course.course.slug_name %}">
39
33
            {{ conflict.course }}</a><br />
40
-
        {# FIXME Temporarily disabled until all users have an associated user_data #}
+
34
        {# FIXME Temporarily disabled until all users have an associated user_data #}
41
35
        {#<a href="{% url "administration-user" conflict.docent.user_data.slug_name %}">#}
42
36
        {#{{ conflict.docent }}</a><br />#}
43
37
        {{ conflict.docent }}<br />
44
38
        {{ conflict.begin_time|date:"H:i" }} - {{ conflict.end_time|date:"H:i" }}<br />
45
39
        <a href="{% url "administration-room-detail" conflict.room %}">
46
40
            {{ conflict.room }}</a> ({{ conflict.subject }})
47
41
        </div>
48
-
    {% endfor %}
+
42
    {% endfor %}
49
43
{% endfor %}
50
44

static/css/base.scss

45 additions and 4 deletions.

View changes Hide changes
1
1
    font-family: ubuntu;
2
2
    border-style: solid;
3
3
    text-transform: uppercase;
4
4
    border-width: 0.3em;
5
5
    margin: 1em;
6
6
    border-color: $uhasselt-color;
7
7
    padding: 0.2em;
8
8
    text-decoration: none;
9
9
    color: $uhasselt-color;
10
10
    font-weight: bold;
11
11
}
12
12
a.btn:hover {
13
13
    background-color: $uhasselt-color;
14
14
    color: white;
15
15
}
16
16
17
17
dl dt {
18
18
    margin: 5px;
19
19
}
20
20
21
21
.event {
22
22
    padding: 5px;
23
23
}
+
24
    a {
+
25
        text-decoration: none;
+
26
        color: inherit;
+
27
        &:hover {
+
28
            text-decoration: white underline dotted;
+
29
            font-style: italic;
+
30
        }
+
31
    }
+
32
}
24
33
25
34
td {
26
35
    padding-right: 1em;
27
36
}
28
37
29
38
.event-update {
30
39
    padding: 5px;
31
40
    background-color: yellow;
32
41
    color: red;
33
42
    border: medium dotted red;
34
43
}
+
44
        text-decoration: none;
+
45
        color: inherit;
+
46
        &:hover {
+
47
            text-decoration: red underline dotted;
+
48
            font-style: italic;
+
49
        }
+
50
    }
+
51
}
35
52
.event-new {
36
53
    padding: 5px;
37
54
    background-color: white;
38
55
    color: black;
39
56
    border: medium dashed black;
40
57
}
+
58
        text-decoration: none;
+
59
        color: inherit;
+
60
        &:hover {
+
61
            text-decoration: black underline dotted;
+
62
            font-style: italic;
+
63
        }
+
64
    }
+
65
}
41
66
.event-note {
42
67
    padding: 5px;
43
68
    color: purple;
44
69
    border: medium double purple;
45
70
}
+
71
        text-decoration: none;
+
72
        color: inherit;
+
73
        &:hover {
+
74
            text-decoration: white underline dotted;
+
75
            font-style: italic;
+
76
        }
+
77
    }
+
78
}
46
79
.event-conflict {
47
80
    padding: 5px;
48
81
    background-color: red;
49
82
    color: white;
50
83
    border: medium dashed;
51
84
    border-color: inherit;
52
85
}
53
-
.event a {
54
-
    text-decoration: none;
55
-
    color: inherit;
56
-
}
+
86
        text-decoration-color: blue;
+
87
        font-style: italic;
+
88
        }
+
89
}
57
90
58
91
p.conflict {
+
92
    width: 50%;
+
93
    @media screen
+
94
    and (max-device-width: 440px)
+
95
    and (max-device-height: 800px) {
+
96
        width: 100%;
+
97
    }
+
98
}
+
99