Skip to content

Commit 25c954e

Browse files
authored
Merge pull request #5545 from benjaoming/bug/video_url
Harden download robustness
2 parents 86e1ea6 + e941add commit 25c954e

File tree

23 files changed

+559
-235
lines changed

23 files changed

+559
-235
lines changed

docs/installguide/release_notes.rst

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,19 @@ to read the release notes.
1212
0.17.4 (unreleased)
1313
-------------------
1414

15+
Added
16+
^^^^^
17+
18+
* Progress displayed while downloading content packs :url-issue:`5356`
19+
1520
Bug fixes
1621
^^^^^^^^^
1722

18-
* Video download retry upon connection timeouts/errors :url-issue:`5528`
23+
* Video download retry upon connection establishment errors :url-issue:`5528`
24+
* Resume download if unplugged connection while downloading :url-issue:`5545`
25+
* Blank videos produced when ``kalite.learningequality.org`` server down :url-issue:`5538`
26+
* Video download jobs hanging indefinitely after pressing "Cancel" :url-issue:`5545`
27+
* Also delete content database when deleting a content pack :url-issue:`5545`
1928
* Simplified login is now working when there are 1,000 or more users registered in a facility. :url-issue:`5523`
2029

2130
New Features
@@ -26,12 +35,25 @@ New Features
2635
Developers
2736
^^^^^^^^^^
2837

29-
* Do not use `npm clean`, now requires npm>=5 for building on unclean systems :url-issue:`5519`
38+
* Do not use `npm clean`, now requires npm>=5 for building on unclean systems :url-issue:`5519`
3039

3140
Contents
3241
^^^^^^^^
3342

34-
* Resized video torrent set for English rebuilt with missing videos
43+
* Resized video torrent set for English rebuilt with missing videos
44+
45+
46+
Known issues
47+
^^^^^^^^^^^^
48+
49+
* It isn't possible to cancel video downloads if a video is downloading while
50+
the connection is switched off.
51+
* **Chrome 55-56** has issues scrolling the menus on touch devices. Upgrading to Chrome 57 fixes this. :url-issue:`5407`
52+
* **Windows** needs at least Python 2.7.11. The Windows installer for KA Lite will install the latest version of Python. If you installed KA Lite in another way, and your Python installation is more than a year old, you probably have to upgrade Python - you can fetch the latest 2.7.12 version `here <https://www.python.org/downloads/windows/>`__.
53+
* **Windows** installer tray application option "Run on start" does not work, see `learningequality/installers#106 <https://github.com/learningequality/installers/issues/106>`__ (also contains `a work-around <https://github.com/learningequality/installers/issues/106#issuecomment-237729680>`__)
54+
* **Windows + IE9** One-Click device registration is broken. Work-around: Use a different browser or use manual device registration. :url-issue:`5409`
55+
* **Firefox 47**: Subtitles are misaligned in the video player. This is fixed by upgrading Firefox.
56+
* A limited number of exercises with radio buttons have problems displaying :url-issue:`5172`
3557

3658

3759
0.17.3

kalite/distributed/management/commands/retrievecontentpack.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import logging
12
import os
23
import shutil
4+
import socket
35
import tempfile
46
import zipfile
57

@@ -20,8 +22,10 @@
2022
from kalite.updates.management.utils import UpdatesStaticCommand
2123
from peewee import SqliteDatabase
2224
from kalite.topic_tools.content_models import Item, AssessmentItem
25+
from requests.exceptions import ConnectionError, HTTPError
2326

24-
logging = django_settings.LOG
27+
28+
logger = logging.getLogger(__name__)
2529

2630

2731
class Command(UpdatesStaticCommand):
@@ -143,8 +147,22 @@ def download(self, *args, **options):
143147

144148
lang = args[1]
145149

150+
def download_callback(fraction):
151+
percent = int(fraction * 100)
152+
self.update_stage(
153+
fraction,
154+
stage_status=_("Downloaded {pct}% of {lang} content pack").format(
155+
pct=percent,
156+
lang=lang,
157+
)
158+
)
159+
146160
with tempfile.NamedTemporaryFile() as f:
147-
zf = download_content_pack(f, lang)
161+
try:
162+
zf = download_content_pack(f, lang, callback=download_callback)
163+
except (socket.error, ConnectionError, HTTPError) as e:
164+
self.cancel("Could not download content pack, unable to connect", str(e))
165+
return
148166
self.process_content_pack(zf, lang)
149167
zf.close()
150168

@@ -194,7 +212,7 @@ def extract_catalog_files(zf, lang):
194212
for zipmo, djangomo in filename_mapping.items():
195213
zipmof = zf.open(zipmo)
196214
mopath = os.path.join(modir, djangomo)
197-
logging.debug("writing to %s" % mopath)
215+
logger.debug("writing to %s" % mopath)
198216
with open(mopath, "wb") as djangomof:
199217
shutil.copyfileobj(zipmof, djangomof)
200218

kalite/distributed/static/js/distributed/perseus/ke/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"devDependencies": {
2424
"jison" : "0.4.13",
2525
"cheerio": "0.19.0",
26-
"uglify-js": "2.4.20",
26+
"uglify-js": "2.6.0",
2727
"jshint": "2.7.0"
2828
}
2929
}

kalite/i18n/base.py

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import errno
2+
import logging
23
import os
34
import re
45
import shutil
5-
import urllib
66
import zipfile
77
from distutils.version import LooseVersion
88
from fle_utils.internet.webcache import invalidate_web_cache
@@ -19,17 +19,24 @@
1919

2020
from fle_utils.config.models import Settings
2121
from fle_utils.general import ensure_dir, softload_json
22+
from fle_utils.internet.download import download_file
23+
import sys
24+
from kalite.topic_tools.settings import CONTENT_DATABASE_PATH
2225

2326

24-
CONTENT_PACK_URL_TEMPLATE = ("http://pantry.learningequality.org/downloads"
25-
"/ka-lite/{version}/content/contentpacks/{langcode}{suffix}.zip")
27+
CONTENT_PACK_URL_TEMPLATE = (
28+
"http://pantry.learningequality.org/downloads"
29+
"/ka-lite/{version}/content/contentpacks/{langcode}.zip"
30+
)
2631

2732

2833
CACHE_VARS = []
2934

3035

3136
from django.conf import settings
32-
logging = settings.LOG
37+
38+
39+
logger = logging.getLogger(__name__)
3340

3441

3542
class LanguageNotFoundError(Exception):
@@ -98,7 +105,7 @@ def get_code2lang_map(lang_code=None, force=False):
98105
global CODE2LANG_MAP
99106

100107
if force or not CODE2LANG_MAP:
101-
lmap = softload_json(settings.LANG_LOOKUP_FILEPATH, logger=logging.debug)
108+
lmap = softload_json(settings.LANG_LOOKUP_FILEPATH, logger=logger.debug)
102109

103110
CODE2LANG_MAP = {}
104111
for lc, entry in lmap.iteritems():
@@ -228,12 +235,12 @@ def _get_installed_language_packs():
228235
metadata_filepath = os.path.join(locale_dir, django_disk_code, "%s_metadata.json" % lcode_to_ietf(django_disk_code))
229236
lang_meta = softload_json(metadata_filepath, raises=True)
230237

231-
logging.debug("Found language pack %s" % (django_disk_code))
238+
logger.debug("Found language pack %s" % (django_disk_code))
232239
except IOError as e:
233240
if e.errno == errno.ENOENT:
234-
logging.info("Ignoring non-language pack %s in %s" % (django_disk_code, locale_dir))
241+
logger.info("Ignoring non-language pack %s in %s" % (django_disk_code, locale_dir))
235242
else:
236-
logging.error("Error reading %s metadata (%s): %s" % (django_disk_code, metadata_filepath, e))
243+
logger.error("Error reading %s metadata (%s): %s" % (django_disk_code, metadata_filepath, e))
237244
continue
238245

239246
installed_language_packs.append(lang_meta)
@@ -273,9 +280,9 @@ def update_jsi18n_file(code="en"):
273280
try:
274281
icu_js = open(os.path.join(path, code, "%s_icu.js" % code), "r").read()
275282
except IOError:
276-
logging.warn("No {code}_icu.js file found in locale_path {path}".format(code=code, path=path))
283+
logger.warn("No {code}_icu.js file found in locale_path {path}".format(code=code, path=path))
277284
output_js = response.content + "\n" + icu_js
278-
logging.info("Writing i18nized js file to {0}".format(output_file))
285+
logger.info("Writing i18nized js file to {0}".format(output_file))
279286
with open(output_file, "w") as fp:
280287
fp.write(output_js)
281288
translation.deactivate()
@@ -309,7 +316,7 @@ def select_best_available_language(target_code, available_codes=None):
309316
store_cache = True
310317
available_codes = get_installed_language_packs().keys()
311318

312-
# logging.debug("choosing best language among %s" % (available_codes))
319+
# logger.debug("choosing best language among %s" % (available_codes))
313320

314321
# Make it a tuple so we can hash it
315322
available_codes = [lcode_to_django_lang(lc) for lc in available_codes if lc]
@@ -329,7 +336,7 @@ def select_best_available_language(target_code, available_codes=None):
329336
raise RuntimeError("No languages found")
330337

331338
# if actual_code != target_code:
332-
# logging.debug("Requested code %s, got code %s" % (target_code, actual_code))
339+
# logger.debug("Requested code %s, got code %s" % (target_code, actual_code))
333340

334341
# Store in cache when available_codes are not set
335342
if store_cache:
@@ -345,12 +352,21 @@ def delete_language(lang_code):
345352
for langpack_resource_path in langpack_resource_paths:
346353
try:
347354
shutil.rmtree(langpack_resource_path)
348-
logging.info("Deleted language pack resource path: %s" % langpack_resource_path)
355+
logger.info("Deleted language pack resource path: %s" % langpack_resource_path)
349356
except OSError as e:
350357
if e.errno != 2: # Only ignore error: No Such File or Directory
351358
raise
352359
else:
353-
logging.debug("Not deleting missing language pack resource path: %s" % langpack_resource_path)
360+
logger.debug("Not deleting missing language pack resource path: %s" % langpack_resource_path)
361+
362+
# Delete the content db of particular language
363+
content_db = CONTENT_DATABASE_PATH.format(channel="khan", language=lang_code)
364+
if os.path.isfile(content_db):
365+
try:
366+
os.unlink(content_db)
367+
except OSError as e:
368+
if e.errno != 2: # Only ignore error: No Such File or Directory
369+
raise
354370

355371
invalidate_web_cache()
356372

@@ -361,7 +377,7 @@ def set_request_language(request, lang_code):
361377
lang_code = select_best_available_language(lang_code) # output is in django_lang format
362378

363379
if lang_code != request.session.get(settings.LANGUAGE_COOKIE_NAME):
364-
logging.debug("setting request language to %s (session language %s), from %s" % (lang_code, request.session.get("default_language"), request.session.get(settings.LANGUAGE_COOKIE_NAME)))
380+
logger.debug("setting request language to %s (session language %s), from %s" % (lang_code, request.session.get("default_language"), request.session.get(settings.LANGUAGE_COOKIE_NAME)))
365381
# Just in case we have a db-backed session, don't write unless we have to.
366382
request.session[settings.LANGUAGE_COOKIE_NAME] = lang_code
367383

@@ -376,29 +392,33 @@ def translate_block(language):
376392
translation.deactivate()
377393

378394

379-
def download_content_pack(fobj, lang, minimal=False):
395+
def get_content_pack_url(lang):
396+
return CONTENT_PACK_URL_TEMPLATE.format(
397+
version=SHORTVERSION,
398+
langcode=lang,
399+
)
400+
401+
def download_content_pack(fobj, lang, callback=None):
380402
"""Given a file object where the content pack lang will be stored, return a
381403
zipfile object pointing to the content pack.
404+
"""
405+
url = get_content_pack_url(lang)
382406

383-
If minimal is set to True, append the "-minimal" flag when downloading the
384-
contentpack.
407+
logger.info("Downloading content pack from {}".format(url))
385408

386-
"""
387-
url = CONTENT_PACK_URL_TEMPLATE.format(
388-
version=SHORTVERSION,
389-
langcode=lang,
390-
suffix="-minimal" if minimal else "",
391-
)
409+
def _callback(fraction):
410+
sys.stdout.write("\rProgress: [{}{}] ({}%)".format(
411+
"=" * (int(fraction * 100) / 2),
412+
" " * (50 - int(fraction * 100) / 2),
413+
int(fraction * 100),
414+
))
415+
if callback:
416+
callback(fraction)
392417

393-
logging.info("Downloading content pack from {}".format(url))
394-
httpf = urllib.urlopen(url) # returns a file-like object not exactly to zipfile's liking, so save first
418+
download_file(url, fp=fobj, callback=_callback)
395419

396-
shutil.copyfileobj(httpf, fobj)
397-
fobj.seek(0)
398420
zf = zipfile.ZipFile(fobj)
399421

400-
httpf.close()
401-
402422
return zf
403423

404424

0 commit comments

Comments
 (0)