diff --git a/.gitignore b/.gitignore index d9afb0c99d..e68a4576cf 100644 --- a/.gitignore +++ b/.gitignore @@ -24,9 +24,6 @@ dist-packages-temp/ dist-packages-downloads/ dist-packages/ -# Generated by kalite manage setup -secretkey.txt - # User cache data generated at runtime cache/ @@ -48,13 +45,21 @@ static-updates # Python compiling *.pyc *.pyo + +# Backup files *~ + +# Sublime editor *.sublime-* + +# Ignore all .zip files!? No comment?? Needs fixing, I guess it's to avoid +# committing local assessment items. *.zip # Documentation docs/_build docs/images/* +docs/kalite.1.gz # oh-my-zsh convention for automatically # switching on a venv diff --git a/MANIFEST.in.dist b/MANIFEST.in.dist index c87008e666..811c9b900c 100644 --- a/MANIFEST.in.dist +++ b/MANIFEST.in.dist @@ -27,3 +27,6 @@ recursive-include data * # Necessary because it's a data directory so they # do not get automatically excluded recursive-exclude python-packages *pyc + +# Docs +recursive-include docs/_build/html * diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..0b020362df --- /dev/null +++ b/Makefile @@ -0,0 +1,97 @@ +.PHONY: clean-pyc clean-build docs clean + +help: + @echo "clean - remove all build, test, coverage and Python artifacts" + @echo "clean-build - remove build artifacts" + @echo "clean-pyc - remove Python file artifacts" + @echo "clean-test - remove test and coverage artifacts" + @echo "lint - check style with pep8" + @echo "test - run tests the default Python" + @echo "test-bdd - run BDD tests only" + @echo "test-nobdd - run non-BDD tests only" + @echo "assets - build all JS/CSS assets" + @echo "coverage - check code coverage quickly with the default Python" + @echo "docs - generate Sphinx HTML documentation, including API docs" + @echo "release - package and upload a release" + @echo "dist - package locally" + @echo "install - install the package to the active Python's site-packages" + +clean: clean-build clean-pyc clean-test + +clean-build: + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + rm -fr dist-packages/ + rm -fr dist-packages-temp/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + +lint: + pep8 kalite + jshint kalite/*/static/js/*/ + +test: + bin/kalite manage test --bdd-only + +test-bdd: + bin/kalite manage test --bdd-only + +test-nobdd: + bin/kalite manage test --no-bdd + +test-all: + @echo "Not supported yet" + # tox + +coverage: + coverage run --source kalite kalitectl.py test + coverage report -m + +coverage-bdd: + coverage run --source kalite kalitectl.py test --bdd-only + coverage report -m + +coverage-nobdd: + coverage run --source kalite kalitectl.py test --no-bdd + coverage report -m + +docs: + # rm -f docs/ka-lite.rst + # rm -f docs/modules.rst + # sphinx-apidoc -o docs/ ka-lite-gtk + $(MAKE) -C docs clean + $(MAKE) -C docs html + cli2man bin/kalite -o docs/kalite.1.gz + # open docs/_build/html/index.html + +assets: + # Necessary because NPM may have wrong versions in the cache + npm cache clean + npm install --production + node build.js + bin/kalite manage compileymltojson + +release: clean docs assets + python setup.py sdist --formats=gztar,zip upload --sign + python setup.py sdist --formats=gztar,zip upload --sign --static + ls -l dist + +dist: clean docs assets + python setup.py sdist + python setup.py sdist --static + ls -l dist + +install: clean + python setup.py install diff --git a/circle.yml b/circle.yml index 09bdbebfa5..aeb73234ec 100644 --- a/circle.yml +++ b/circle.yml @@ -9,22 +9,25 @@ dependencies: override: - pip install -r requirements_test.txt - pip install -e . - - npm install - - npm install -g jshint + # This cannot be done because pip on Circle doesn't understand our sdist + # - make sdist + # - pip install dist/ka-lite-"$(python setup.py --version)".tar.gz post: - if [[ ! -e sc-latest-linux/bin/sc ]]; then wget https://saucelabs.com/downloads/sc-latest-linux.tar.gz && tar -xzf sc-latest-linux.tar.gz && mv sc-*-linux sc-latest-linux; fi test: override: - - node build.js + - make assets - kalite start --settings=kalite.project.settings.disk_based_cache --traceback -v2 - kalite status - kalite stop --traceback -v2 - cd sc-*-linux && ./bin/sc -u $SAUCE_USERNAME -k $SAUCE_ACCESS_KEY --tunnel-identifier $CIRCLE_BUILD_NUM-$CIRCLE_NODE_INDEX --readyfile ~/sauce_is_ready > sc_output.txt 2>&1: background: true - while [ ! -e ~/sauce_is_ready ]; do sleep 1; done - - case $CIRCLE_NODE_INDEX in 0) kalite manage test --bdd-only ;; 1) kalite manage test --no-bdd;; esac: + - case $CIRCLE_NODE_INDEX in 0) make test-bdd ;; 1) make test-nobdd;; esac: parallel: true + # TODO: replace below with "make link" when we're pep8 + - npm install -g jshint - jshint kalite/*/static/js/*/ post: - killall --wait sc # wait for Sauce Connect to close the tunnel diff --git a/data/version.yml b/data/version.yml index ea94f51324..d29098d7f5 100644 --- a/data/version.yml +++ b/data/version.yml @@ -18,7 +18,6 @@ students: coaches: [] admins: [] - bugs_fixed: all: Properly compare language pack versions (#4587). diff --git a/docs/installguide/install_all.rst b/docs/installguide/install_all.rst index e14e6e697f..77b49dfe92 100644 --- a/docs/installguide/install_all.rst +++ b/docs/installguide/install_all.rst @@ -7,37 +7,50 @@ Windows Upgrading KA Lite over an existing installation is easy -- just run the installer and follow the prompts! You don't need to uninstall your old KA Lite installation first. -Mac/OSX -======= +Mac OS X +======== + +.. warning:: The latest OS X version (EL Capitan) is not yet supported. -**Note:** The latest OSX version (EL Capitan) is not yet supported. +Installation +____________ #. Download the KA Lite `OSX installer `_. #. After the download is complete, double click the .dmg file. +#. Click the ``Agree`` button to accept the LICENSE agreement. #. On the .dmg window, drag the ``KA-Lite Monitor`` app into the ``Applications`` folder. - - .. note:: - To upgrade an existing KA Lite installation, download the new installer and then drag it over to the "Applications" - folder to replace the existing "KA-Lite-Monitor" application. - Give confirmation to overwrite the existing app. Then proceed from this step as usual. #. Launch ``KA-Lite Monitor`` from your ``Applications`` folder. #. On first load, it will check your current environment and show the Preferences dialog. -#. Input your preferred admin username and password, then click the Apply button in ``KA-Lite Preferences`` dialog. +#. Input your preferred administrator username and password, then click the ``Apply`` button in ``KA-Lite Preferences`` dialog. #. You will be prompted that initial setup will take a few minutes, click the ``OK`` button and wait for the notification that KA-Lite has been setup and can now be started. -#. Click on the KA-Lite logo icon on the Status Menu Bar and select the ``Start KA-Lite`` menu option. +#. Click on the KA-Lite logo icon on the menu bar and select the ``Start KA-Lite`` menu option. #. Wait for the notification that you can now click on ``Open in Browser`` menu option. -#. Click on the KA-Lite logo icon on the Status Menu Bar and select ``Open in Browser`` menu option - this should launch KA-Lite in your preferred web browser. +#. Click on the KA-Lite logo icon on the menu bar and select ``Open in Browser`` menu option - this should launch KA-Lite in your preferred web browser. #. Login using the administrator account you have specified during setup. -If the sidebar shows entries that are greyed-out, the child items of the entry may be videos that were not yet downloaded. If there are assessment items inside, then you need to extract the `assessment.zip` manually: +Upgrade +_______ + +To upgrade an existing KA Lite installation. +#. Download the KA Lite `OSX installer `_. +#. After the download is complete, double click the .dmg file. +#. Click the ``Agree`` button to accept the LICENSE agreement. +#. On the .dmg window, drag the ``KA-Lite Monitor`` app into the ``Applications`` folder. +#. Give confirmation to overwrite the existing app. #. Launch ``KA-Lite Monitor`` from your ``Applications`` folder. -#. Click on the app icon at the menu bar. -#. Click on ``Preferences`` in the menu option. -#. Go to Advanced tab, click on the ``Extract Assessment`` button, then confirm the action. -#. Restart the server, login to the web app, then check the sidebar contents. -This will enable the greyed-out assessment items on the sidebar. +.. tip:: + If the sidebar shows entries that are greyed-out, the child items of the entry may be videos that were not yet downloaded. If there are assessment items inside, then you need to extract the `assessment.zip` manually: + + #. Launch ``KA-Lite Monitor`` from your ``Applications`` folder. + #. Click on the app icon at the menu bar. + #. Click on ``Preferences`` in the menu option. + #. Go to ``Advanced`` tab, click on the ``Extract Assessment`` button, then confirm the action. + #. Restart the server, login to the web app, then check the sidebar contents. + + This will enable the greyed-out assessment items on the sidebar. + Linux ===== @@ -93,7 +106,7 @@ It can be installed by downloading the latest .deb on the Pi and installing it:: # Install dependencies sudo apt-get install python-m2crypto python-pkg-resources nginx python-psutil # Fetch the latest .deb - sudo wget https://learningequality.org/r/deb-pi-installer-0-14 --no-check-certificate + sudo wget https://learningequality.org/r/deb-pi-installer-0-15 --no-check-certificate # Install the .deb sudo dpkg -i ka-lite-raspberry-pi*.deb diff --git a/docs/installguide/release_notes.rst b/docs/installguide/release_notes.rst index 4733affc84..d66cb6c359 100644 --- a/docs/installguide/release_notes.rst +++ b/docs/installguide/release_notes.rst @@ -10,6 +10,7 @@ General Python 2.6 is no longer supported. It *may* still work, but we are no longer actively supporting it. Other known issues: +* The latest OSX version (EL Capitan) is not yet supported. * On OSX, you must restart the server after downloading videos in order for them to be marked as available. * On all platforms, you must restart the server after downloading a language pack in order to use it. * You can no longer configure your server using ``local_settings.py``. Instead, custom settings must appear in diff --git a/docs/installguide/system_requirements.rst b/docs/installguide/system_requirements.rst index 7e259cf23b..ed3b2b6b75 100644 --- a/docs/installguide/system_requirements.rst +++ b/docs/installguide/system_requirements.rst @@ -1,6 +1,9 @@ System requirements =================== +.. note:: + The latest OSX version (EL Capitan) is not yet supported. + Supported Browsers ------------------ KA Lite is currently *not* supported on Internet Explorer version 8 or lower. You must use IE9 or later. diff --git a/kalite/coachreports/api_resources.py b/kalite/coachreports/api_resources.py index 0afb82b08c..5847c1f43c 100644 --- a/kalite/coachreports/api_resources.py +++ b/kalite/coachreports/api_resources.py @@ -9,7 +9,7 @@ class CoachReportBaseResource(Resource): """ - A base resource that houses shared code between the resources we actually use + A base resource that houses shared code between the resources we actually use in the API """ @@ -54,7 +54,7 @@ class Meta: def get_object_list(self, request): user_id = request.GET.get('user_id') - result = PlaylistProgress.user_progress(user_id=user_id) + result = PlaylistProgress.user_progress(user_id=user_id, language=request.language) return result class PlaylistProgressDetailResource(CoachReportBaseResource): @@ -72,8 +72,8 @@ class Meta: def get_object_list(self, request): user_id = request.GET.get("user_id") playlist_id = request.GET.get("playlist_id") - result = PlaylistProgressDetail.user_progress_detail(user_id=user_id, playlist_id=playlist_id) + language = request.language + result = PlaylistProgressDetail.user_progress_detail(user_id=user_id, playlist_id=playlist_id, language=language) if not result: - raise NotFound("User playlist progress details with user ID '%s' and playlist ID '%s' were not found." % (user_id, playlist_id)) + raise NotFound("User playlist progress details with user ID '%s' and playlist ID '%s' were not found." % (user_id, playlist_id)) return result - diff --git a/kalite/coachreports/api_views.py b/kalite/coachreports/api_views.py index c15b9cea62..527ad8536c 100644 --- a/kalite/coachreports/api_views.py +++ b/kalite/coachreports/api_views.py @@ -61,6 +61,7 @@ def return_log_type_details(log_type, topic_ids=None): @require_admin def learner_logs(request): + lang = request.language page = request.GET.get("page", 1) limit = request.GET.get("limit", 50) @@ -100,7 +101,7 @@ def learner_logs(request): topic_objects = log_objects.filter(latest_activity_timestamp__gte=start_date, latest_activity_timestamp__lte=end_date) if topic_objects.count() == 0: topic_objects = log_objects - objects = dict([(obj[id_field], get_content_cache().get(obj[id_field], get_exercise_cache().get(obj[id_field]))) for obj in topic_objects]).values() + objects = dict([(obj[id_field], get_content_cache(language=lang).get(obj[id_field], get_exercise_cache(language=lang).get(obj[id_field]))) for obj in topic_objects]).values() output_objects.extend(objects) output_logs.extend(log_objects) @@ -122,6 +123,7 @@ def learner_logs(request): @require_admin def aggregate_learner_logs(request): + lang = request.language learners = get_learners_from_GET(request) event_limit = request.GET.get("event_limit", 10) @@ -144,7 +146,7 @@ def aggregate_learner_logs(request): "exercise_attempts": 0, "exercise_mastery": None, } - + end_date = datetime.datetime.strptime(end_date,'%Y/%m/%d') if end_date else datetime.datetime.now() start_date = datetime.datetime.strptime(start_date,'%Y/%m/%d') if start_date else end_date - datetime.timedelta(time_window) @@ -179,7 +181,7 @@ def aggregate_learner_logs(request): "complete": log.complete, "struggling": getattr(log, "struggling", None), "progress": getattr(log, "streak_progress", getattr(log, "progress", None)), - "content": get_exercise_cache().get(getattr(log, "exercise_id", ""), get_content_cache().get(getattr(log, "video_id", getattr(log, "content_id", "")), {})), + "content": get_exercise_cache(language=lang).get(getattr(log, "exercise_id", "")) or get_content_cache(language=lang).get(getattr(log, "video_id", None) or getattr(log, "content_id", "")) or {} } for log in output_logs[:event_limit]] output_dict["total_time_logged"] = round((UserLogSummary.objects\ .filter(user__in=learners, start_datetime__gte=start_date, start_datetime__lte=end_date)\ diff --git a/kalite/coachreports/models.py b/kalite/coachreports/models.py index 7aa86d8897..70d40a227d 100644 --- a/kalite/coachreports/models.py +++ b/kalite/coachreports/models.py @@ -1,8 +1,11 @@ """Classes used by the student progress tastypie API""" import json +from fle_utils.config.models import Settings +from django.conf import settings from django.core.urlresolvers import reverse, NoReverseMatch from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import ugettext as _ from kalite.facility.models import FacilityUser from kalite.main.models import ExerciseLog, VideoLog @@ -59,12 +62,16 @@ def __init__(self, **kwargs): setattr(self, k, v) @classmethod - def user_progress(cls, user_id): + def user_progress(cls, user_id, language=None): """ Return a list of PlaylistProgress objects associated with the user. """ + + if not language: + language = Settings.get("default_language") or settings.LANGUAGE_CODE + user = FacilityUser.objects.get(id=user_id) - all_playlists = get_leafed_topics() + all_playlists = get_leafed_topics(language=language) # Retrieve video, exercise, and quiz logs that appear in this playlist user_vid_logs, user_ex_logs = cls.get_user_logs(user) @@ -110,9 +117,9 @@ def user_progress(cls, user_id): n_ex_started = len([ex for ex in pl_ex_logs if ex["attempts"] > 0]) n_ex_incomplete = len([ex for ex in pl_ex_logs if (ex["attempts"] > 0 and not ex["complete"])]) n_ex_struggling = len([ex for ex in pl_ex_logs if ex["struggling"]]) - ex_pct_mastered = int(float(n_ex_mastered) / n_pl_exercises * 100) - ex_pct_incomplete = int(float(n_ex_incomplete) / n_pl_exercises * 100) - ex_pct_struggling = int(float(n_ex_struggling) / n_pl_exercises * 100) + ex_pct_mastered = int(float(n_ex_mastered) / (n_pl_exercises or 1) * 100) + ex_pct_incomplete = int(float(n_ex_incomplete) / (n_pl_exercises or 1) * 100) + ex_pct_struggling = int(float(n_ex_struggling) / (n_pl_exercises or 1) * 100) if not n_ex_started: ex_status = "notstarted" elif ex_pct_struggling > 0: @@ -177,34 +184,14 @@ def __init__(self, **kwargs): self.path = kwargs.get("path") @classmethod - def create_empty_entry(cls, entity_id, kind, playlist): - if kind != "Quiz": - if kind == "Video": - topic_node = get_content_cache().get(entity_id) - elif kind == "Exercise": - topic_node = get_exercise_cache().get(entity_id) - title = topic_node["title"] - path = topic_node["path"] - else: - title = playlist["title"] - path = "" - entry = { - "id": entity_id, - "kind": kind, - "status": "notstarted", - "score": 0, - "title": title, - "path": path, - } - - return entry - - @classmethod - def user_progress_detail(cls, user_id, playlist_id): + def user_progress_detail(cls, user_id, playlist_id, language=None): """ Return a list of video, exercise, and quiz log PlaylistProgressDetail objects associated with a specific user and playlist ID. """ + if not language: + language = Settings.get("default_language") or settings.LANGUAGE_CODE + user = FacilityUser.objects.get(id=user_id) playlist = next((pl for pl in get_leafed_topics() if pl.get("id") == playlist_id), None) @@ -221,9 +208,12 @@ def user_progress_detail(cls, user_id, playlist_id): progress_details = list() for entity_id in playlist.get("children"): entry = {} - leaf_node = get_content_cache().get(entity_id, get_exercise_cache().get(entity_id, {})) + leaf_node = get_content_cache(language=language).get(entity_id) or get_exercise_cache(language=language).get(entity_id) or {} kind = leaf_node.get("kind") + status = "notstarted" + score = 0 + if kind == "Video": vid_log = next((vid_log for vid_log in user_vid_logs if vid_log["video_id"] == entity_id), None) if vid_log: @@ -231,17 +221,8 @@ def user_progress_detail(cls, user_id, playlist_id): status = "complete" elif vid_log.get("total_seconds_watched"): status = "inprogress" - else: - status = "notstarted" - - entry = { - "id": entity_id, - "kind": kind, - "status": status, - "score": int(float(vid_log.get("points")) / float(750) * 100), - "title": leaf_node["title"], - "path": leaf_node["path"], - } + + score = int(float(vid_log.get("points")) / float(750) * 100) elif kind == "Exercise": ex_log = next((ex_log for ex_log in user_ex_logs if ex_log["exercise_id"] == entity_id), None) @@ -253,47 +234,16 @@ def user_progress_detail(cls, user_id, playlist_id): elif ex_log.get("attempts"): status = "inprogress" - entry = { - "id": entity_id, - "kind": kind, - "status": status, - "score": ex_log.get("streak_progress"), - "title": leaf_node["title"], - "path": leaf_node["path"], - } + score = ex_log.get('streak_progress') - # Oh Quizzes, we hardly knew ye! - # TODO (rtibbles): Sort out the status of Quizzes, and either reinstate them or remove them. - # Quizzes were introduced to provide a way of practicing multiple types of exercise at once - # However, there is currently no way to access them, and the manner for generating them (from the now deprecated Playlist models) is inaccessible - # elif kind == "Quiz": - # entity_id = playlist["id"] - # if quiz_log: - # if quiz_log.complete: - # if quiz_pct_score <= 59: - # status = "fail" - # elif quiz_pct_score <= 79: - # status = "borderline" - # else: - # status = "pass" - # elif quiz_log.attempts: - # status = "inprogress" - # else: - # status = "notstarted" - - # quiz_log_id = quiz_log.quiz - - # entry = { - # "id": quiz_log_id, - # "kind": "Quiz", - # "status": status, - # "score": quiz_pct_score, - # "title": playlist.get("title"), - # "path": "", - # } - - if not entry: - entry = cls.create_empty_entry(entity_id, kind, playlist) + entry = { + "id": entity_id, + "kind": kind, + "status": status, + "score": score, + "title": leaf_node["title"], + "path": leaf_node["path"], + } progress_details.append(cls(**entry)) diff --git a/kalite/coachreports/static/js/coachreports/student_progress/hbtemplates/playlist-progress-container.handlebars b/kalite/coachreports/static/js/coachreports/student_progress/hbtemplates/playlist-progress-container.handlebars index ecc39909c6..4c1f1010b6 100644 --- a/kalite/coachreports/static/js/coachreports/student_progress/hbtemplates/playlist-progress-container.handlebars +++ b/kalite/coachreports/static/js/coachreports/student_progress/hbtemplates/playlist-progress-container.handlebars @@ -24,13 +24,13 @@ {{/unless}} {{/if}} {{#if vid_pct_complete}} -
+
{{_ "Of those you have completed " }} {{vid_pct_complete}}%
{{/if}} {{#if vid_pct_started}} -
+
{{#ifcond vid_pct_started "<" 100}} {{_ "You are still working on " }} {{vid_pct_started}}% diff --git a/kalite/coachreports/static/js/coachreports/student_progress/hbtemplates/playlist-progress-details.handlebars b/kalite/coachreports/static/js/coachreports/student_progress/hbtemplates/playlist-progress-details.handlebars index d6f99a0919..b3390af245 100644 --- a/kalite/coachreports/static/js/coachreports/student_progress/hbtemplates/playlist-progress-details.handlebars +++ b/kalite/coachreports/static/js/coachreports/student_progress/hbtemplates/playlist-progress-details.handlebars @@ -15,10 +15,10 @@ {{#ifcond attributes.status "===" "struggling"}} {{_ " Struggling. " }} {{/ifcond}} - + {{#ifcond attributes.status "===" "notstarted"}} {{_ " Not started. " }} - {{/ifcond}} + {{/ifcond}}
{{!-- Score info for sr-user. --}} @@ -41,26 +41,26 @@ {{!-- Whole section is rendered invisible to screen readers --}}