Skip to content

Commit

Permalink
Add editor for audio translations (oppia#3692)
Browse files Browse the repository at this point in the history
* Add editor for audio translations.

* Complete functionality for adding and deleting audio files.

* Remove console logs.

* More minor updates from the merge. Reinstate resources_test.

* Address most review comments.

* Add modal for audio deletion. Fix backend tests.

* Add functionality for 'mark audio as needing update'.

* Add Karma tests.

* Add missing dependency.
  • Loading branch information
seanlip authored Aug 5, 2017
1 parent f6fe0a5 commit 597ce33
Show file tree
Hide file tree
Showing 43 changed files with 1,386 additions and 218 deletions.
12 changes: 12 additions & 0 deletions app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,15 @@ libraries:
# running the server in the dev environment.
env_variables:
MINIFICATION: false


skip_files:
# .pyc and .pyo files
- ^(.*/)?.*\.py[co]$
# Unix hidden files whose names begin with a dot
- ^(.*/)?\..*$
# Karma test files
- ^(.*/)Spec.js$
# Other folders to ignore
- tests/
- scripts/
16 changes: 0 additions & 16 deletions assets/images/icons/play-audio-button.svg

This file was deleted.

32 changes: 0 additions & 32 deletions assets/images/icons/settings.svg

This file was deleted.

18 changes: 0 additions & 18 deletions assets/images/icons/speaker-not-playing.svg

This file was deleted.

30 changes: 0 additions & 30 deletions assets/images/icons/speaker-playing.svg

This file was deleted.

16 changes: 0 additions & 16 deletions assets/images/icons/unresolved_answers.svg

This file was deleted.

42 changes: 23 additions & 19 deletions core/controllers/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import logging
import StringIO

import cloudstorage
import jinja2
import mutagen

Expand Down Expand Up @@ -864,18 +863,24 @@ def post(self, exploration_id):
self.render_json({'filepath': filename})


class AudioFileHandler(EditorHandler):
"""Handles audio file uploads to Google Cloud Storage."""
class AudioUploadHandler(EditorHandler):
"""Handles audio file uploads (to Google Cloud Storage in production, and
to the local datastore in dev).
"""

# The string to prefix to the filename (before tacking the whole thing on
# to the end of 'assets/').
_FILENAME_PREFIX = 'audio'

@acl_decorators.can_edit_exploration
def post(self, exploration_id):
"""Saves an audio file uploaded by a content creator."""

raw = self.request.get('raw')
raw_audio_file = self.request.get('raw_audio_file')
filename = self.payload.get('filename')
allowed_formats = feconf.ACCEPTED_AUDIO_EXTENSIONS.keys()

if not raw:
if not raw_audio_file:
raise self.InvalidInputException('No audio supplied')
dot_index = filename.rfind('.')
extension = filename[dot_index + 1:].lower()
Expand All @@ -890,7 +895,7 @@ def post(self, exploration_id):
'one of the following extensions: %s' % allowed_formats)

tempbuffer = StringIO.StringIO()
tempbuffer.write(raw)
tempbuffer.write(raw_audio_file)
tempbuffer.seek(0)
try:
audio = mutagen.File(tempbuffer)
Expand All @@ -906,9 +911,9 @@ def post(self, exploration_id):
'as a %s file' % extension)
if audio.info.length > feconf.MAX_AUDIO_FILE_LENGTH_SEC:
raise self.InvalidInputException(
'Audio must be under %s seconds in length. '
'Found length %s' % (feconf.MAX_AUDIO_FILE_LENGTH_SEC,
audio.info.length))
'Audio files must be under %s seconds in length. The uploaded '
'file is %.2f seconds long.' % (
feconf.MAX_AUDIO_FILE_LENGTH_SEC, audio.info.length))
if len(set(audio.mime).intersection(
set(feconf.ACCEPTED_AUDIO_EXTENSIONS[extension]))) == 0:
raise self.InvalidInputException(
Expand All @@ -924,16 +929,15 @@ def post(self, exploration_id):
# object being recursively passed around in app engine.
del audio

bucket_name = feconf.GCS_RESOURCE_BUCKET_NAME

# Upload to GCS bucket with filepath
# "<bucket>/<exploration-id>/assets/audio/<filename>".
gcs_file_url = ('/%s/%s/assets/audio/%s'
% (bucket_name, exploration_id, filename))
gcs_file = cloudstorage.open(
gcs_file_url, 'w', content_type=mimetype)
gcs_file.write(raw)
gcs_file.close()
# Audio files are stored to the datastore in the dev env, and to GCS
# in production.
file_system_class = (
fs_domain.ExplorationFileSystem if feconf.DEV_MODE
else fs_domain.GcsFileSystem)
fs = fs_domain.AbstractFileSystem(file_system_class(exploration_id))
fs.commit(
self.user_id, '%s/%s' % (self._FILENAME_PREFIX, filename),
raw_audio_file, mimetype=mimetype)

self.render_json({'filename': filename})

Expand Down
35 changes: 35 additions & 0 deletions core/controllers/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,38 @@ def get(self, exploration_id, encoded_filepath):
self.response.write(raw)
except:
raise self.PageNotFoundException


class AudioHandler(base.BaseHandler):
"""Handles audio retrievals (only in dev -- in production, audio files are
served from GCS).
"""

_AUDIO_PATH_PREFIX = 'audio'

@acl_decorators.open_access
def get(self, exploration_id, filename):
"""Returns an audio file.
Args:
encoded_filepath: a string representing the audio filepath. This
string is encoded in the frontend using encodeURIComponent().
"""
file_format = filename[(filename.rfind('.') + 1):]
# If the following is not cast to str, an error occurs in the wsgi
# library because unicode gets used.
self.response.headers['Content-Type'] = str(
'audio/%s' % file_format)

fs = fs_domain.AbstractFileSystem(
fs_domain.ExplorationFileSystem(exploration_id))

try:
raw = fs.get('%s/%s' % (self._AUDIO_PATH_PREFIX, filename))
except:
raise self.PageNotFoundException

self.response.cache_control.no_cache = None
self.response.cache_control.public = True
self.response.cache_control.max_age = 600
self.response.write(raw)
Loading

0 comments on commit 597ce33

Please sign in to comment.