def test_filter_categories(client: MoodleClient, setenv_category: t.Callable): ''' Does client mix well categories filtering and filters keywords? ''' setenv_category('1', '2') with patch.object(client, '_get_courses') as get_courses: courses = get_courses.return_value = [ JsonDict(course_id='foo', category=1), JsonDict(course_id='bar', category=2), JsonDict(course_id='baz', category=5), JsonDict(course_id='spam', category=10), ] client.use_categories() client.load_courses() assert client.courses == courses[:2] client.courses = [] client.load_courses(course_id='foo') assert client.courses == courses[:1] client.courses = [] client.load_courses(course_id=('spam', 'baz')) assert client.courses == []
def format_course(cls, course: JsonType) -> Course: '''Format raw json response to convinient dictionary. Args: course (JsonType): Raw Json from LMS containing course data. Returns: Course: JsonDict with course information ''' return JsonDict({ 'id': course['id'], 'course_id': cls.format_string(course['shortname']), 'title': course['displayname'], 'category': course['categoryid'], 'need_nbgrader': course['categoryid'] == int( os.environ['MOODLE_NBGRADER_CATEGORY_ID']), 'instructors': [], 'students': [], 'graders': [], 'lms_lineitems_endpoint': f'{os.environ["MOODLE_BASE_URL"]}/mod/lti/services.php/{course["id"]}/lineitems' })
def process_data( self, courses: t.Optional[Course] = None, json_path: t.Optional[PathLike] = None, **filters: Filters, ) -> None: '''Iterates through json file and calls self.process_course for every course found in a file. Args: json_path (t.Optional[PathLike]): Custom path to json. Defaults to None. filters (t.Dict[str, t.Union[t.Sequence[t.AnyStr], t.AnyStr]]): key-value pairs where value can be both single value or list of valid items. ''' if courses is None: courses = (JsonDict(crs) for crs in load_moodle_courses(json_path)) for course in courses: if self.helper.skip_course(course, filters): logger.debug(f'Skipping course {course.title!r}') continue logger.debug(f'Processing course {course.title!r}') self.courses.append(course) self.process_course(course)
def test_filter_courses(client: MoodleClient): with patch.object(client, '_get_courses') as get_courses: courses = get_courses.return_value = [ JsonDict(course_id='foo_course'), JsonDict(course_id='bar_course'), ] client.load_courses(course_id='foo_course') assert client.courses == courses[:1] # Filtering with multiple options courses = get_courses.return_value = [ JsonDict(course_id='foo'), JsonDict(course_id='bar'), JsonDict(course_id='egg'), JsonDict(course_id='baz'), ] for seq in (list, tuple, set, frozenset): client.courses = [] client.load_courses(course_id=seq(('foo', 'bar'))) assert client.courses == courses[:2] # Filtering by multiple fields courses = get_courses.return_value = [ JsonDict(title='The Foo', course_id='baz'), JsonDict(title='The Bar', course_id='baz'), JsonDict(title='The Spam', course_id='spam'), ] client.courses = [] client.load_courses(title=('The Foo', 'The Spam'), course_id='baz') assert client.courses == [courses[0]] # Empty sequence is not allowed with pytest.raises(ValueError): client.load_courses(title=(), course_id='baz') # Invalid key for the course with pytest.raises(KeyError): client.load_courses(foo='bar')
def test_get_categories(client: MoodleClient): ''' Does get categories returns courses in valid format? ''' courses = [ JsonDict(title='Foo Course', course_id='foo_course', category=1) ] with patch.object(client, '_get_courses', return_value=[]) as get_course: assert client.get_categories() == [] get_course.return_value = courses assert client.get_categories() == [('Foo Course', 'foo_course', 1)]
def _course_fabric( *, id: t.Optional[int] = None, title: t.Optional[str] = None, course_id: t.Optional[str] = None, category: t.Optional[int] = None, instructors: t.Optional[t.List[User]] = None, students: t.Optional[t.List[User]] = None, graders: t.Optional[t.List[User]] = None, ) -> Course: ''' Create new course. Keyword-only arguments allowed. ''' if id and not isinstance(id, int): raise TypeError('User id must be int.') if course_id and re.findall(r'\W+', course_id): raise ValueError('Short name contains invalid characters: %s' % course_id) # Python does strange things # with lists in default values ... instructors = instructors or [] students = students or [] graders = graders or [] for user in instructors + students + graders: assert tuple(user.keys()) == ('id', 'username', 'email', 'first_name', 'last_name', 'roles') return JsonDict({ 'id': id or random.randint(1, 100), 'title': title or random_string(20), 'course_id': course_id or random_string(20), 'category': category or random.randint(0, 100), 'instructors': [] or [user_fabric() for _ in range(random.randint(0, 5))], 'students': [] or [user_fabric() for _ in range(random.randint(0, 5))], 'graders': [] or [user_fabric() for _ in range(random.randint(0, 5))], })
def _user_fabric( *, id: t.Optional[int] = None, username: t.Optional[str] = None, email: t.Optional[str] = None, first_name: t.Optional[str] = None, last_name: t.Optional[str] = None, roles: t.Optional[t.List[Role]] = None, ) -> User: ''' Create new user. Keyword-only arguments allowed. ''' if id and not isinstance(id, int): raise TypeError('User id must be int.') if email and not valid_email(email): raise ValueError('Invalid email: %s' % email) if username and re.findall(r'\W+', username): raise ValueError('Username contains invalid characters: %s' % username) if roles: for role in roles: if role not in ROLES: raise ValueError( 'Role [%s] does not set in moodle.settings.ROLES tuple.' % role) return JsonDict({ 'id': id or random.randint(0, 100), 'username': username or random_string(10), 'email': email or f'{random_string(10)}@mail.com', 'first_name': first_name or random_string(10), 'last_name': last_name or random_string(10), 'roles': roles or [random.choice(ROLES) for _ in range(random.randint(0, 5))], })
def format_user(cls, user: JsonType) -> User: '''Format raw json response to convinient dictionary. Args: user (JsonType): Raw Json from LMS containing user data. Returns: User: JsonDict with user information ''' return JsonDict({ 'id': user['id'], 'first_name': user['firstname'], 'last_name': user['lastname'], 'username': cls.format_string(user['username']), 'email': user['email'], 'roles': [role['shortname'] for role in user['roles']], })
def test_load_users(client: MoodleClient, student: User, teacher: User, course: Course): ''' Test transforming data process. ''' # reset groups course['graders'] = [] course['instructors'] = [] course['students'] = [] client.courses = [ course, ] # we have teacher and two repeated students. with patch.object(client, '_get_users') as mocked_get_users: mocked_get_users.return_value = [ JsonDict(u) for u in (student, student, teacher) ] client.load_users() # do the same work client does with users # to compare data student.role = student.pop('roles')[0] teacher.role = teacher.pop('roles')[0] mocked_get_users.assert_called_once_with(course) assert client.courses == [{ **course, 'graders': [teacher], 'students': [student, student] }] assert client.users == { teacher.username: teacher, student.username: student }
def create_service(course_id: str, api_token: str, port: int = 0) -> JsonDict: ''' Fills service template with provided data. Args: course_id (str): Normalized course's name. api_token (str): Jupyterhub Service API token. port (int): Port to run service on. Defaults to 0. Returns: JsonDict: Dict that you can add to services in jupyterhub_config.py ''' return JsonDict({ 'name': course_id, 'admin': True, 'url': f'http://127.0.0.1:{9000 + port}', 'command': [ 'jupyterhub-singleuser', f'--group=formgrade-{course_id}', '--debug', '--allow-root', ], 'user': f'grader-{course_id}', 'cwd': f'/home/grader-{course_id}', 'api_token': api_token, 'environment': { 'JUPYTERHUB_SERVICE_USER': f'grader-{course_id}' } })
def _format(users: JsonType) -> t.List[User]: return [JsonDict(user) for user in users]