def get_sitemap_route(request): """ Generate a sitemap so Google can find Sagefy's content. Should be linked to from https://sagefy.org/robots.txt Sitemap: https://sagefy.org/s/sitemap.txt """ # TODO-1 cache in redis db_conn = request['db_conn'] sitemap = DEFAULTS | set() # Card, unit, subject kinds = { 'card': list_all_card_entity_ids(db_conn), 'unit': list_all_unit_entity_ids(db_conn), 'subject': list_all_subject_entity_ids(db_conn), } for kind, entity_ids in kinds.items(): for entity_id in entity_ids: sitemap.add('https://sagefy.org/{kind}s/{id}'.format( id=convert_uuid_to_slug(entity_id), kind=kind, )) # TODO-2 ...and versions pages # Topic for topic in list_topics(db_conn, {}): sitemap.add('https://sagefy.org/topics/{id}'.format( id=convert_uuid_to_slug(topic['id']))) # User users = [deliver_user(user) for user in list_users(db_conn, {})] for user in users: sitemap.add('https://sagefy.org/users/{id}'.format( id=convert_uuid_to_slug(user['id']))) sitemap = '\n'.join(sitemap) return 200, sitemap
def test_is_valid_members(db_conn): create_subject_test_data(db_conn) data = { 'members': [{ 'kind': 'unit', 'id': convert_uuid_to_slug(uuid.uuid4()), }], } errors = is_valid_members(db_conn, data) assert errors data = { 'members': [{ 'kind': 'unit', 'id': convert_uuid_to_slug(unit_a_uuid), }], } errors = is_valid_members(db_conn, data) assert not errors data = { 'members': [{ 'kind': 'subject', 'id': convert_uuid_to_slug(test_subject_a_uuid), }], } errors = is_valid_members(db_conn, data) assert not errors
def test_learn_card(db_conn, session): """ Expect to get a card for learn mode. (200) """ create_test_cards(db_conn) redis_key = 'learning_context_{user_id}'.format( user_id=convert_uuid_to_slug(user_id)) red.set( redis_key, json.dumps({ 'unit': { 'entity_id': convert_uuid_to_slug(unit_id) }, })) request = {'cookies': {'session_id': session}, 'db_conn': db_conn} code, response = routes.card.learn_card_route( request, convert_uuid_to_slug(card_id)) red.delete(redis_key) assert not response.get('errors') assert code == 200 assert 'order' not in response['card'] # TODO-3 assert 'correct' not in response['card']['options'][0] # TODO-3 assert 'feedback' not in response['card']['options'][0] assert 'subject' in response assert 'unit' in response
def test_respond_card(db_conn, session): """ Expect to respond to a card. (200) """ create_test_cards(db_conn) redis_key = 'learning_context_{user_id}'.format( user_id=convert_uuid_to_slug(user_id)) red.set( redis_key, json.dumps({ 'card': { 'entity_id': convert_uuid_to_slug(card_id) }, 'unit': { 'entity_id': convert_uuid_to_slug(unit_id) }, })) request = { 'params': { 'response': convert_uuid_to_slug(good_response_id) }, 'cookies': { 'session_id': session }, 'db_conn': db_conn, } code, response = routes.card.respond_to_card_route(request, card_id) red.delete(redis_key) assert not response.get('errors') assert code == 200 assert 'response' in response assert 'feedback' in response
def test_respond_card_400b(db_conn, session): """ Expect response to a card to make sense. (400) """ create_test_cards(db_conn) redis_key = 'learning_context_{user_id}'.format( user_id=convert_uuid_to_slug(user_id)) red.set( redis_key, json.dumps({ 'card': { 'entity_id': convert_uuid_to_slug(card_id) }, 'unit': { 'entity_id': convert_uuid_to_slug(unit_id) }, })) request = { 'params': { 'response': convert_uuid_to_slug(uuid.uuid4()) }, 'cookies': { 'session_id': session }, 'db_conn': db_conn, } code, response = routes.card.respond_to_card_route(request, card_id) red.delete(redis_key) assert code == 400 assert 'errors' in response
def create_response_test_data(db_conn): users = [{ 'id': user_a_uuid, 'name': 'test', 'email': '*****@*****.**', 'password': '******', }] raw_insert_users(db_conn, users) units = [{ 'entity_id': test_unit_uuid, 'name': 'Calculus', 'user_id': user_a_uuid, 'body': 'Calculus is fun sometimes.', }] raw_insert_units(db_conn, units) cards = [{ 'user_id': user_a_uuid, 'entity_id': test_card_uuid, 'unit_id': test_unit_uuid, 'kind': 'choice', 'name': 'Meaning of Life', 'data': { 'body': 'What is the meaning of life?', 'options': [{ 'id': convert_uuid_to_slug(test_card_option1_uuid), 'value': '42', 'correct': True, 'feedback': 'Yay!', }, { 'id': convert_uuid_to_slug(test_card_option2_uuid), 'value': 'love', 'correct': False, 'feedback': 'Boo!', }], 'order': 'set', 'max_options_to_show': 4, }, }] raw_insert_cards(db_conn, cards) responses = [{ 'user_id': user_a_uuid, 'card_id': test_card_uuid, 'unit_id': test_unit_uuid, 'response': test_card_option1_uuid, 'score': 1, 'learned': 0.5, }, { 'user_id': user_a_uuid, 'card_id': test_card_uuid, 'unit_id': test_unit_uuid, 'response': test_card_option2_uuid, 'score': 0, 'learned': 0.4, }] raw_insert_responses(db_conn, responses)
def choose_unit_route(request, subject_id, unit_id): """ Updates the learner's information based on the unit they have chosen. NEXT STATE POST Chosen Unit -> GET Learn Card """ # TODO-3 simplify this method. should it be broken up or moved to model? db_conn = request['db_conn'] current_user = get_current_user(request) if not current_user: return abort(401, 'NcrRkqIlSW-BkcWLBkwORQ') unit = get_latest_accepted_unit(db_conn, unit_id) if not unit: return abort(404, 'PIti_qwAQ7WXwQwNZertxw') # If the unit isn't in the subject... context = get_learning_context(current_user) subject_ids = [ convert_uuid_to_slug(subject['entity_id']) for subject in list_subjects_by_unit_recursive(db_conn, unit_id) ] context_subject_id = context.get('subject', {}).get('entity_id') if context_subject_id not in subject_ids: return 400, { 'errors': [{ 'message': 'Unit not in subject.', 'ref': 'r32fH0eCRZCivJoh-hKwZQ', }] } # Or, the unit doesn't need to be learned... status = judge(db_conn, unit, current_user) if status == "done": return 400, { 'errors': [{ 'message': 'Unit not needed.', 'ref': 'YTU27E63Rfiy3Rqmqd6Bew', }] } # Choose a card for the learner to learn card = choose_card(db_conn, current_user, unit) if card: next_ = { 'method': 'GET', 'path': '/s/cards/{card_id}/learn'.format( card_id=convert_uuid_to_slug(card['entity_id'])), } else: next_ = {} set_learning_context(current_user, unit=unit, card=card if card else None, next=next_) return 200, {'next': next_}
def test_validate_card_response(db_conn): create_card_test_data(db_conn) card = get_card_version(db_conn, version_id=card_version_b_uuid) errors = validate_card_response(card, response=convert_uuid_to_slug( uuid.uuid4())) assert errors errors = validate_card_response( card, response=convert_uuid_to_slug(test_card_option1_uuid)) assert not errors
def create_route_subject_test_data(db_conn): units = [{ 'version_id': unit_version_a_uuid, 'user_id': user_id, 'entity_id': unit_a_uuid, 'name': 'test unit add', 'body': 'adding numbers is fun' }, { 'user_id': user_id, 'entity_id': unit_b_uuid, 'name': 'test unit subtract', 'body': 'subtracting numbers is fun', 'require_ids': [unit_a_uuid], }] raw_insert_units(db_conn, units) cards = [{ 'entity_id': card_a_uuid, 'unit_id': unit_a_uuid, 'kind': 'video', 'name': 'Video Z', }] raw_insert_cards(db_conn, cards) subjects = [{ 'version_id': subject_version_a_uuid, 'entity_id': subject_a_uuid, 'name': 'Math', 'user_id': user_id, 'body': 'Math is fun.', 'members': [{ 'kind': 'unit', 'id': convert_uuid_to_slug(unit_a_uuid), }, { 'kind': 'unit', 'id': convert_uuid_to_slug(unit_b_uuid), }], }, { 'entity_id': subject_b_uuid, 'name': 'An Introduction to Electronic Music', 'user_id': user_id, 'body': 'Art is fun.', 'members': [{ 'kind': 'subject', 'id': convert_uuid_to_slug(subject_a_uuid), }], }] raw_insert_subjects(db_conn, subjects)
def log_in_user(user): """ Log in the given user. """ session_id = convert_uuid_to_slug(uuid.uuid4()) red.setex( session_id, 2 * 7 * 24 * 60 * 60, convert_uuid_to_slug(user['id']), ) return session_id
def test_insert_card(db_conn): create_card_test_data(db_conn) # A Bad kind data = { 'version_id': uuid.uuid4(), 'entity_id': uuid.uuid4(), 'unit_id': unit_a_uuid, 'user_id': user_a_uuid, 'status': 'accepted', 'kind': 'dinosaur', 'name': 'Story of Love Video', 'data': {}, } card, errors = insert_card(db_conn, data) assert errors assert not card # A bad require data = { 'version_id': uuid.uuid4(), 'entity_id': uuid.uuid4(), 'unit_id': unit_a_uuid, 'user_id': user_a_uuid, 'status': 'accepted', 'kind': 'video', 'name': 'Story of Love Video', 'data': { 'site': 'youtube', 'video_id': convert_uuid_to_slug(uuid.uuid4()), }, 'require_ids': [uuid.uuid4()], } card, errors = insert_card(db_conn, data) assert errors assert not card # For real data = { 'version_id': uuid.uuid4(), 'entity_id': uuid.uuid4(), 'unit_id': unit_a_uuid, 'user_id': user_a_uuid, 'status': 'accepted', 'kind': 'video', 'name': 'Story of Love Video', 'data': { 'site': 'youtube', 'video_id': convert_uuid_to_slug(uuid.uuid4()), }, } card, errors = insert_card(db_conn, data) assert not errors assert card
def select_subject_route(request, user_id, subject_id): """ Select the subject to work on. NEXT STATE POST Choose Subject (Update Learner Context) -> GET Choose Subject ...when subject is complete -> GET Choose Unit ...when in learn or review mode -> GET Learn Card ...when in diagnosis (Unit auto chosen) """ db_conn = request['db_conn'] current_user = get_current_user(request) if not current_user: return abort(401, 'f8IynoM9RLmW0Ae14_Hukw') subject = get_latest_accepted_subject(db_conn, subject_id) set_learning_context(current_user, subject=subject) buckets = traverse(db_conn, current_user, subject) # When in diagnosis, choose the unit and card automagically. # if buckets.get('diagnose'): # unit = buckets['diagnose'][0] # card = choose_card(db_conn, current_user, unit) # next_ = { # 'method': 'GET', # 'path': '/s/cards/{card_id}/learn' # .format(card_id=convert_uuid_to_slug(card['entity_id'])), # } # set_learning_context( # current_user, # next=next_, unit=unit, card=card) # When in learn or review mode, lead me to choose a unit. # elif buckets.get('review') or if buckets.get('learn'): next_ = { 'method': 'GET', 'path': '/s/subjects/{subject_id}/units'.format( subject_id=convert_uuid_to_slug(subject_id) ), } set_learning_context(current_user, next=next_) # If the subject is complete, lead the learner to choose another subject. else: next_ = { 'method': 'GET', 'path': '/s/users/{user_id}/subjects'.format( user_id=convert_uuid_to_slug(current_user['id']) ), } set_learning_context(current_user, next=next_, unit=None, subject=None) return 200, {'next': next_}
def create_subject_test_data(db_conn): users = [{ 'id': user_a_uuid, 'name': 'test', 'email': '*****@*****.**', 'password': '******', }, { 'id': user_b_uuid, 'name': 'other', 'email': '*****@*****.**', 'password': '******', }] raw_insert_users(db_conn, users) units = [{ 'user_id': user_a_uuid, 'entity_id': unit_a_uuid, 'name': 'test unit add', 'body': 'adding numbers is fun' }, { 'user_id': user_a_uuid, 'entity_id': unit_b_uuid, 'name': 'test unit subtract', 'body': 'subtracting numbers is fun', 'require_ids': [unit_a_uuid], }] raw_insert_units(db_conn, units) subjects = [{ 'version_id': subject_version_a_uuid, 'entity_id': test_subject_a_uuid, 'name': 'Math', 'user_id': user_b_uuid, 'body': 'Math is fun.', 'members': [{ 'kind': 'unit', 'id': convert_uuid_to_slug(unit_a_uuid), }, { 'kind': 'unit', 'id': convert_uuid_to_slug(unit_b_uuid), }], }, { 'entity_id': test_subject_b_uuid, 'name': 'Art', 'user_id': user_b_uuid, 'body': 'Art is fun.', 'members': [{ 'kind': 'subject', 'id': convert_uuid_to_slug(test_subject_a_uuid), }], }] raw_insert_subjects(db_conn, subjects)
def test_score_card_response(db_conn): create_card_test_data(db_conn) card = get_card_version(db_conn, version_id=card_version_b_uuid) score, feedback = score_card_response( card, response=convert_uuid_to_slug(test_card_option1_uuid)) assert score == 1 score, feedback = score_card_response( card, response=convert_uuid_to_slug(test_card_option2_uuid)) assert score == 0 score, feedback = score_card_response(card, response=convert_uuid_to_slug( uuid.uuid4())) assert score == 0 assert 'Vqjk9WHrR0CSVKQeZZ8svQ' in feedback
def test_respond_card_404(db_conn, session): """ Expect to fail to respond to an unknown card. (404) """ code, _ = routes.card.respond_to_card_route( { 'params': { 'response': convert_uuid_to_slug(uuid.uuid4()) }, 'cookies': { 'session_id': session }, 'db_conn': db_conn, }, convert_uuid_to_slug(uuid.uuid4())) assert code == 404
def raw_insert_responses(db_conn, responses): for response in responses: query = """ INSERT INTO responses (id, created, modified, user_id, card_id, unit_id, response, score, learned) VALUES (%(id)s, %(created)s, %(modified)s, %(user_id)s, %(card_id)s, %(unit_id)s, %(response)s, %(score)s, %(learned)s) RETURNING *; """ params = { 'id': response.get('id', uuid.uuid4()), 'created': response.get('created', datetime.utcnow()), 'modified': response.get('modified', datetime.utcnow()), 'user_id': convert_slug_to_uuid(response.get('user_id')), 'card_id': convert_slug_to_uuid(response.get('card_id')), 'unit_id': convert_slug_to_uuid(response.get('unit_id')), 'response': convert_uuid_to_slug(response.get('response')), 'score': response.get('score'), 'learned': response.get('learned'), } save_row(db_conn, query, params)
def test_insert_card_version(db_conn): create_card_test_data(db_conn) current_data = get_card_version(db_conn, version_id=card_version_a_uuid) # A bad kind current_data['kind'] = 'elephant' next_data = { 'user_id': user_b_uuid, } version, errors = insert_card_version(db_conn, current_data, next_data) assert errors assert not version current_data['kind'] = 'video' # A bad require next_data = { 'user_id': user_b_uuid, 'require_ids': [uuid.uuid4()], } version, errors = insert_card_version(db_conn, current_data, next_data) assert errors assert not version # For real next_data = { 'user_id': user_b_uuid, 'name': 'Story of Apathy Video', 'data': { 'site': 'youtube', 'video_id': convert_uuid_to_slug(uuid.uuid4()), } } version, errors = insert_card_version(db_conn, current_data, next_data) assert not errors assert version
def test_create_post_proposal(db_conn, session): """ Expect create post to create a proposal. """ create_unit_in_db(db_conn) create_topic_in_db(db_conn) request = { 'cookies': { 'session_id': session }, 'params': { 'kind': 'proposal', 'name': 'New Unit', 'body': '''A Modest Proposal for Preventing the Children of Poor People From Being a Burthen to Their Parents or Country, and for Making Them Beneficial to the Publick.''', 'entity_versions': [{ 'kind': 'unit', 'id': convert_uuid_to_slug(unit_version_a_uuid), }] }, 'db_conn': db_conn } code, response = routes.post.create_post_route(request, topic_a_uuid) assert not response.get('errors') assert code == 200 assert response['post']['kind'] == 'proposal'
def next_route(request): """ Tell the learner where to go next. TODO-3 should we move all `next` data from individual endpoints to this one, and have the UI call this endpoint each time to get the next state? """ current_user = get_current_user(request) if not current_user: return abort(401) context = get_learning_context(current_user) # If 'next' action, return that, # else 'next' is GET Choose Subject if context.get('next'): return 200, {'next': context['next']} return 200, { 'next': { 'method': 'GET', 'path': '/s/users/{user_id}/subjects'.format( user_id=convert_uuid_to_slug(current_user['id'])), } }
def test_insert_subject(db_conn): create_subject_test_data(db_conn) data = { 'entity_id': uuid.uuid4(), 'name': 'History', 'user_id': user_b_uuid, 'body': 'History is fun.', 'members': [{ 'kind': 'unit', 'id': convert_uuid_to_slug(uuid.uuid4()), }], } subject, errors = insert_subject(db_conn, data) assert errors assert not subject data = { 'entity_id': uuid.uuid4(), 'name': 'History', 'user_id': user_b_uuid, 'body': 'History is fun.', 'members': [] } subject, errors = insert_subject(db_conn, data) assert not errors assert subject
def test_get_card_404(db_conn): """ Expect to fail to get an unknown card. (404) """ code, _ = routes.card.get_card_route({'db_conn': db_conn}, convert_uuid_to_slug(uuid.uuid4())) assert code == 404
def test_learn_card_401(db_conn): """ Expect to require log in to get a card for learn mode. (401) """ code, _ = routes.card.learn_card_route({'db_conn': db_conn}, convert_uuid_to_slug(uuid.uuid4())) assert code == 401
def create_recurse_test_data(db_conn): create_test_user(db_conn) units = [{ 'user_id': user_a_uuid, 'entity_id': unit_a_uuid, 'name': 'test unit add', 'body': 'adding numbers is fun' }, { 'user_id': user_a_uuid, 'entity_id': unit_b_uuid, 'name': 'test unit subtract', 'body': 'subtracting numbers is fun', }] raw_insert_units(db_conn, units) subjects = [{ 'entity_id': subject_a_uuid, 'name': 'Math', 'user_id': user_a_uuid, 'body': 'Math is fun.', 'members': [{ 'kind': 'unit', 'id': convert_uuid_to_slug(unit_a_uuid), }, { 'kind': 'unit', 'id': convert_uuid_to_slug(unit_b_uuid), }], }, { 'entity_id': subject_b_uuid, 'name': 'Art', 'user_id': user_a_uuid, 'body': 'Art is fun.', 'members': [{ 'kind': 'subject', 'id': convert_uuid_to_slug(subject_a_uuid), }], }] raw_insert_subjects(db_conn, subjects)
def test_validate_entity_versions(db_conn): create_test_posts(db_conn) data = { 'entity_versions': [{ 'id': convert_uuid_to_slug(uuid.uuid4()), 'kind': 'unit', }] } errors = validate_entity_versions(db_conn, data) assert errors data = { 'entity_versions': [{ 'id': convert_uuid_to_slug(test_unit_version_uuid), 'kind': 'unit', }] } errors = validate_entity_versions(db_conn, data) assert not errors
def test_list_users_route(db_conn, session): code, response = routes.user.list_users_route({ 'params': { 'user_ids': convert_uuid_to_slug(user_id), }, 'db_conn': db_conn, }) assert code == 200 assert len(response['users']) == 1
def test_update_entity_status_by_kind(db_conn): create_test_user(db_conn) unit_version_a_uuid = uuid.uuid4() card_version_a_uuid = uuid.uuid4() subject_version_a_uuid = uuid.uuid4() units = [{ 'version_id': unit_version_a_uuid, 'user_id': user_a_uuid, 'entity_id': unit_a_uuid, 'name': 'test unit add', 'body': 'adding numbers is fun', 'status': 'pending', }] raw_insert_units(db_conn, units) cards = [{ 'version_id': card_version_a_uuid, 'status': 'pending', 'entity_id': uuid.uuid4(), 'unit_id': unit_a_uuid, 'user_id': user_a_uuid, 'kind': 'video', 'name': 'Meaning of Life Video', 'data': { 'site': 'youtube', 'video_id': convert_uuid_to_slug(uuid.uuid4()), }, }] raw_insert_cards(db_conn, cards) subjects = [{ 'version_id': subject_version_a_uuid, 'status': 'pending', 'entity_id': subject_a_uuid, 'name': 'Math', 'user_id': user_a_uuid, 'body': 'Math is fun.', 'members': [], }] raw_insert_subjects(db_conn, subjects) unit, errors = update_entity_status_by_kind(db_conn, kind='unit', version_id=unit_version_a_uuid, status='accepted') assert not errors assert unit['status'] == 'accepted' card, errors = update_entity_status_by_kind(db_conn, kind='card', version_id=card_version_a_uuid, status='accepted') assert not errors assert card['status'] == 'accepted' subject, errors = update_entity_status_by_kind( db_conn, kind='subject', version_id=subject_version_a_uuid, status='accepted') assert not errors assert subject['status'] == 'accepted'
def test_learn_card_404(db_conn, session): """ Expect to fail to get an unknown card for learn mode. (404) """ request = {'cookies': {'session_id': session}, 'db_conn': db_conn} code, _ = routes.card.learn_card_route(request, convert_uuid_to_slug(uuid.uuid4())) assert code == 404
def test_respond_card_401(db_conn): """ Expect to require log in to get an unknown card. (401) """ code, _ = routes.card.respond_to_card_route({'db_conn': db_conn}, convert_uuid_to_slug( uuid.uuid4())) assert code == 401
def test_list_subjects_route(db_conn, session): create_route_subject_test_data(db_conn) request = { 'db_conn': db_conn, 'cookies': { 'session_id': session, }, 'params': { 'entity_ids': ','.join([ convert_uuid_to_slug(subject_a_uuid), convert_uuid_to_slug(subject_b_uuid), ]) } } code, response = list_subjects_route(request) assert not response.get('errors') assert code == 200 assert response['subjects']
def test_log_in_user(db_conn): """ Expect to log in as a user. """ create_user_in_db(db_conn) user = get_user(db_conn, {'id': user_id}) token = log_in_user(user) assert token assert red.get(token).decode() == convert_uuid_to_slug(user_id)