-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use manifest.json files to download collections #436
Changes from all commits
ed1fdb1
523780c
63cdfa6
5f80a8f
53cf7b2
195f340
4068f9b
5cadfc7
c29aeae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,12 +16,12 @@ | |
from django.views.generic.base import TemplateView | ||
from django.views.generic.base import View | ||
from kolibri.core.content.api import cache_forever | ||
from kolibri.core.content.api import RemoteChannelViewSet | ||
from kolibri.core.content.zip_wsgi import add_security_headers | ||
from kolibri.core.content.zip_wsgi import get_embedded_file | ||
from kolibri.core.decorators import cache_no_user_data | ||
from kolibri.core.tasks.api import _job_to_response | ||
from kolibri.core.tasks.api import _remoteimport | ||
from kolibri.core.tasks.exceptions import JobNotFound | ||
from kolibri.core.tasks.job import State | ||
from kolibri.core.tasks.main import queue | ||
from kolibri.utils import conf | ||
|
@@ -35,6 +35,13 @@ | |
APPS_BUNDLE_PATHS.append(os.path.join(os.path.dirname(__file__), "apps")) | ||
|
||
|
||
COLLECTION_PATHS = os.path.join( | ||
os.path.dirname(__file__), "static", "collections" | ||
) | ||
if conf.OPTIONS["Explore"]["CONTENT_COLLECTIONS_PATH"]: | ||
COLLECTION_PATHS = conf.OPTIONS["Explore"]["CONTENT_COLLECTIONS_PATH"] | ||
|
||
|
||
@method_decorator(cache_no_user_data, name="dispatch") | ||
class ExploreView(TemplateView): | ||
template_name = "explore/explore.html" | ||
|
@@ -112,46 +119,84 @@ def get(self, request, app): | |
|
||
@method_decorator(csrf_exempt, name="dispatch") | ||
class EndlessLearningCollection(View): | ||
COLLECTIONS = { | ||
"small": { | ||
"title": "3 GB", | ||
"subtitle": "Small", | ||
"channels": 10, | ||
"size": 3, | ||
"text": "Primary", | ||
"token": "kopip-lakip", | ||
"available": True, | ||
grade_collections = { | ||
"primary": { | ||
"small": { | ||
"title": "2 GB", | ||
"subtitle": "Small", | ||
"channels": 13, | ||
"size": 2, | ||
"text": "Primary", | ||
"token": "kopip-lakip", | ||
"available": True, | ||
}, | ||
"large": { | ||
"title": "5 GB", | ||
"subtitle": "Large", | ||
"channels": 14, | ||
"size": 5, | ||
"text": "Primary", | ||
"token": "vofog-gufap", | ||
"available": True, | ||
}, | ||
}, | ||
"medium": { | ||
"title": "6 GB", | ||
"subtitle": "Medium", | ||
"channels": 10, | ||
"size": 6, | ||
"text": "Intermediate", | ||
"token": "zubit-vusus", | ||
"available": True, | ||
"intermediate": { | ||
"small": { | ||
"title": "3 GB", | ||
"subtitle": "Small", | ||
"channels": 22, | ||
"size": 3, | ||
"text": "Intermediate", | ||
"token": "kopip-lakip", | ||
"available": True, | ||
}, | ||
"large": { | ||
"title": "6 GB", | ||
"subtitle": "Large", | ||
"channels": 22, | ||
"size": 6, | ||
"text": "Intermediate", | ||
"token": "vofog-gufap", | ||
"available": True, | ||
}, | ||
}, | ||
"large": { | ||
"title": "12 GB", | ||
"subtitle": "Large", | ||
"channels": 10, | ||
"size": 12, | ||
"text": "Secondary", | ||
"token": "vofog-gufap", | ||
"available": True, | ||
"secondary": { | ||
"small": { | ||
"title": "3 GB", | ||
"subtitle": "Small", | ||
"channels": 23, | ||
"size": 3, | ||
"text": "Secondary", | ||
"token": "kopip-lakip", | ||
"available": True, | ||
}, | ||
"large": { | ||
"title": "6 GB", | ||
"subtitle": "Large", | ||
"channels": 24, | ||
"size": 6, | ||
"text": "Secondary", | ||
"token": "vofog-gufap", | ||
"available": True, | ||
}, | ||
}, | ||
} | ||
|
||
BASE_URL = "https://kolibri-content.endlessos.org/" | ||
|
||
def check_collection_availability(self): | ||
free_space_gb = get_free_space() / 1024**3 | ||
for _k, v in self.COLLECTIONS.items(): | ||
v["available"] = v["size"] < free_space_gb | ||
for collections in self.grade_collections.values(): | ||
for v in collections.values(): | ||
v["available"] = v["size"] < free_space_gb | ||
|
||
def get(self, request): | ||
job_ids = request.session.get("job_ids", []) | ||
jobs = [queue.fetch_job(job) for job in job_ids] | ||
try: | ||
jobs = [queue.fetch_job(job) for job in job_ids] | ||
except JobNotFound: | ||
request.session["job_ids"] = [] | ||
jobs = [] | ||
running = [job for job in jobs if job.state == State.RUNNING] | ||
pid, _, _ = get_status() | ||
|
||
|
@@ -184,7 +229,7 @@ def get(self, request): | |
collection = request.session.get("downloading") | ||
self.check_collection_availability() | ||
jobs_response = { | ||
"collections": self.COLLECTIONS, | ||
"collections": self.grade_collections, | ||
"collection": collection, | ||
"jobs": [_job_to_response(job) for job in jobs], | ||
} | ||
|
@@ -194,25 +239,30 @@ def get(self, request): | |
) | ||
|
||
def post(self, request): | ||
grade = "primary" | ||
collection = "small" | ||
if request.body: | ||
data = json.loads(request.body) | ||
collection = data.get("collection", "small") | ||
grade = data.get("grade", "primary") | ||
|
||
token = self.COLLECTIONS[collection]["token"] | ||
|
||
channel_viewset = RemoteChannelViewSet() | ||
channels = channel_viewset._make_channel_endpoint_request( | ||
identifier=token | ||
collection_manifest = os.path.join( | ||
COLLECTION_PATHS, f"{grade}-{collection}.json" | ||
) | ||
|
||
job_ids = [] | ||
if not os.path.exists(collection_manifest): | ||
raise Http404("Collection manifest not found") | ||
|
||
manifest = {} | ||
with open(collection_manifest) as f: | ||
manifest = json.load(f) | ||
channels = manifest.get("channels", []) | ||
|
||
job_ids = [] | ||
pid, _, _ = get_status() | ||
for channel in channels: | ||
task = { | ||
"channel_id": channel["id"], | ||
"channel_name": channel["name"], | ||
"baseurl": self.BASE_URL, | ||
"started_by_username": "endless", | ||
"type": "REMOTEIMPORT", | ||
|
@@ -223,6 +273,10 @@ def post(self, request): | |
_remoteimport, | ||
task["channel_id"], | ||
task["baseurl"], | ||
# Done this way to convert [] to None | ||
node_ids=channel.get("include_node_ids") or None, | ||
# Done this way to convert [] to None | ||
exclude_node_ids=channel.get("exclude_node_ids") or None, | ||
Comment on lines
+276
to
+279
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 there's a subtle difference in how It's worth noting this behaviour will be different with learningequality/kolibri#9460. Setting Once we have Kolibri with my changes applied, the manifest file code should actually never create a manifest file with the latter: if it includes all the nodes from a channel, it ends up with Hopefully in a future iteration we can pass a manifest file directly to the remoteimport task so we don't need to deal with all this :) |
||
extra_metadata=task, | ||
track_progress=True, | ||
cancellable=True, | ||
|
@@ -232,7 +286,7 @@ def post(self, request): | |
# Two weeks session expiry | ||
request.session.set_expiry(1209600) | ||
request.session["job_ids"] = job_ids | ||
request.session["downloading"] = collection | ||
request.session["downloading"] = f"{grade}-{collection}" | ||
return HttpResponse( | ||
json.dumps(job_ids), content_type="application/json" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like "grade" is an awkward word to use here since it's very specific to our current implementation, but at the same time the alternatives I can think of ("group", "category") would be terrrible and confusing, so 🤷