Auto suggest new class times
This commit is contained in:
		| @@ -11,7 +11,7 @@ from rest_auth.registration.serializers import RegisterSerializer | |||||||
| from rest_auth.serializers import PasswordChangeSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, LoginSerializer | from rest_auth.serializers import PasswordChangeSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, LoginSerializer | ||||||
| from rest_auth.serializers import UserDetailsSerializer | from rest_auth.serializers import UserDetailsSerializer | ||||||
| import re | import re | ||||||
| import datetime, time | import datetime, time, calendar | ||||||
|  |  | ||||||
| from . import models, fields, utils, utils_ldap, utils_auth, utils_stats | from . import models, fields, utils, utils_ldap, utils_auth, utils_stats | ||||||
| from .. import settings, secrets | from .. import settings, secrets | ||||||
| @@ -528,10 +528,80 @@ class CourseDetailSerializer(serializers.ModelSerializer): | |||||||
|     sessions = SessionListSerializer(many=True, read_only=True) |     sessions = SessionListSerializer(many=True, read_only=True) | ||||||
|     name = serializers.CharField(max_length=100) |     name = serializers.CharField(max_length=100) | ||||||
|     description = fields.HTMLField(max_length=6000) |     description = fields.HTMLField(max_length=6000) | ||||||
|  |     suggestion = serializers.SerializerMethodField() | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = models.Course |         model = models.Course | ||||||
|         fields = '__all__' |         fields = '__all__' | ||||||
|  |  | ||||||
|  |     def get_suggestion(self, obj): | ||||||
|  |         def iter_dates(): | ||||||
|  |             start_of_month = utils.today_alberta_tz().replace(day=1) | ||||||
|  |             for i in range(90): | ||||||
|  |                 yield start_of_month + datetime.timedelta(days=i) | ||||||
|  |  | ||||||
|  |         def iter_matching_dates(weekday, week_num=False): | ||||||
|  |             week_num_counts = [0] * 13 | ||||||
|  |             for date in iter_dates(): | ||||||
|  |                 if date.weekday() == weekday: | ||||||
|  |                     week_num_counts[date.month] += 1 | ||||||
|  |                     if week_num and week_num_counts[date.month] != week_num: | ||||||
|  |                         continue | ||||||
|  |                     yield date | ||||||
|  |  | ||||||
|  |         def next_date(weekday, week_num=False): | ||||||
|  |             for date in iter_matching_dates(weekday, week_num): | ||||||
|  |                 if date > utils.today_alberta_tz(): | ||||||
|  |                     return date | ||||||
|  |             raise | ||||||
|  |  | ||||||
|  |         def course_is_usually_monthly(course): | ||||||
|  |             two_months_ago = utils.today_alberta_tz() - datetime.timedelta(days=61) | ||||||
|  |             recent_sessions = obj.sessions.filter(datetime__gte=two_months_ago) | ||||||
|  |             if recent_sessions.count() < 3: | ||||||
|  |                 return True | ||||||
|  |             else: | ||||||
|  |                 return False | ||||||
|  |  | ||||||
|  |         prev_session = obj.sessions.last() | ||||||
|  |  | ||||||
|  |         if obj.id == 273: # monthly clean 10:00 AM 3rd Saturday of each month | ||||||
|  |             date = next_date(calendar.SATURDAY, week_num=3) | ||||||
|  |             time = datetime.time(10, 0) | ||||||
|  |             dt = datetime.datetime.combine(date, time) | ||||||
|  |             dt = utils.TIMEZONE_CALGARY.localize(dt) | ||||||
|  |             cost = 0 | ||||||
|  |             max_students = None | ||||||
|  |         elif obj.id == 317: # members' meeting 7:00 PM 3rd Thursday of odd months, Wednesday of even months | ||||||
|  |             if utils.today_alberta_tz().month % 2 == 0: | ||||||
|  |                 date = next_date(calendar.WEDNESDAY, week_num=3) | ||||||
|  |             else: | ||||||
|  |                 date = next_date(calendar.THURSDAY, week_num=3) | ||||||
|  |             time = datetime.time(19, 0) | ||||||
|  |             dt = datetime.datetime.combine(date, time) | ||||||
|  |             dt = utils.TIMEZONE_CALGARY.localize(dt) | ||||||
|  |             cost = 0 | ||||||
|  |             max_students = None | ||||||
|  |         elif prev_session: | ||||||
|  |             prev_session = obj.sessions.last() | ||||||
|  |             dt = prev_session.datetime | ||||||
|  |  | ||||||
|  |             if course_is_usually_monthly(obj): | ||||||
|  |                 offset_weeks = 4 | ||||||
|  |             else: | ||||||
|  |                 offset_weeks = 1 | ||||||
|  |             dt = dt + datetime.timedelta(weeks=offset_weeks) | ||||||
|  |  | ||||||
|  |             five_days_from_now = utils.now_alberta_tz() + datetime.timedelta(days=5) | ||||||
|  |             while dt < five_days_from_now: | ||||||
|  |                 dt = dt + datetime.timedelta(weeks=offset_weeks) | ||||||
|  |  | ||||||
|  |             cost = prev_session.cost | ||||||
|  |             max_students = prev_session.max_students | ||||||
|  |         else: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         return dict(datetime=dt, cost=str(cost), max_students=max_students) | ||||||
|  |  | ||||||
|  |  | ||||||
| class UserTrainingSerializer(serializers.ModelSerializer): | class UserTrainingSerializer(serializers.ModelSerializer): | ||||||
|     session = SessionListSerializer() |     session = SessionListSerializer() | ||||||
|   | |||||||
| @@ -420,6 +420,11 @@ export function InstructorClassList(props) { | |||||||
| 		).sort((a, b) => a.datetime > b.datetime ? 1 : -1)); | 		).sort((a, b) => a.datetime > b.datetime ? 1 : -1)); | ||||||
| 	}, [input.datetime]); | 	}, [input.datetime]); | ||||||
|  |  | ||||||
|  | 	const fillSuggestion = (e) => { | ||||||
|  | 		e.preventDefault(); | ||||||
|  | 		setInput({ ...input, ...course.suggestion }); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
| 	return ( | 	return ( | ||||||
| 		<div> | 		<div> | ||||||
| 			<Header size='medium'>Instructor Panel</Header> | 			<Header size='medium'>Instructor Panel</Header> | ||||||
| @@ -434,6 +439,10 @@ export function InstructorClassList(props) { | |||||||
|  |  | ||||||
| 							<p>Documentation: <a href='https://wiki.protospace.ca/Be_a_Course_Instructor' target='_blank' rel='noopener noreferrer'>https://wiki.protospace.ca/Be_a_Course_Instructor</a></p> | 							<p>Documentation: <a href='https://wiki.protospace.ca/Be_a_Course_Instructor' target='_blank' rel='noopener noreferrer'>https://wiki.protospace.ca/Be_a_Course_Instructor</a></p> | ||||||
|  |  | ||||||
|  | 							{course.suggestion && | ||||||
|  | 								<p><Button onClick={fillSuggestion}>Suggest</Button> based off previous classes.</p> | ||||||
|  | 							} | ||||||
|  |  | ||||||
| 							<InstructorClassEditor input={input} setInput={setInput} error={error} token={token} /> | 							<InstructorClassEditor input={input} setInput={setInput} error={error} token={token} /> | ||||||
|  |  | ||||||
| 							<Form.Button loading={loading} error={error.non_field_errors}> | 							<Form.Button loading={loading} error={error.non_field_errors}> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user