Skip to content
This repository was archived by the owner on Feb 7, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# static files generated from Django application using `collectstatic`
media
static

41 changes: 41 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -1 +1,42 @@
# Report

Time spent: about 2h30

## Level 1

The class `ActivitiesManager` is used as the central point for manipulating activities. It loads them from the json then exposes methods to find one or many and the main `get_most_relevant_activity` method.

I used dedicated model classes to make it easier to manipulate the different objects. Using dicts makes it hard for people to know what they are manipulating.

## Level 2

Just added some conditions to the existing methods while trying to avoid re-iterating over the activities.

## Level 3

Added filter on microphone attribute when getting the list of activities. This way, every other call depending on this is affected by the rule.

## Level 4

Understood it this way: « if the activity that is going to be suggested is a new skill we should suggest a discovery instead, then we suggest an activity in this skill »

I'm not sure my code respects the first part of the sentence because I ran out of time.

## Conclusion

### It took time to get started...

It took me some time to get the project started with the proper models and deserialization of the json document because I am not using Python daily and I had to search the web to find how to do most of it.

### Then I encountered some issues

I did not encounter any major issue, but I can still mention the following:
- I'm still not sure how the activities relate to one another, the link between them is not clear and it was hard to get a good idea just by browsing the file
- Linked to the above, I was not sure of what "the next issue" meant. Was it dependant on the id, or on the order in the activities file. I went with the order in the file.
- Trying to avoid unnessary complexity took me some time and led to some refactoring along the

### Improvement ideas

- Using a proper database would be a great way to simplify this code. It would make it way easier to find activities matching a set of criteria
- Adding some proper tests using a unit testing framework. It was difficult in the time frame of the exercise, espacially given that the expectations changed between the exercises, but it's always a good idea to test this kind of logic.
- If not using a database, maybe building several hashmaps when loading the data could have simplified the code (by id, by skill, by level, ...).
Empty file added python/lalilo/__init__.py
Empty file.
39 changes: 39 additions & 0 deletions python/lalilo/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import enum


class Language(enum.Enum):
FR = 1
EN = 2


class Activity:

def __init__(self, id: int, language: Language, exercise_type: str, skill: str, level: int):
self.id: int = id
self.language: Language = language
self.exercise_type: str = exercise_type
self.skill: str = skill
self.level: int = level

@classmethod
def from_json(cls, data):
return cls(**data)


class Student:

def __init__(self, language: Language, microphone: bool = False):
self.language: Language = language
self.traces: List[StudentTrace] = []
self.microphone = microphone


class StudentTrace:
"""
A list of traces corresponding to the activities the student previously finished.
Traces are chronologically ordered (latest to oldest).
"""

def __init__(self, activity_id: int, score: float):
self.activity_id: int = activity_id
self.score: float = score
123 changes: 123 additions & 0 deletions python/lalilo/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import json
from collections import defaultdict
from typing import List, Dict
from lalilo.model import *


class ActivitiesManager():
"""
Loads activities and offers method to manipulate them
"""
DISCOVERY_TYPES = ("discovery_generativity",
"discovery_grapheme_to_phoneme", "discovery_sight_word")

def __init__(self, activities_file: str):
with open(activities_file, "r") as fp:
self.activities: List[Activity] = json.load(
fp, object_hook=Activity.from_json)
# Group activities by language
self.activities_by_language: Dict[Language,
List[Activity]] = defaultdict(list)
for a in self.activities:
self.activities_by_language[a.language].append(a)
print(f"Loaded {len(self.activities)} activities")
print(
f"{len(self.activities_by_language[Language.EN.name])} EN activities")
print(
f"{len(self.activities_by_language[Language.FR.name])} FR activities")

def get_most_relevant_activity(self, student: Student) -> Activity:
if student.traces == None or len(student.traces) == 0:
return self.activities_by_language[student.language.name][0]

last_activity: Activity = self.get_activity(
student.traces[0].activity_id, student.language)
next_activity = None
# If last activity was a discovery, we give the student an activity on the same skill
if last_activity.exercise_type in ActivitiesManager.DISCOVERY_TYPES:
next_activity = self.get_activity_with_skill(
last_activity, student.language, student.microphone)

# If perfect score on last activity
if next_activity is None and student.traces[0].score == 1.0:
next_activity = self.get_next_activity(
last_activity, student.language, student.microphone)
elif student.traces[0].score != 1.0:
next_activity = self.get_lower_level_activity(
last_activity, student.language, student.microphone)

if next_activity is not None:
# Check if the proposed activity is a new skill
# If so, propose a discovery instead
if self.student_has_skill(student, next_activity.skill):
return next_activity
discovery = self.get_discovery(
next_activity.skill, student.language)
if discovery is not None:
return discovery
return next_activity
return last_activity

def get_next_activity(self, activity: Activity, language: Language = None, microphone: bool = True) -> Activity:
activities = self.get_activities(microphone, language)
# Find index of activity in list
for i in range(len(activities)):
a = activities[i]
if a.id == activity.id:
activity_idx = i
break
# Find next activity of different type
for i in range(activity_idx + 1, len(activities)):
a = activities[i]
if a.exercise_type != activity.exercise_type:
return a
# If no different type found, just return next
if activity_idx is not None and activity_idx < len(activities) - 1:
return activities[activity_idx+1]
return None

def get_activity_with_skill(self, activity: Activity, language: Language = None, microphone: bool = True) -> Activity:
for a in self.get_activities(microphone, language):
if a.exercise_type != activity.exercise_type and a.skill == activity.skill:
return a
return None

def get_discovery(self, skill: str, language: Language) -> Activity:
activities = self.get_activities(language)
for a in activities:
if a.skill == skill and a.exercise_type not in ActivitiesManager.DISCOVERY_TYPES:
return a
return None

def get_lower_level_activity(self, activity: Activity, language: Language = None, microphone: bool = True) -> Activity:
first_match = None
for a in self.get_activities(microphone, language):
if a.level < activity.level:
if first_match is None:
first_match = a
if a.exercise_type != activity.exercise_type:
return a
# If no exercise matched, we return the first match (without type condition)
if first_match is not None:
return first_match
return None

def get_activity(self, activity_id: str, language: Language = None) -> Activity:
activities = self.get_activities(language)
for a in activities:
if a.id == activity_id:
return a
return None

def get_activities(self, microphone: bool = True, language: Language = None) -> List[Activity]:
activities = self.activities_by_language[language.name] if language is not None else self.activities
if microphone:
return activities
return [a for a in activities if a.exercise_type != "reading"]

def student_has_skill(self, student: Student, skill: str) -> bool:
for t in student.traces:
activity = self.get_activity(t.activity_id, student.language)
if activity.skill == skill:
return True
return False
Loading