def delete_appointment_with_fcm(appointment: Appointment): appointment.update(deleted=True) user_to_send_to = appointment.teacher.user other_user = appointment.student.user if current_user == appointment.teacher.user: other_user = user_to_send_to user_to_send_to = appointment.student.user if user_to_send_to.firebase_token: try: logger.debug(f"sending fcm to {user_to_send_to} for deleting lesson") FCM.notify( token=user_to_send_to.firebase_token, title=gettext("Lesson Deleted"), body=gettext( "The lesson at %(value)s has been deleted by %(user)s.", value=format_datetime( appointment.date, locale=LOCALE, format="short", tzinfo=timezone(TIMEZONE), ), user=other_user.name, ), ) except NotificationError: pass
def test_teacher_available_hours_with_rules( teacher, student, requester, meetup, dropoff ): tomorrow = datetime.utcnow().replace(hour=7, minute=0) + timedelta(days=1) kwargs = { "teacher": teacher, "day": 1, "from_hour": tomorrow.hour, "from_minutes": tomorrow.minute, "to_hour": 23, "to_minutes": 59, "on_date": tomorrow, } WorkDay.create(**kwargs) for i in range(3): Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=teacher.lesson_duration, date=tomorrow + timedelta(minutes=i), meetup_place=meetup, dropoff_place=dropoff, is_approved=True, ) hours_with_rules = list(teacher.available_hours(tomorrow, student=student)) hours_without_rules = list(teacher.available_hours(tomorrow)) assert hours_with_rules != hours_without_rules
def test_student_blocked_hours_by_test( teacher, student, meetup, dropoff, auth, requester ): date = (datetime.utcnow() + timedelta(days=1)).replace(hour=13, minute=0) data = { "teacher": teacher, "from_hour": 13, "from_minutes": 0, "to_hour": 17, "to_minutes": 0, "on_date": date, } WorkDay.create(**data) Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=date, meetup_place=meetup, dropoff_place=dropoff, is_approved=False, type=AppointmentType.TEST.value, ) auth.login(email=student.user.email) resp = requester.post( f"/teacher/{teacher.id}/available_hours", json={"date": date.strftime(WORKDAY_DATE_FORMAT)}, ) hours = [hour[0] for hour in resp.json["data"]] assert all(date.strftime("%H:%M") not in hour for hour in hours)
def test_lessons_done(teacher, student, meetup, dropoff): """create new lesson for student, check lessons_done updates""" old_lesson_number = student.lessons_done lesson = Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=60, date=datetime.utcnow(), meetup_place=meetup, dropoff_place=dropoff, ) assert student.lessons_done == old_lesson_number + 60 / teacher.lesson_duration assert student.lessons_done == lesson.lesson_number old_lesson_number = student.lessons_done Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=datetime.utcnow() + timedelta(hours=20), meetup_place=meetup, dropoff_place=dropoff, ) assert (student.lessons_done == old_lesson_number ) # because it's later than now, it hasn't changed st = Student.query.filter( Student.lessons_done == old_lesson_number).first() assert st.id == student.id
def test_filter_data(teacher, student, meetup, dropoff): date = datetime.utcnow() + timedelta(days=100) lesson = Appointment.create( teacher=teacher, student=student, creator=student.user, duration=40, date=date, meetup_place=meetup, dropoff_place=dropoff, ) # date=ge:DATE date = datetime.strftime(date, DATE_FORMAT) lessons_from_db = Appointment.query.filter( Appointment._filter_data("date", f"ge:{date}")).all() assert lessons_from_db[0] == lesson # student_id=2 lessons_from_db = Appointment.query.filter( Appointment._filter_data("student_id", 2)).all() assert not lessons_from_db # date=gggg:DATE lessons_from_db = Appointment.query.filter( Appointment._filter_data( "date", f"ggfggg:{date}")).all() # supposed to translate to equal assert lessons_from_db[0] == lesson # date=DATE with pytest.raises(ValueError): lessons_from_db = Appointment.query.filter( Appointment._filter_data("date", f"{date}")).all()
def test_sort_data(teacher, student, meetup, dropoff): lessons = [] for _ in range(3): lessons.append( Appointment.create( teacher=teacher, student=student, creator=student.user, duration=40, date=datetime.utcnow(), meetup_place=meetup, dropoff_place=dropoff, )) args = {"order_by": "created_at desc"} lessons_from_db = Appointment.query.order_by( Appointment._sort_data(args)).all() assert lessons_from_db[0] == lessons[-1] args = {"order_by": "does_not_exist desc"} lessons_from_db = Appointment.query.order_by( Appointment._sort_data(args)).all() assert lessons_from_db args = {"order_by": "created_at huh"} lessons_from_db = Appointment.query.order_by( Appointment._sort_data(args)).all() assert lessons_from_db
def test_commons(teacher, student, meetup, dropoff): """add 3 lessons, 2 of them with same meetup and dropoff. check returns of common_meetup and common_dropoff""" for _ in range(2): Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=datetime.utcnow(), meetup_place=meetup, dropoff_place=dropoff, ) second_meetup = Place.create(description="other", used_as=PlaceType.meetup.value, student=student) second_dropoff = Place.create(description="other", used_as=PlaceType.dropoff.value, student=student) Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=datetime.utcnow(), meetup_place=second_meetup, dropoff_place=second_dropoff, ) assert student.common_meetup == meetup assert student.common_dropoff == dropoff
def test_total_lessons_price(teacher, student, meetup, dropoff): st = Student.query.filter(Student.total_lessons_price == 0).first() # no lesson has been done yet assert st == student Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=datetime.utcnow() - timedelta(hours=2), meetup_place=meetup, dropoff_place=dropoff, is_approved=True, ) st = Student.query.filter( Student.total_lessons_price == teacher.price).first() assert st == student student.update(price=1000) # this is still true because lessons have a fixed price once scheduled assert student.total_lessons_price == teacher.price Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=datetime.utcnow() - timedelta(hours=2), meetup_place=meetup, dropoff_place=dropoff, is_approved=True, ) assert student.total_lessons_price == teacher.price + student.price
def test_init_hours(student, teacher, meetup, dropoff): # TODO improve test logic initial_hours = LessonRule.hours date = datetime.utcnow().replace(hour=6, minute=0) + timedelta(days=2) Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=teacher.lesson_duration, date=date, meetup_place=meetup, dropoff_place=dropoff, is_approved=True, ) query = Appointment.query.filter( func.extract("day", Appointment.date) == date.day).filter( func.extract("month", Appointment.date) == date.month) new_hours = LessonRule.init_hours( date, student, teacher.work_hours_for_date(date), teacher.taken_appointments_tuples(query, only_approved=True), ) assert initial_hours != new_hours # we want to fill the gap after 6, so hours 7 and 8 should be really cold hour_8 = new_hours[1].score hour_7 = new_hours[0].score old_hour_7 = initial_hours[0].score old_hour_8 = initial_hours[1].score assert hour_7 < old_hour_7 assert hour_8 < old_hour_8
def test_topics_in_progress(teacher, student, topic, meetup, dropoff, lesson): lesson_topic = LessonTopic(is_finished=False, topic_id=topic.id) lesson.topics.append(lesson_topic) lesson = Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=datetime.utcnow(), meetup_place=meetup, dropoff_place=dropoff, ) lesson_topic = LessonTopic(is_finished=False, topic_id=topic.id) lesson.topics.append(lesson_topic) lt = student._lesson_topics(is_finished=False) in_progress = student._topics_in_progress(lt) assert topic in in_progress lesson = Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=datetime.utcnow(), meetup_place=meetup, dropoff_place=dropoff, ) lesson_topic = LessonTopic(is_finished=True, topic_id=topic.id) lesson.topics.append(lesson_topic) lt = student._lesson_topics(is_finished=False) in_progress = student._topics_in_progress(lt) assert len(in_progress) == 0
def test_available_hours_route_with_places( teacher, student, meetup, dropoff, auth, requester, responses ): auth.login(email=student.user.email) tomorrow = datetime.utcnow() + timedelta(days=1) data = { "teacher": teacher, "from_hour": 7, "from_minutes": 0, "to_hour": 17, "to_minutes": 0, "on_date": tomorrow, } WorkDay.create(**data) date = tomorrow.replace(hour=16, minute=0) Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=date, meetup_place=meetup, dropoff_place=dropoff, is_approved=True, # only check places for approved lessons ) ret = { "destination_addresses": ["HaNassi Blvd, Haifa, Israel"], "origin_addresses": ["Gruenbaum St 3, Haifa, Israel"], "rows": [ { "elements": [ { "distance": {"text": "4.8 mi", "value": 7742}, "duration": {"text": "17 mins", "value": 1037}, "status": "OK", } ] } ], "status": "OK", } responses.add( responses.GET, "https://maps.googleapis.com/maps/api/distancematrix/json", body=json.dumps(ret), status=200, content_type="application/json", ) resp = requester.post( f"/teacher/{teacher.id}/available_hours", json={ "date": date.strftime(WORKDAY_DATE_FORMAT), "meetup_place_id": "test1", "dropoff_place_id": "test2", }, ) assert resp.json["data"]
def total_lessons_price(cls): q = (select([coalesce(func.sum(Appointment.price), 0)]).where( Appointment.approved_lessons_filter( Appointment.date < datetime.utcnow(), Appointment.student_id == cls.id, )).label("total_lessons_price")) return q + cls.number_of_old_lessons * cls.price
def approve_lesson(lesson_id): lesson = current_user.teacher.lessons.filter_by(id=lesson_id).first() if not lesson: raise RouteError("Lesson does not exist", 404) # check if there isn't another lesson at the same time same_time_lesson = Appointment.query.filter( Appointment.approved_lessons_filter( Appointment.date == lesson.date, Appointment.id != lesson.id ) ).first() if same_time_lesson: raise RouteError("There is another lesson at the same time.") lesson.update(is_approved=True) if lesson.student.user.firebase_token: logger.debug(f"sending fcm for lesson approval") try: FCM.notify( token=lesson.student.user.firebase_token, title=gettext("Lesson Approved"), body=gettext( "Lesson at %(date)s has been approved!", date=format_datetime( lesson.date, locale=LOCALE, format="short", tzinfo=timezone(TIMEZONE), ), ), ) except NotificationError: pass return {"message": "Lesson approved."}
def handle_teacher_hours( teacher: Teacher, date: datetime, duration: int, type_: Optional[AppointmentType], appointment: Optional[Appointment], ): """check if there are existing lessons in the date given. If so - is test? - delete all existing lessons. NO - raise RouteError""" # check if there's another lesson that ends or starts within this time end_date = date + timedelta(minutes=duration) existing_lessons = Appointment.query.filter_by(teacher=teacher).filter( Appointment.appointments_between(date, end_date) ) if appointment: existing_lessons = existing_lessons.filter(Appointment.id != appointment.id) existing_lessons = existing_lessons.all() logger.debug(f"For {date}, found existing lessons: {existing_lessons}") if existing_lessons: if type_ == AppointmentType.LESSON or date < datetime.utcnow(): raise RouteError("This hour is not available.") # delete all lessons and send FCMs for existing_appointment in existing_lessons: if existing_appointment.type == AppointmentType.LESSON: delete_appointment_with_fcm(existing_appointment)
def test_more_than_lessons_in_week(student, teacher, hours, meetup, dropoff): date = datetime.utcnow() + timedelta(days=2) rule = more_than_lessons_week.MoreThanLessonsWeek(date, student, hours) assert not rule.blacklisted()["start_hour"] for i in range(3): Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=teacher.lesson_duration, date=date + timedelta(minutes=i * teacher.lesson_duration), meetup_place=meetup, dropoff_place=dropoff, is_approved=True, ) assert rule.blacklisted()["start_hour"]
def test_teacher_available_hours(teacher, student, requester, meetup, dropoff): tomorrow = datetime.utcnow().replace(hour=7, minute=0) + timedelta(days=1) kwargs = { "teacher": teacher, "day": 1, "from_hour": tomorrow.hour, "from_minutes": tomorrow.minute, "to_hour": 23, "to_minutes": 59, "on_date": tomorrow, } WorkDay.create(**kwargs) assert next(teacher.available_hours(tomorrow))[0] == tomorrow # we create a non approved lesson - available hours should still contain its date lesson = Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=teacher.lesson_duration, date=tomorrow, meetup_place=meetup, dropoff_place=dropoff, is_approved=False, ) assert next(teacher.available_hours(tomorrow, only_approved=True))[0] == tomorrow assert next(teacher.available_hours(tomorrow, only_approved=False))[0] != tomorrow
def test_regular_students(student, teacher, hours, meetup, dropoff): date = datetime.utcnow() - timedelta(days=2) rule = regular_students.RegularStudents(date, student, hours) assert not rule.blacklisted()["start_hour"] for i in range(10): Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=teacher.lesson_duration, date=date + timedelta(minutes=i * teacher.lesson_duration), meetup_place=meetup, dropoff_place=dropoff, is_approved=True, ) assert rule.blacklisted()["start_hour"]
def __init__(self, date, student, hours, places): super().__init__(date, student, hours) self.meetup_place_id = places[0] self.dropoff_place_id = places[1] self.today_lessons = self.student.teacher.lessons.filter( Appointment.approved_filter( func.extract("day", Appointment.date) == self.date.day, func.extract("month", Appointment.date) == self.date.month, )).all()
def test_filter_multiple_params(teacher, student, meetup, dropoff): date = datetime.utcnow() + timedelta(days=100) month_start = date.replace(day=1, hour=0, minute=0, second=0, microsecond=0) month_range = calendar.monthrange(month_start.year, month_start.month) month_end = date.replace( month=month_start.month, day=month_range[1], hour=0, minute=0, second=0, microsecond=0, ) duration = 1200 lesson = Appointment.create( teacher=teacher, student=student, creator=student.user, duration=duration, date=date, meetup_place=meetup, dropoff_place=dropoff, ) Appointment.create( teacher=teacher, student=student, creator=student.user, duration=duration, date=date + timedelta(days=100), # so it won't be the same month meetup_place=meetup, dropoff_place=dropoff, ) month_end = datetime.strftime(month_end, DATE_FORMAT) month_start = datetime.strftime(month_start, DATE_FORMAT) lessons_from_db = (Appointment.query.filter( Appointment._filter_data("date", f"ge:{month_start}")).filter( Appointment._filter_data("date", f"le:{month_end}")).all()) assert len(lessons_from_db) == 1 assert lessons_from_db[0] == lesson
def lessons_done(self) -> int: """return the number of a new lesson: num of latest lesson+1""" latest_lesson = (self.lessons.filter( Appointment.approved_lessons_filter( Appointment.date < datetime.utcnow())).order_by( Appointment.date.desc()).limit(1).one_or_none()) starting_count = self.number_of_old_lessons if not latest_lesson: return starting_count return latest_lesson.lesson_number
def lessons_done(cls): q = select([ cast(func.sum(Appointment.duration), db.Float) / (func.count(Appointment.student_id) * Teacher.lesson_duration) ]).where( Appointment.approved_lessons_filter( Appointment.date < datetime.utcnow(), Appointment.student_id == cls.id)) j = Student.__table__.join(Teacher.__table__) q = q.select_from(j).label("lessons_done") return q + cls.number_of_old_lessons
def test_taken_appointments_tuples(teacher, student, meetup, dropoff, lesson): taken_lessons = teacher.taken_appointments_tuples( Appointment.query, only_approved=False ) assert len(taken_lessons) == 1 assert isinstance(taken_lessons[0], tuple) Appointment.create( teacher=teacher, student=student, # schedule to 5 days from now to it won't bother with no test creator=teacher.user, duration=40, date=(datetime.utcnow() + timedelta(days=5)), meetup_place=meetup, dropoff_place=dropoff, is_approved=False, ) taken_lessons = teacher.taken_appointments_tuples( Appointment.query, only_approved=True ) assert len(taken_lessons) == 1
def appointment(id_): appointment = Appointment.get_by_id(id_) if not appointment: raise RouteError("Appointment does not exist.") if current_user.id not in ( appointment.student.user.id, appointment.teacher.user.id, ): raise RouteError("You are not allowed to view this appointment.", 401) return {"data": appointment.to_dict()}
def test_appointments_between(teacher, student, requester, meetup, dropoff, date, end_date, result): """1. get lessons with the same starting hour 2. get lessons with the same ending hour 3. get lessons that end within existing hours 4. get lessons that start within existing hours 5. lessons that start when existing lesson ends 6. lesson that ends when existing lesson starts 7. exactly the same hours """ lesson = create_lesson(teacher, student, meetup, dropoff, tomorrow) existing_lessons = Appointment.query.filter( Appointment.appointments_between(date, end_date)).all() assert (lesson in existing_lessons) == result
def filter_appointments(self, args: werkzeug.datastructures.MultiDict): args = args.copy() query = self.appointments if "deleted" not in args or self.__class__.__name__.lower( ) == "student": # default to non deleted items query = query.filter_by(deleted=False) try: args.pop("deleted") except KeyError: pass return Appointment.filter_and_sort(args, query=query, with_pagination=True)
def test_total_lessons_price_with_different_prices(teacher, student, meetup, dropoff): st = Student.query.filter(Student.total_lessons_price == 0).first() # no lesson has been done yet assert st == student price = 100 prices = price for x in range(3): Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=datetime.utcnow() - timedelta(hours=x), meetup_place=meetup, dropoff_place=dropoff, is_approved=True, price=price * x, ) prices += price * x assert student.total_lessons_price == prices st = Student.query.filter(Student.total_lessons_price == prices).first() assert st == student
def test_lesson_topics(teacher, student, topic, meetup, dropoff): lesson = Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=datetime.utcnow(), meetup_place=meetup, dropoff_place=dropoff, ) lesson_topic = LessonTopic(is_finished=False, topic_id=topic.id) lesson.topics.append(lesson_topic) lt = student._lesson_topics(is_finished=False) assert lesson_topic in lt assert isinstance(lt, BaseQuery)
def test_available_hours_route(teacher, student, meetup, dropoff, auth, requester): auth.login(email=teacher.user.email) tomorrow = datetime.utcnow() + timedelta(days=1) date = tomorrow.strftime(WORKDAY_DATE_FORMAT) time_and_date = date + "T13:30:20.123123Z" data = { "teacher": teacher, "from_hour": 13, "from_minutes": 0, "to_hour": 17, "to_minutes": 0, "on_date": tomorrow, } WorkDay.create(**data) # now let's add a lesson lesson_date = datetime.strptime(time_and_date, DATE_FORMAT) lesson = Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=lesson_date, meetup_place=meetup, dropoff_place=dropoff, is_approved=False, ) resp = requester.post(f"/teacher/{teacher.id}/available_hours", json={"date": date}) assert len(resp.json["data"]) == 6 lesson.update(is_approved=True) resp = requester.post( f"/teacher/{teacher.id}/available_hours", json={"date": date, "duration": "120"} ) assert len(resp.json["data"]) == 1 auth.logout() # if we login as student, we shouldn't this non approved lesson date auth.login(email=student.user.email) lesson.update(is_approved=False) resp = requester.post(f"/teacher/{teacher.id}/available_hours", json={"date": date}) hours = [hour[0] for hour in resp.json["data"]] assert all(lesson_date.strftime("%H:%M") not in hour for hour in hours)
def test_balance(teacher, student, meetup, dropoff): # we have one lesson currently and 0 payments, but the lesson hasn't yet happened assert student.balance == 0 lesson = Appointment.create( teacher=teacher, student=student, creator=teacher.user, duration=40, date=datetime.utcnow() - timedelta(hours=2), meetup_place=meetup, dropoff_place=dropoff, is_approved=True, ) assert student.balance == -teacher.price lesson.update(is_approved=False) assert student.balance == 0 st = Student.query.filter(Student.balance == 0).first() assert st == student Payment.create(amount=teacher.price, teacher=teacher, student=student) assert student.balance == teacher.price
def new_appointment(): data = flask.request.get_json() if not data.get("date"): raise RouteError("Please insert the date of the appointment.") appointment = Appointment.create(**get_data(data, current_user)) # send fcm to the user who wasn't the one creating the lesson user_to_send_to = appointment.teacher.user body_text = gettext( "%(student)s wants to schedule a new lesson at %(date)s. Click here to check it out.", student=appointment.student.user.name, date=format_datetime( appointment.date, locale=LOCALE, format="short", tzinfo=timezone(TIMEZONE) ), ) if appointment.creator == appointment.teacher.user and appointment.student: user_to_send_to = appointment.student.user body_text = gettext( "%(teacher)s scheduled a new lesson at %(value)s. Click here to check it out.", teacher=appointment.teacher.user.name, value=format_datetime( appointment.date, locale=LOCALE, format="short", tzinfo=timezone(TIMEZONE), ), ) if user_to_send_to.firebase_token: logger.debug(f"sending fcm to {user_to_send_to} for new appointment") try: FCM.notify( token=user_to_send_to.firebase_token, title=gettext("New Lesson!"), body=body_text, ) except NotificationError: pass return {"data": appointment.to_dict()}, 201