uw-course 1.0.2__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uw_course/ClassSchedule/SearchInfo.py +2 -3
- uw_course/ClassSchedule/runner.py +28 -17
- uw_course/DB/dbClass.py +20 -0
- uw_course/Utiles/manageDBClass.py +5 -0
- uw_course/Utiles/randomColor.py +22 -10
- uw_course/main.py +8 -77
- uw_course/pdfschedule.py +385 -0
- uw_course/setting.py +7 -2
- uw_course/ui/__init__.py +1 -0
- uw_course/ui/app.py +730 -0
- uw_course/ui/components.py +99 -0
- uw_course/ui/constants.py +9 -0
- uw_course/ui/schedule_view.py +87 -0
- uw_course-2.0.0.dist-info/METADATA +111 -0
- uw_course-2.0.0.dist-info/RECORD +23 -0
- {uw_course-1.0.2.dist-info → uw_course-2.0.0.dist-info}/WHEEL +1 -1
- uw_course/Utiles/colorMessage.py +0 -9
- uw_course-1.0.2.dist-info/METADATA +0 -95
- uw_course-1.0.2.dist-info/RECORD +0 -18
- {uw_course-1.0.2.dist-info → uw_course-2.0.0.dist-info}/entry_points.txt +0 -0
- {uw_course-1.0.2.dist-info → uw_course-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {uw_course-1.0.2.dist-info → uw_course-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import re
|
|
2
|
-
from ..Utiles.colorMessage import *
|
|
3
2
|
|
|
4
3
|
|
|
5
4
|
class Course():
|
|
@@ -20,7 +19,7 @@ class Course():
|
|
|
20
19
|
self.courseIndex = course['ClassIndex']
|
|
21
20
|
self.courseTitle = course['classTitle']
|
|
22
21
|
self.courseSeat = course['availableSeat']
|
|
23
|
-
if re.search("\d:\d*", self.time):
|
|
22
|
+
if re.search(r"\d:\d*", self.time):
|
|
24
23
|
self.startTime = self.time[0:5]
|
|
25
24
|
self.endTime = self.time[6:11]
|
|
26
25
|
StartTimeFlag = False
|
|
@@ -37,7 +36,7 @@ class Course():
|
|
|
37
36
|
self.weekDay = "H" if self.time[11:15] == "Th" else self.time[11:15]
|
|
38
37
|
self.writeSchedule()
|
|
39
38
|
else:
|
|
40
|
-
print(
|
|
39
|
+
print("Z_Z For course %s in class %s, Time Data Error Z_Z" % (self.courseIndex, self.classNum))
|
|
41
40
|
|
|
42
41
|
def writeSchedule(self):
|
|
43
42
|
self.fileOut.write(
|
|
@@ -1,38 +1,49 @@
|
|
|
1
|
-
from .SearchInfo import Course
|
|
2
|
-
from
|
|
3
|
-
from
|
|
4
|
-
from ..Utiles.colorMessage import *
|
|
1
|
+
from uw_course.ClassSchedule.SearchInfo import Course
|
|
2
|
+
from uw_course.Utiles.randomColor import randomColor, randomGray
|
|
3
|
+
from uw_course.setting import Setting
|
|
5
4
|
|
|
6
5
|
from os import remove
|
|
7
6
|
|
|
8
7
|
setting = Setting()
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
def
|
|
10
|
+
def get_course_detail(dbClassUW, courseIndex):
|
|
12
11
|
CourseDescribe = dbClassUW.CourseDescribe
|
|
12
|
+
if not courseIndex or " " not in courseIndex:
|
|
13
|
+
return None
|
|
13
14
|
faculty = courseIndex.split(" ")[0]
|
|
14
15
|
courseNum = courseIndex.split(" ")[1]
|
|
15
16
|
FacultyList = CourseDescribe.find({"faculty": faculty})
|
|
16
17
|
for course in FacultyList:
|
|
17
|
-
if course
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
if course.get("courseIndex") == courseNum:
|
|
19
|
+
return course
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def SearchCourse(dbClassUW, courseIndex):
|
|
24
|
+
course = get_course_detail(dbClassUW, courseIndex)
|
|
25
|
+
if not course:
|
|
26
|
+
print("@_@ Course %s Not Found @_@" % (courseIndex))
|
|
27
|
+
return
|
|
28
|
+
print("\n" + "$" * 50 + "\n\n")
|
|
29
|
+
print("Description: ", end="")
|
|
30
|
+
print(course.get("courseDescription"))
|
|
31
|
+
print("\ncourseCredit: ", end="")
|
|
32
|
+
print(course.get("courseCredit"))
|
|
33
|
+
print("\n\n" + "$" * 50)
|
|
25
34
|
print("\n\n")
|
|
26
35
|
|
|
27
36
|
|
|
28
|
-
def SearchAvalibleInTerm(dbClassUW, courseIndex, classNum=None):
|
|
37
|
+
def SearchAvalibleInTerm(dbClassUW, courseIndex, classNum=None, quiet=False):
|
|
29
38
|
ClassSchedule = dbClassUW.ClassSchedule
|
|
30
39
|
courseSelect = ClassSchedule.find({"ClassIndex": courseIndex})
|
|
31
40
|
if courseSelect == None:
|
|
32
|
-
|
|
41
|
+
if not quiet:
|
|
42
|
+
print("@_@ Course %s Not Found in %s @_@" % (courseIndex, dbClassUW.ClassCollectionName))
|
|
33
43
|
return None
|
|
34
44
|
else:
|
|
35
|
-
|
|
45
|
+
if not quiet:
|
|
46
|
+
print("!! Found Course %s in %s !!" % (courseIndex, dbClassUW.ClassCollectionName))
|
|
36
47
|
if classNum != None:
|
|
37
48
|
return [courseIndex, classNum]
|
|
38
49
|
return [courseIndex]
|
|
@@ -41,7 +52,7 @@ def SearchAvalibleInTerm(dbClassUW, courseIndex, classNum=None):
|
|
|
41
52
|
def makeSchedule(dbClassUW, courseWishList: list, gray: bool=False):
|
|
42
53
|
classSchedule = dbClassUW.ClassSchedule
|
|
43
54
|
if gray:
|
|
44
|
-
print(
|
|
55
|
+
print("!! Make Schedule with Gray Color !!")
|
|
45
56
|
try:
|
|
46
57
|
remove(setting.outDir)
|
|
47
58
|
except:
|
uw_course/DB/dbClass.py
CHANGED
|
@@ -23,3 +23,23 @@ class dbClass:
|
|
|
23
23
|
self.ClassCollectionName = collectionName
|
|
24
24
|
self.ClassDATABASE.selectCollection(self.ClassCollectionName)
|
|
25
25
|
self.ClassSchedule = self.ClassDATABASE.mongo_collection
|
|
26
|
+
|
|
27
|
+
def listClassCollections(self):
|
|
28
|
+
collections = self.ClassDATABASE.listCollections()
|
|
29
|
+
term_order = {"Winter": 0, "Spring": 1, "Fall": 2}
|
|
30
|
+
|
|
31
|
+
def sort_key(name):
|
|
32
|
+
if not name.startswith("Class"):
|
|
33
|
+
return (9999, 99, name)
|
|
34
|
+
tail = name[len("Class") :]
|
|
35
|
+
year = tail[:4]
|
|
36
|
+
term = tail[4:]
|
|
37
|
+
try:
|
|
38
|
+
year_value = int(year)
|
|
39
|
+
except ValueError:
|
|
40
|
+
year_value = 9999
|
|
41
|
+
term_value = term_order.get(term, 99)
|
|
42
|
+
return (year_value, term_value, name)
|
|
43
|
+
|
|
44
|
+
class_names = [name for name in collections if name.startswith("Class")]
|
|
45
|
+
return sorted(class_names, key=sort_key, reverse=True)
|
|
@@ -16,5 +16,10 @@ class connectDB:
|
|
|
16
16
|
def selectCollection(self, collection_name):
|
|
17
17
|
self.mongo_collection = self.mongo_db[collection_name]
|
|
18
18
|
|
|
19
|
+
def listCollections(self):
|
|
20
|
+
if self.mongo_db is None:
|
|
21
|
+
return []
|
|
22
|
+
return self.mongo_db.list_collection_names()
|
|
23
|
+
|
|
19
24
|
def closeDB(self):
|
|
20
25
|
self.client.close()
|
uw_course/Utiles/randomColor.py
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
from random import
|
|
1
|
+
from random import choice
|
|
2
|
+
|
|
3
|
+
_PALETTE = [
|
|
4
|
+
"#1F77B4", # blue
|
|
5
|
+
"#FF7F0E", # orange
|
|
6
|
+
"#2CA02C", # green
|
|
7
|
+
"#D62728", # red
|
|
8
|
+
"#9467BD", # purple
|
|
9
|
+
"#8C564B", # brown
|
|
10
|
+
"#E377C2", # pink
|
|
11
|
+
"#7F7F7F", # gray
|
|
12
|
+
"#BCBD22", # olive
|
|
13
|
+
"#17BECF", # cyan
|
|
14
|
+
"#4E79A7", # blue
|
|
15
|
+
"#F28E2B", # orange
|
|
16
|
+
"#59A14F", # green
|
|
17
|
+
"#E15759", # red
|
|
18
|
+
"#76B7B2", # teal
|
|
19
|
+
"#EDC948", # yellow
|
|
20
|
+
]
|
|
2
21
|
|
|
3
22
|
|
|
4
23
|
def randomColor():
|
|
5
|
-
|
|
6
|
-
color = ""
|
|
7
|
-
for i in range(6):
|
|
8
|
-
color += colorArr[randint(0, 14)]
|
|
9
|
-
return "#" + color
|
|
24
|
+
return choice(_PALETTE)
|
|
10
25
|
|
|
11
26
|
|
|
12
27
|
def randomGray():
|
|
13
|
-
|
|
14
|
-
color = colorArr[randint(2, 12)]
|
|
15
|
-
color += colorArr[randint(0, 14)]
|
|
16
|
-
return "#" + color + color + color
|
|
28
|
+
return "#9E9E9E"
|
uw_course/main.py
CHANGED
|
@@ -1,85 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
from .ClassSchedule.runner import SearchCourse, makeSchedule, SearchAvalibleInTerm
|
|
3
|
-
from .Utiles.colorMessage import *
|
|
4
|
-
from .setting import Setting
|
|
5
|
-
|
|
6
|
-
from os import system
|
|
7
|
-
import argparse
|
|
1
|
+
import os
|
|
8
2
|
import sys
|
|
9
3
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
url = "https://github.com/zangjiucheng/CourseExplorer/blob/Release/schema.txt"
|
|
15
|
-
|
|
16
|
-
def addCourse(course):
|
|
17
|
-
if course != None:
|
|
18
|
-
courseWishList.append(course)
|
|
19
|
-
|
|
4
|
+
if __package__ in (None, ""):
|
|
5
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
6
|
+
__package__ = "uw_course"
|
|
20
7
|
|
|
21
|
-
|
|
22
|
-
SearchCourse(dbClassUW, course)
|
|
23
|
-
exit(0)
|
|
24
|
-
|
|
25
|
-
def parse_arguments():
|
|
26
|
-
"""Parse command-line arguments and validate them."""
|
|
27
|
-
parser = argparse.ArgumentParser(
|
|
28
|
-
description="Course Helper: A tool to manage course details and collections.",
|
|
29
|
-
epilog=f"""Hint: Use --course to specify a course
|
|
30
|
-
or --file to specify a file with courses using schema: {url}
|
|
31
|
-
or --export to export the schedule to pdf (.out only).""",
|
|
32
|
-
)
|
|
33
|
-
parser.add_argument(
|
|
34
|
-
"-c", "--course", type=str, help="Specify the course to check details for."
|
|
35
|
-
)
|
|
36
|
-
parser.add_argument(
|
|
37
|
-
"-f", "--file", type=str, help="Specify a file with collection of courses."
|
|
38
|
-
)
|
|
39
|
-
parser.add_argument(
|
|
40
|
-
"-e", "--export", type=str, help="Export the schedule to pdf (.out only)."
|
|
41
|
-
)
|
|
42
|
-
parser.add_argument(
|
|
43
|
-
"-g", "--gray", action="store_true", help="Enable gray color mode for output."
|
|
44
|
-
)
|
|
8
|
+
from uw_course.ui.app import run_app
|
|
45
9
|
|
|
46
|
-
args = parser.parse_args()
|
|
47
|
-
|
|
48
|
-
# Ensure at least one argument is provided
|
|
49
|
-
if len(sys.argv) == 1:
|
|
50
|
-
parser.print_help()
|
|
51
|
-
sys.exit(1)
|
|
52
|
-
|
|
53
|
-
return args
|
|
54
10
|
|
|
55
11
|
def main():
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if args.course:
|
|
59
|
-
checkDetail(args.course)
|
|
60
|
-
elif args.export:
|
|
61
|
-
system("pdfschedule " + args.export)
|
|
62
|
-
print(OKGREEN + "\n\n ---------------- Done!!! ---------------- \n\n" + ENDC)
|
|
63
|
-
elif args.file:
|
|
64
|
-
with open(args.file, "r") as f:
|
|
65
|
-
collection = f.readline().strip().split("#")[0].strip()
|
|
66
|
-
dbClassUW.switchCollection(collection)
|
|
67
|
-
next(f) # Skip the first line
|
|
68
|
-
for line in f:
|
|
69
|
-
try:
|
|
70
|
-
if line.startswith("#"):
|
|
71
|
-
continue
|
|
72
|
-
info = line.strip().split(",")
|
|
73
|
-
course = info[0].strip()
|
|
74
|
-
if len(info) > 1:
|
|
75
|
-
addCourse(SearchAvalibleInTerm(dbClassUW, course, int(info[1].strip())))
|
|
76
|
-
else:
|
|
77
|
-
addCourse(SearchAvalibleInTerm(dbClassUW, course))
|
|
78
|
-
except Exception as e:
|
|
79
|
-
print(FAIL + f"Error: {e}" + ENDC)
|
|
80
|
-
makeSchedule(dbClassUW, courseWishList=courseWishList, gray=gray)
|
|
81
|
-
system("pdfschedule " + setting.outDir)
|
|
82
|
-
print(OKGREEN + "\n\n ---------------- Done!!! ---------------- \n\n" + ENDC)
|
|
12
|
+
run_app()
|
|
13
|
+
|
|
83
14
|
|
|
84
15
|
if __name__ == "__main__":
|
|
85
|
-
main()
|
|
16
|
+
main()
|
uw_course/pdfschedule.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
Weekly schedule typesetter
|
|
4
|
+
|
|
5
|
+
Run ``pdfschedule --help`` or visit <https://github.com/jwodder/schedule> for
|
|
6
|
+
more information.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.4.1.post1"
|
|
10
|
+
__author__ = "John Thorvald Wodder II; modified by Jiucheng Zang"
|
|
11
|
+
__author_email__ = "pdfschedule@varonathe.org, git.jiucheng@gmail.com"
|
|
12
|
+
__license__ = "MIT"
|
|
13
|
+
__url__ = "https://github.com/jwodder/schedule"
|
|
14
|
+
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from datetime import time
|
|
17
|
+
from math import ceil, floor
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
import re
|
|
20
|
+
from textwrap import wrap
|
|
21
|
+
import attr
|
|
22
|
+
from reportlab.lib import pagesizes
|
|
23
|
+
from reportlab.lib.units import inch
|
|
24
|
+
from reportlab.pdfbase import pdfmetrics
|
|
25
|
+
from reportlab.pdfbase.ttfonts import TTFont
|
|
26
|
+
from reportlab.pdfgen.canvas import Canvas
|
|
27
|
+
import yaml
|
|
28
|
+
|
|
29
|
+
EM = 0.6 ### TODO: Eliminate
|
|
30
|
+
|
|
31
|
+
WEEKDAYS_EN = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
|
|
32
|
+
FULL_WEEK_EN = ["Sunday"] + WEEKDAYS_EN + ["Saturday"]
|
|
33
|
+
FULL_WEEK_MON_EN = WEEKDAYS_EN + ["Saturday", "Sunday"]
|
|
34
|
+
|
|
35
|
+
DAY_REGEXES = [
|
|
36
|
+
("Sunday", "Sun?"),
|
|
37
|
+
("Monday", "M(on?)?"),
|
|
38
|
+
("Tuesday", "T(ue?)?"),
|
|
39
|
+
("Wednesday", "W(ed?)?"),
|
|
40
|
+
("Thursday", "Thu?|H|R"),
|
|
41
|
+
("Friday", "F(ri?)?"),
|
|
42
|
+
("Saturday", "Sat?"),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
GREY = (0.8, 0.8, 0.8)
|
|
46
|
+
|
|
47
|
+
COLORS = [
|
|
48
|
+
GREY,
|
|
49
|
+
(1, 0, 0), # red
|
|
50
|
+
(0, 1, 0), # blue
|
|
51
|
+
(0, 0, 1), # green
|
|
52
|
+
(0, 1, 1), # cyan
|
|
53
|
+
(1, 1, 0), # yellow
|
|
54
|
+
(0.5, 0, 0.5), # purple
|
|
55
|
+
(1, 1, 1), # white
|
|
56
|
+
(1, 0.5, 0), # orange
|
|
57
|
+
(1, 0, 1), # magenta
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Schedule:
|
|
62
|
+
def __init__(self, days, day_names=None):
|
|
63
|
+
self.events = []
|
|
64
|
+
self.days = list(days)
|
|
65
|
+
if day_names is None:
|
|
66
|
+
self._day_names = lambda d: d
|
|
67
|
+
elif isinstance(day_names, Mapping):
|
|
68
|
+
self._day_names = day_names.__getitem__
|
|
69
|
+
elif not callable(day_names):
|
|
70
|
+
raise TypeError("day_names must be a callable or dict")
|
|
71
|
+
else:
|
|
72
|
+
self._day_names = day_names
|
|
73
|
+
|
|
74
|
+
def add_event(self, event):
|
|
75
|
+
self.events.append(event)
|
|
76
|
+
|
|
77
|
+
def day_names(self):
|
|
78
|
+
return map(self._day_names, self.days)
|
|
79
|
+
|
|
80
|
+
def all_events(self):
|
|
81
|
+
return self.events
|
|
82
|
+
|
|
83
|
+
def events_on_day(self, day):
|
|
84
|
+
return [e for e in self.events if day in e.days]
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def number_of_days(self):
|
|
88
|
+
return len(self.days)
|
|
89
|
+
|
|
90
|
+
# The font and pagesize of the canvas must already have been set.
|
|
91
|
+
# x,y: upper-left corner of schedule to render (counting times along the
|
|
92
|
+
# edge; `render` should not draw anything outside the given box)
|
|
93
|
+
def render(
|
|
94
|
+
self,
|
|
95
|
+
canvas,
|
|
96
|
+
width,
|
|
97
|
+
height,
|
|
98
|
+
x,
|
|
99
|
+
y,
|
|
100
|
+
font_size,
|
|
101
|
+
show_times=True,
|
|
102
|
+
min_time=None,
|
|
103
|
+
max_time=None,
|
|
104
|
+
):
|
|
105
|
+
if min_time is None:
|
|
106
|
+
min_time = max(
|
|
107
|
+
min(time2hours(ev.start_time) for ev in self.all_events()) - 0.5, 0
|
|
108
|
+
)
|
|
109
|
+
if max_time is None:
|
|
110
|
+
max_time = min(
|
|
111
|
+
max(time2hours(ev.end_time) for ev in self.all_events()) + 0.5, 24
|
|
112
|
+
)
|
|
113
|
+
# List of hours to label and draw a line across
|
|
114
|
+
hours = range(floor(min_time) + 1, ceil(max_time))
|
|
115
|
+
line_height = font_size * 1.2
|
|
116
|
+
# Font size of the day headers at the top of each column:
|
|
117
|
+
header_size = font_size * 1.2
|
|
118
|
+
# Height of the boxes in which the day headers will be drawn:
|
|
119
|
+
day_height = header_size * 1.2
|
|
120
|
+
# Font size of the time labels at the left of each hour:
|
|
121
|
+
time_size = font_size / 1.2
|
|
122
|
+
# Boundaries of where this method is allowed to draw stuff:
|
|
123
|
+
area = Box(x, y, width, height)
|
|
124
|
+
|
|
125
|
+
canvas.setFontSize(time_size)
|
|
126
|
+
# Gap between the right edge of the time labels and the left edge of
|
|
127
|
+
# the schedule box. I don't remember how I came up with this formula.
|
|
128
|
+
time_gap = 0.2 * canvas.stringWidth(":00")
|
|
129
|
+
if show_times:
|
|
130
|
+
time_width = time_gap + max(canvas.stringWidth(f"{i}:00") for i in hours)
|
|
131
|
+
else:
|
|
132
|
+
time_width = 0
|
|
133
|
+
|
|
134
|
+
sched = Box(
|
|
135
|
+
x + time_width,
|
|
136
|
+
y - day_height,
|
|
137
|
+
width - time_width,
|
|
138
|
+
height - day_height,
|
|
139
|
+
)
|
|
140
|
+
hour_height = sched.height / (max_time - min_time)
|
|
141
|
+
day_width = sched.width / self.number_of_days
|
|
142
|
+
line_width = floor(day_width / (font_size * EM))
|
|
143
|
+
|
|
144
|
+
# Border around schedule and day headers:
|
|
145
|
+
canvas.rect(sched.ulx, sched.lry, sched.width, area.height)
|
|
146
|
+
|
|
147
|
+
# Day headers text:
|
|
148
|
+
canvas.setFontSize(header_size)
|
|
149
|
+
for i, day in enumerate(self.day_names()):
|
|
150
|
+
canvas.drawCentredString(
|
|
151
|
+
sched.ulx + day_width * (i + 0.5),
|
|
152
|
+
area.uly - line_height,
|
|
153
|
+
day,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Underline beneath day headers:
|
|
157
|
+
canvas.line(sched.ulx, sched.uly, sched.lrx, sched.uly)
|
|
158
|
+
|
|
159
|
+
# Lines across each hour:
|
|
160
|
+
canvas.setDash([2], 0)
|
|
161
|
+
for i in hours:
|
|
162
|
+
y = sched.uly - (i - min_time) * hour_height
|
|
163
|
+
canvas.line(sched.ulx, y, sched.lrx, y)
|
|
164
|
+
|
|
165
|
+
# Lines between each day:
|
|
166
|
+
canvas.setDash([], 0)
|
|
167
|
+
for i in range(1, self.number_of_days):
|
|
168
|
+
x = sched.ulx + i * day_width
|
|
169
|
+
canvas.line(x, area.uly, x, area.lry)
|
|
170
|
+
|
|
171
|
+
if show_times:
|
|
172
|
+
canvas.setFontSize(time_size)
|
|
173
|
+
for i in hours:
|
|
174
|
+
canvas.drawRightString(
|
|
175
|
+
sched.ulx - time_gap,
|
|
176
|
+
sched.uly - (i - min_time) * hour_height - time_size / 2,
|
|
177
|
+
f"{i}:00",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Events:
|
|
181
|
+
canvas.setFontSize(font_size)
|
|
182
|
+
for i, day in enumerate(self.days):
|
|
183
|
+
dx = sched.ulx + day_width * i
|
|
184
|
+
for ev in self.events_on_day(day):
|
|
185
|
+
ebox = Box(
|
|
186
|
+
dx,
|
|
187
|
+
sched.uly - (time2hours(ev.start_time) - min_time) * hour_height,
|
|
188
|
+
day_width,
|
|
189
|
+
ev.length * hour_height,
|
|
190
|
+
)
|
|
191
|
+
# Event box:
|
|
192
|
+
canvas.setStrokeColorRGB(0, 0, 0)
|
|
193
|
+
canvas.setFillColorRGB(*ev.color)
|
|
194
|
+
canvas.rect(*ebox.rect(), stroke=1, fill=1)
|
|
195
|
+
canvas.setFillColorRGB(0, 0, 0)
|
|
196
|
+
|
|
197
|
+
if ev.color[1] <= 0.33333:
|
|
198
|
+
# Background color is too dark; print text in white
|
|
199
|
+
canvas.setFillColorRGB(1, 1, 1)
|
|
200
|
+
|
|
201
|
+
# Event text:
|
|
202
|
+
### TODO: Use PLATYPUS or whatever for this part:
|
|
203
|
+
text = sum((wrap(t, line_width) for t in ev.text), [])
|
|
204
|
+
tmp_size = None
|
|
205
|
+
if len(text) * line_height > ebox.height:
|
|
206
|
+
tmp_size = ebox.height / len(text) / 1.2
|
|
207
|
+
canvas.setFontSize(tmp_size)
|
|
208
|
+
line_height = tmp_size * 1.2
|
|
209
|
+
y = (
|
|
210
|
+
ebox.lry
|
|
211
|
+
+ ebox.height / 2
|
|
212
|
+
+ len(text) * line_height / 2
|
|
213
|
+
+ (tmp_size or font_size) / 3
|
|
214
|
+
)
|
|
215
|
+
for t in text:
|
|
216
|
+
y -= line_height
|
|
217
|
+
canvas.drawCentredString(ebox.ulx + day_width / 2, y, t)
|
|
218
|
+
if tmp_size is not None:
|
|
219
|
+
canvas.setFontSize(font_size)
|
|
220
|
+
line_height = font_size * 1.2
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@attr.define
|
|
224
|
+
class Event:
|
|
225
|
+
start_time: time = attr.field(validator=attr.validators.instance_of(time))
|
|
226
|
+
end_time: time = attr.field(validator=attr.validators.instance_of(time))
|
|
227
|
+
text: list[str] = attr.field()
|
|
228
|
+
color: tuple[float, float, float] = attr.field()
|
|
229
|
+
days: list[str] = attr.field() # List of days
|
|
230
|
+
|
|
231
|
+
def __attrs_post_init__(self):
|
|
232
|
+
if self.start_time >= self.end_time:
|
|
233
|
+
raise ValueError("Event must start before it ends")
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def length(self):
|
|
237
|
+
"""The length of the event in hours"""
|
|
238
|
+
return timediff(self.start_time, self.end_time)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@attr.define
|
|
242
|
+
class Box:
|
|
243
|
+
ulx: float = attr.field()
|
|
244
|
+
uly: float = attr.field()
|
|
245
|
+
width: float = attr.field()
|
|
246
|
+
height: float = attr.field()
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def lrx(self):
|
|
250
|
+
return self.ulx + self.width
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def lry(self):
|
|
254
|
+
return self.uly - self.height
|
|
255
|
+
|
|
256
|
+
def rect(self):
|
|
257
|
+
return (self.ulx, self.lry, self.width, self.height)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def parse_time(s):
|
|
261
|
+
m = re.fullmatch(r"([0-9]{1,2})(?:[:.]?([0-9]{2}))?", s.strip())
|
|
262
|
+
if m:
|
|
263
|
+
return time(int(m[1]), int(m[2] or 0))
|
|
264
|
+
else:
|
|
265
|
+
raise ValueError(s)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def generate_pdf(
|
|
269
|
+
infile_path,
|
|
270
|
+
outfile_path=None,
|
|
271
|
+
color=False,
|
|
272
|
+
font="Helvetica",
|
|
273
|
+
font_size=10,
|
|
274
|
+
portrait=False,
|
|
275
|
+
scale=None,
|
|
276
|
+
no_times=False,
|
|
277
|
+
no_weekends=False,
|
|
278
|
+
start_monday=False,
|
|
279
|
+
start_time=None,
|
|
280
|
+
end_time=None,
|
|
281
|
+
):
|
|
282
|
+
if font in available_fonts():
|
|
283
|
+
font_name = font
|
|
284
|
+
else:
|
|
285
|
+
font_name = "CustomFont"
|
|
286
|
+
pdfmetrics.registerFont(TTFont(font_name, font))
|
|
287
|
+
if portrait:
|
|
288
|
+
page_width, page_height = pagesizes.portrait(pagesizes.letter)
|
|
289
|
+
else:
|
|
290
|
+
page_width, page_height = pagesizes.landscape(pagesizes.letter)
|
|
291
|
+
colors = COLORS if color else [GREY]
|
|
292
|
+
if no_weekends:
|
|
293
|
+
week = WEEKDAYS_EN
|
|
294
|
+
elif start_monday:
|
|
295
|
+
week = FULL_WEEK_MON_EN
|
|
296
|
+
else:
|
|
297
|
+
week = FULL_WEEK_EN
|
|
298
|
+
sched = Schedule(week)
|
|
299
|
+
with open(infile_path, "r", encoding="utf-8") as infile:
|
|
300
|
+
for ev in read_events(infile, colors=colors):
|
|
301
|
+
sched.add_event(ev)
|
|
302
|
+
if outfile_path is None:
|
|
303
|
+
outfile_path = str(Path(infile_path).with_suffix(".pdf"))
|
|
304
|
+
with open(outfile_path, "wb") as outfile:
|
|
305
|
+
c = Canvas(outfile, (page_width, page_height))
|
|
306
|
+
c.setFont(font_name, font_size)
|
|
307
|
+
if scale is not None:
|
|
308
|
+
factor = 1 / scale
|
|
309
|
+
c.translate(
|
|
310
|
+
(1 - factor) * page_width / 2,
|
|
311
|
+
(1 - factor) * page_height / 2,
|
|
312
|
+
)
|
|
313
|
+
c.scale(factor, factor)
|
|
314
|
+
sched.render(
|
|
315
|
+
c,
|
|
316
|
+
x=inch,
|
|
317
|
+
y=page_height - inch,
|
|
318
|
+
width=page_width - 2 * inch,
|
|
319
|
+
height=page_height - 2 * inch,
|
|
320
|
+
font_size=font_size,
|
|
321
|
+
show_times=not no_times,
|
|
322
|
+
min_time=time2hours(start_time) if start_time is not None else None,
|
|
323
|
+
max_time=time2hours(end_time) if end_time is not None else None,
|
|
324
|
+
)
|
|
325
|
+
c.showPage()
|
|
326
|
+
c.save()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def read_events(infile, colors):
|
|
330
|
+
indata = yaml.safe_load(infile)
|
|
331
|
+
if not isinstance(indata, list):
|
|
332
|
+
raise ValueError("Input must be a YAML list")
|
|
333
|
+
for i, entry in enumerate(indata):
|
|
334
|
+
text = entry.get("name", "").splitlines()
|
|
335
|
+
try:
|
|
336
|
+
days = entry["days"]
|
|
337
|
+
timestr = entry["time"]
|
|
338
|
+
except KeyError as e:
|
|
339
|
+
raise ValueError(f"{str(e)!r} field missing from event #{i + 1}")
|
|
340
|
+
start_str, _, end_str = timestr.partition("-")
|
|
341
|
+
try:
|
|
342
|
+
start = parse_time(start_str)
|
|
343
|
+
end = parse_time(end_str)
|
|
344
|
+
except ValueError:
|
|
345
|
+
raise ValueError(f"Invalid time: {timestr!r}")
|
|
346
|
+
if "color" in entry:
|
|
347
|
+
m = re.fullmatch(
|
|
348
|
+
r"\s*#?\s*([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})\s*",
|
|
349
|
+
entry["color"],
|
|
350
|
+
)
|
|
351
|
+
if not m:
|
|
352
|
+
raise ValueError("Invalid color: " + repr(entry["color"]))
|
|
353
|
+
color = (
|
|
354
|
+
int(m.group(1), 16) / 255,
|
|
355
|
+
int(m.group(2), 16) / 255,
|
|
356
|
+
int(m.group(3), 16) / 255,
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
359
|
+
color = colors[i % len(colors)]
|
|
360
|
+
yield Event(
|
|
361
|
+
start_time=start,
|
|
362
|
+
end_time=end,
|
|
363
|
+
text=text,
|
|
364
|
+
color=color,
|
|
365
|
+
days=[d for d, rgx in DAY_REGEXES if re.search(rgx, days)],
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def time2hours(t):
|
|
370
|
+
return t.hour + (t.minute + (t.second + t.microsecond / 1000000) / 60) / 60
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def timediff(t1, t2):
|
|
374
|
+
# Returns the difference between two `datetime.time` objects as a number of
|
|
375
|
+
# hours
|
|
376
|
+
return time2hours(t2) - time2hours(t1)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def available_fonts():
|
|
380
|
+
return Canvas("").getAvailableFonts()
|
|
381
|
+
# return pdfmetrics.standardFonts
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
if __name__ == "__main__":
|
|
385
|
+
raise SystemExit("This module is intended to be imported and used as a library.")
|
uw_course/setting.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
from os import makedirs
|
|
2
|
+
from os.path import join
|
|
3
|
+
|
|
4
|
+
|
|
1
5
|
class Setting:
|
|
2
6
|
def __init__(self):
|
|
3
|
-
self.dataName = "./"
|
|
7
|
+
self.dataName = "./uw-course-files"
|
|
4
8
|
self.configFileName = "schedule.out"
|
|
5
|
-
self.
|
|
9
|
+
makedirs(self.dataName, exist_ok=True)
|
|
10
|
+
self.outDir = join(self.dataName, self.configFileName)
|
uw_course/ui/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""UI package for uw_course."""
|