11from csbot .plugin import Plugin
2- from datetime import datetime , timedelta
2+ import datetime
33import math
4+ import typing as _t
45
56from ..util import ordinal
67
78
9+ class Term :
10+ def __init__ (self , key : str , start_date : datetime .datetime ):
11+ self .key = key
12+ self .start_date = start_date
13+
14+ @property
15+ def first_monday (self ) -> datetime .datetime :
16+ return self .start_date - datetime .timedelta (days = self .start_date .weekday ())
17+
18+ @property
19+ def last_friday (self ) -> datetime .datetime :
20+ return self .first_monday + datetime .timedelta (days = 4 , weeks = 9 )
21+
22+ def get_week_number (self , date : datetime .date ) -> int :
23+ """Get the "term week number" of a date relative to this term.
24+
25+ The first week of term is week 1, not week 0. Week 1 starts at the
26+ Monday of the term's start date, even if the term's start date is not
27+ Monday. Any date before the start of the term gives a negative week
28+ number.
29+ """
30+ delta = date - self .first_monday .date ()
31+ week_number = math .floor (delta .days / 7.0 )
32+ if week_number >= 0 :
33+ return week_number + 1
34+ else :
35+ return week_number
36+
37+ def get_week_start (self , week_number : int ) -> datetime .datetime :
38+ """Get the start date of a specific week number relative to this term.
39+
40+ The first week of term is week 1, not week 0, although this method
41+ allows both. When referring to the first week of term, the start date is
42+ the term start date (which may not be a Monday). All other weeks start
43+ on their Monday.
44+ """
45+ if week_number in (0 , 1 ):
46+ return self .start_date
47+ elif week_number > 1 :
48+ return self .first_monday + datetime .timedelta (weeks = week_number - 1 )
49+ else :
50+ return self .first_monday + datetime .timedelta (weeks = week_number )
51+
52+
853class TermDates (Plugin ):
954 """
1055 A wonderful plugin allowing old people (graduates) to keep track of the
1156 ever-changing calendar.
1257 """
1358 DATE_FORMAT = '%Y-%m-%d'
59+ TERM_KEYS = ('aut' , 'spr' , 'sum' )
1460
1561 db_terms = Plugin .use ('mongodb' , collection = 'terms' )
16- db_weeks = Plugin .use ('mongodb' , collection = 'weeks' )
62+
63+ terms = None
64+ _doc_id = None
1765
1866 def setup (self ):
1967 super (TermDates , self ).setup ()
68+ self ._load ()
69+
70+ def _load (self ):
71+ doc = self .db_terms .find_one ()
72+ if not doc :
73+ return False
74+ self .terms = {key : Term (key , doc [key ][0 ]) for key in self .TERM_KEYS }
75+ self ._doc_id = doc ['_id' ]
76+ return True
77+
78+ def _save (self ):
79+ if not self .terms :
80+ return False
81+ doc = {key : (self .terms [key ].start_date , self .terms [key ].last_friday ) for key in self .TERM_KEYS }
82+ if self ._doc_id :
83+ self .db_terms .replace_one ({'_id' : self ._doc_id }, doc , upsert = True )
84+ else :
85+ res = self .db_terms .insert_one (doc )
86+ self ._doc_id = res .inserted_id
87+ return True
2088
21- # If we have stuff in mongodb, we can just load it directly.
22- if self .db_terms .find_one ():
23- self .initialised = True
24- self .terms = self .db_terms .find_one ()
25- self .weeks = self .db_weeks .find_one ()
26- return
27-
28- # If no term dates have been set, the calendar is uninitialised and
29- # can't be asked about term things.
30- self .initialised = False
31-
32- # Each term is represented as a tuple of the date of the first Monday
33- # and the last Friday in it.
34- self .terms = {term : (None , None )
35- for term in ['aut' , 'spr' , 'sum' ]}
36-
37- # And each week is just the date of the Monday
38- self .weeks = {'{} {}' .format (term , week ): None
39- for term in ['aut' , 'spr' , 'sum' ]
40- for week in range (1 , 11 )}
89+ @property
90+ def initialised (self ) -> bool :
91+ """If no term dates have been set, the calendar is uninitialised and can't be asked about term thing."""
92+ return self ._doc_id is not None
4193
4294 @Plugin .command ('termdates' , help = 'termdates: show the current term dates' )
4395 def termdates (self , e ):
@@ -55,15 +107,15 @@ def _term_start(self, term):
55107 """
56108
57109 term = term .lower ()
58- return self .terms [term ][ 0 ] .strftime (self .DATE_FORMAT )
110+ return self .terms [term ]. start_date .strftime (self .DATE_FORMAT )
59111
60112 def _term_end (self , term ):
61113 """
62114 Get the end date (last Friday) of a term as a string.
63115 """
64116
65117 term = term .lower ()
66- return self .terms [term ][ 1 ] .strftime (self .DATE_FORMAT )
118+ return self .terms [term ]. last_friday .strftime (self .DATE_FORMAT )
67119
68120 @Plugin .command ('week' ,
69121 help = 'week [term] [num]: info about a week, '
@@ -80,81 +132,74 @@ def week(self, e):
80132 # !week term n - get the date of week n in the given term
81133 # !week n term - as above
82134
83- week = e ['data' ].split ()
135+ week = e ['data' ].lower (). split ()
84136 if len (week ) == 0 :
85- term , weeknum = self ._current_week ()
137+ term , week_number = self ._current_week ()
86138 elif len (week ) == 1 :
87139 try :
88- term = self ._current_term ()
89- weeknum = int (week [0 ])
90- if weeknum < 1 :
140+ term = self ._current_or_next_term ()
141+ week_number = int (week [0 ])
142+ if week_number < 1 :
91143 e .reply ('error: bad week format' )
92144 return
93145 except ValueError :
94- term = week [0 ][:3 ]
95- term , weeknum = self ._current_week (term )
146+ term_key = week [0 ][:3 ]
147+ term , week_number = self ._current_week (term_key )
96148 elif len (week ) >= 2 :
97149 try :
98- term = week [0 ][:3 ]
99- weeknum = int (week [1 ])
150+ term_key = week [0 ][:3 ]
151+ week_number = int (week [1 ])
100152 except ValueError :
101153 try :
102- term = week [1 ][:3 ]
103- weeknum = int (week [0 ])
154+ term_key = week [1 ][:3 ]
155+ week_number = int (week [0 ])
104156 except ValueError :
105157 e .reply ('error: bad week format' )
106158 return
159+ try :
160+ term = self .terms [term_key ]
161+ except KeyError :
162+ e .reply ('error: bad week format' )
163+ return
107164 else :
108165 e .reply ('error: bad week format' )
109166 return
110167
111- if weeknum > 0 :
112- e .reply ('{} {}: {}' .format (term .capitalize (),
113- weeknum ,
114- self ._week_start (term , weeknum )))
168+ if term is None :
169+ e .reply ('error: no term dates (see termdates.set)' )
170+ elif week_number > 0 :
171+ e .reply ('{} {}: {}' .format (term .key .capitalize (),
172+ week_number ,
173+ term .get_week_start (week_number ).strftime (self .DATE_FORMAT )))
115174 else :
116175 e .reply ('{} week before {} (starts {})'
117- .format (ordinal (- weeknum ),
118- term .capitalize (),
119- self . _week_start ( term , 1 )))
176+ .format (ordinal (- week_number ),
177+ term .key . capitalize (),
178+ term . start_date . strftime ( self . DATE_FORMAT )))
120179
121- def _current_term (self ):
180+ def _current_or_next_term (self ) -> _t . Optional [ Term ] :
122181 """
123182 Get the name of the current term
124183 """
125184
126- now = datetime .now ().date ()
127- for term in [ 'aut' , 'spr' , 'sum' ] :
128- dates = self .terms [term ]
129- if now >= dates [ 0 ]. date () and now <= dates [ 1 ] .date ():
185+ now = datetime .datetime . now ().date ()
186+ for key in self . TERM_KEYS :
187+ term = self .terms [key ]
188+ if now < term . first_monday .date ():
130189 return term
131- elif now <= dates [0 ].date ():
132- # We can do this because the terms are ordered
190+ elif now <= term .last_friday .date ():
133191 return term
192+ return None
134193
135- def _current_week (self , term = None ):
136- if term is None :
137- term = self ._current_term ()
138- start , _ = self .terms [term ]
139- now = datetime .now ()
140- delta = now .date () - start .date ()
141- weeknum = math .floor (delta .days / 7.0 )
142- if weeknum >= 0 :
143- weeknum += 1
144- return term , weeknum
145-
146- def _week_start (self , term , week ):
147- """
148- Get the start date of a week as a string.
149- """
150-
151- term = term .lower ()
152- start = self .weeks ['{} 1' .format (term )]
153- if week > 0 :
154- offset = timedelta (weeks = week - 1 )
194+ def _current_week (self , key : _t .Optional [str ] = None ) -> (_t .Optional [Term ], _t .Optional [int ]):
195+ if key :
196+ term = self .terms .get (key .lower ())
155197 else :
156- offset = timedelta (weeks = week )
157- return (start + offset ).strftime (self .DATE_FORMAT )
198+ term = self ._current_or_next_term ()
199+ if term :
200+ return term , term .get_week_number (datetime .date .today ())
201+ else :
202+ return None , None
158203
159204 @Plugin .command ('termdates.set' ,
160205 help = 'termdates.set <aut> <spr> <sum>: set the term dates' )
@@ -165,47 +210,15 @@ def termdates_set(self, e):
165210 e .reply ('error: all three dates must be provided' )
166211 return
167212
168- # Firstly compute the start and end dates of each term
169- for term , date in zip ([ 'aut' , 'spr' , 'sum' ] , dates ):
213+ terms = {}
214+ for key , date in zip (self . TERM_KEYS , dates ):
170215 try :
171- term_start = datetime .strptime (date , self .DATE_FORMAT )
216+ term_start = datetime .datetime . strptime (date , self .DATE_FORMAT )
172217 except ValueError :
173218 e .reply ('error: dates must be in %Y-%M-%d format.' )
174219 return
175220
176- # Not all terms start on a monday, so we need to compute the "real"
177- # term start used in all the other calculations.
178- # Fortunately Monday is used as the start of the week in Python's
179- # datetime stuff, which makes this really simple.
180- real_start = term_start - timedelta (days = term_start .weekday ())
181-
182- # Log for informational purposes
183- if not term_start == real_start :
184- self .log .info ('Computed real_start as {} (from {})' .format (
185- repr (real_start ), repr (term_start )))
186-
187- term_end = real_start + timedelta (days = 4 , weeks = 9 )
188- self .terms [term ] = (term_start , term_end )
189-
190- # Then the start of each week
191- self .weeks ['{} 1' .format (term )] = term_start
192- for week in range (2 , 11 ):
193- week_start = real_start + timedelta (weeks = week - 1 )
194- self .weeks ['{} {}' .format (term , week )] = week_start
195-
196- # Save to the database. As we don't touch the _id attribute in this
197- # method, this will cause `save` to override the previously-loaded
198- # entry (if there is one).
199- if '_id' in self .terms :
200- self .db_terms .replace_one ({'_id' : self .terms ['_id' ]}, self .terms , upsert = True )
201- else :
202- res = self .db_terms .insert_one (self .terms )
203- self .terms ['_id' ] = res .inserted_id
204- if '_id' in self .weeks :
205- self .db_weeks .replace_one ({'_id' : self .weeks ['_id' ]}, self .weeks , upsert = True )
206- else :
207- res = self .db_weeks .insert_one (self .weeks )
208- self .weeks ['_id' ] = res .inserted_id
221+ terms [key ] = Term (key , term_start )
209222
210- # Finally, we're initialised!
211- self .initialised = True
223+ self . terms = terms
224+ self ._save ()
0 commit comments