Commit bc30addf by Jay Zoldak

Unicode changes to support QRF

fixing unit tests

fixing merge error

fixing xqueue submission issue with unicode url (trial 0.1)

fixing fotmats as commented upon

removing yaml file language selection

Unicode changes to support QRF

removed unnecessary pass in modulestore/init.py

fixing merge error

fixing fotmats as commented upon

removing yaml file language selection

fixing pep8 violations

- fixing pylint violations

pylint violation

fixing line spaces and formats

ignore pylint E1101

remove empty line

fixing pylint violations

 pep8 violations

bulk mail unicode/decode

fix migration error

fix pep8 just to push again

more unicode/decode
Final changes to comments and error messages.
parent 1ff4671b
...@@ -143,7 +143,7 @@ def get_lms_link_for_item(location, preview=False, course_id=None): ...@@ -143,7 +143,7 @@ def get_lms_link_for_item(location, preview=False, course_id=None):
else: else:
lms_base = settings.LMS_BASE lms_base = settings.LMS_BASE
lms_link = "//{lms_base}/courses/{course_id}/jump_to/{location}".format( lms_link = u"//{lms_base}/courses/{course_id}/jump_to/{location}".format(
lms_base=lms_base, lms_base=lms_base,
course_id=course_id, course_id=course_id,
location=Location(location) location=Location(location)
...@@ -179,7 +179,7 @@ def get_lms_link_for_about_page(location): ...@@ -179,7 +179,7 @@ def get_lms_link_for_about_page(location):
about_base = None about_base = None
if about_base is not None: if about_base is not None:
lms_link = "//{about_base_url}/courses/{course_id}/about".format( lms_link = u"//{about_base_url}/courses/{course_id}/about".format(
about_base_url=about_base, about_base_url=about_base,
course_id=Location(location).course_id course_id=Location(location).course_id
) )
......
...@@ -267,8 +267,7 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N ...@@ -267,8 +267,7 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N
preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE') preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE')
preview_lms_link = ( preview_lms_link = (
'//{preview_lms_base}/courses/{org}/{course}/' u'//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'
'{course_name}/courseware/{section}/{subsection}/{index}'
).format( ).format(
preview_lms_base=preview_lms_base, preview_lms_base=preview_lms_base,
lms_base=settings.LMS_BASE, lms_base=settings.LMS_BASE,
......
...@@ -251,7 +251,7 @@ def create_new_course(request): ...@@ -251,7 +251,7 @@ def create_new_course(request):
run = request.json.get('run') run = request.json.get('run')
try: try:
dest_location = Location('i4x', org, number, 'course', run) dest_location = Location(u'i4x', org, number, u'course', run)
except InvalidLocationError as error: except InvalidLocationError as error:
return JsonResponse({ return JsonResponse({
"ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format( "ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format(
...@@ -286,8 +286,10 @@ def create_new_course(request): ...@@ -286,8 +286,10 @@ def create_new_course(request):
course_search_location = bson.son.SON({ course_search_location = bson.son.SON({
'_id.tag': 'i4x', '_id.tag': 'i4x',
# cannot pass regex to Location constructor; thus this hack # cannot pass regex to Location constructor; thus this hack
'_id.org': re.compile('^{}$'.format(dest_location.org), re.IGNORECASE), # pylint: disable=E1101
'_id.course': re.compile('^{}$'.format(dest_location.course), re.IGNORECASE), '_id.org': re.compile(u'^{}$'.format(dest_location.org), re.IGNORECASE | re.UNICODE),
# pylint: disable=E1101
'_id.course': re.compile(u'^{}$'.format(dest_location.course), re.IGNORECASE | re.UNICODE),
'_id.category': 'course', '_id.category': 'course',
}) })
courses = modulestore().collection.find(course_search_location, fields=('_id')) courses = modulestore().collection.find(course_search_location, fields=('_id'))
......
...@@ -41,7 +41,7 @@ def handler_prefix(block, handler='', suffix=''): ...@@ -41,7 +41,7 @@ def handler_prefix(block, handler='', suffix=''):
Trailing `/`s are removed from the returned url. Trailing `/`s are removed from the returned url.
""" """
return reverse('preview_handler', kwargs={ return reverse('preview_handler', kwargs={
'usage_id': quote_slashes(str(block.scope_ids.usage_id)), 'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')),
'handler': handler, 'handler': handler,
'suffix': suffix, 'suffix': suffix,
}).rstrip('/?') }).rstrip('/?')
......
...@@ -84,8 +84,8 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"], ...@@ -84,8 +84,8 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
if (required) { if (required) {
return required; return required;
} }
if (item !== encodeURIComponent(item)) { if (/\s/g.test(item)) {
return gettext('Please do not use any spaces or special characters in this field.'); return gettext('Please do not use any spaces in this field.');
} }
return ''; return '';
}; };
......
...@@ -164,7 +164,7 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit" ...@@ -164,7 +164,7 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
% endif % endif
${_("with the subsection {link_start}{name}{link_end}").format( ${_("with the subsection {link_start}{name}{link_end}").format(
name=subsection.display_name_with_default, name=subsection.display_name_with_default,
link_start='<a href="{url}">'.format(url=subsection_url), link_start=u'<a href="{url}">'.format(url=subsection_url),
link_end='</a>', link_end='</a>',
)} )}
</p> </p>
......
...@@ -110,12 +110,12 @@ def instance_key(model, instance_or_pk): ...@@ -110,12 +110,12 @@ def instance_key(model, instance_or_pk):
def set_cached_content(content): def set_cached_content(content):
cache.set(str(content.location), content) cache.set(unicode(content.location).encode("utf-8"), content)
def get_cached_content(location): def get_cached_content(location):
return cache.get(str(location)) return cache.get(unicode(location).encode("utf-8"))
def del_cached_content(location): def del_cached_content(location):
cache.delete(str(location)) cache.delete(unicode(location).encode("utf-8"))
...@@ -77,7 +77,7 @@ def is_commentable_cohorted(course_id, commentable_id): ...@@ -77,7 +77,7 @@ def is_commentable_cohorted(course_id, commentable_id):
# inline discussions are cohorted by default # inline discussions are cohorted by default
ans = True ans = True
log.debug("is_commentable_cohorted({0}, {1}) = {2}".format(course_id, log.debug(u"is_commentable_cohorted({0}, {1}) = {2}".format(course_id,
commentable_id, commentable_id,
ans)) ans))
return ans return ans
......
...@@ -19,7 +19,7 @@ def _url_replace_regex(prefix): ...@@ -19,7 +19,7 @@ def _url_replace_regex(prefix):
To anyone contemplating making this more complicated: To anyone contemplating making this more complicated:
http://xkcd.com/1171/ http://xkcd.com/1171/
""" """
return r""" return ur"""
(?x) # flags=re.VERBOSE (?x) # flags=re.VERBOSE
(?P<quote>\\?['"]) # the opening quotes (?P<quote>\\?['"]) # the opening quotes
(?P<prefix>{prefix}) # the prefix (?P<prefix>{prefix}) # the prefix
...@@ -152,7 +152,7 @@ def replace_static_urls(text, data_directory, course_id=None, static_asset_path= ...@@ -152,7 +152,7 @@ def replace_static_urls(text, data_directory, course_id=None, static_asset_path=
return "".join([quote, url, quote]) return "".join([quote, url, quote])
return re.sub( return re.sub(
_url_replace_regex('(?:{static_url}|/static/)(?!{data_dir})'.format( _url_replace_regex(u'(?:{static_url}|/static/)(?!{data_dir})'.format(
static_url=settings.STATIC_URL, static_url=settings.STATIC_URL,
data_dir=static_asset_path or data_directory data_dir=static_asset_path or data_directory
)), )),
......
...@@ -159,30 +159,30 @@ class CourseRole(GroupBasedRole): ...@@ -159,30 +159,30 @@ class CourseRole(GroupBasedRole):
if isinstance(self.location, Location): if isinstance(self.location, Location):
try: try:
groupnames.append('{0}_{1}'.format(role, self.location.course_id)) groupnames.append(u'{0}_{1}'.format(role, self.location.course_id))
course_context = self.location.course_id # course_id is valid for translation course_context = self.location.course_id # course_id is valid for translation
except InvalidLocationError: # will occur on old locations where location is not of category course except InvalidLocationError: # will occur on old locations where location is not of category course
if course_context is None: if course_context is None:
raise CourseContextRequired() raise CourseContextRequired()
else: else:
groupnames.append('{0}_{1}'.format(role, course_context)) groupnames.append(u'{0}_{1}'.format(role, course_context))
try: try:
locator = loc_mapper().translate_location_to_course_locator(course_context, self.location) locator = loc_mapper().translate_location_to_course_locator(course_context, self.location)
groupnames.append('{0}_{1}'.format(role, locator.package_id)) groupnames.append(u'{0}_{1}'.format(role, locator.package_id))
except (InvalidLocationError, ItemNotFoundError): except (InvalidLocationError, ItemNotFoundError):
# if it's never been mapped, the auth won't be via the Locator syntax # if it's never been mapped, the auth won't be via the Locator syntax
pass pass
# least preferred legacy role_course format # least preferred legacy role_course format
groupnames.append('{0}_{1}'.format(role, self.location.course)) groupnames.append(u'{0}_{1}'.format(role, self.location.course)) # pylint: disable=E1101, E1103
elif isinstance(self.location, CourseLocator): elif isinstance(self.location, CourseLocator):
groupnames.append('{0}_{1}'.format(role, self.location.package_id)) groupnames.append(u'{0}_{1}'.format(role, self.location.package_id))
# handle old Location syntax # handle old Location syntax
old_location = loc_mapper().translate_locator_to_location(self.location, get_course=True) old_location = loc_mapper().translate_locator_to_location(self.location, get_course=True)
if old_location: if old_location:
# the slashified version of the course_id (myu/mycourse/myrun) # the slashified version of the course_id (myu/mycourse/myrun)
groupnames.append('{0}_{1}'.format(role, old_location.course_id)) groupnames.append(u'{0}_{1}'.format(role, old_location.course_id))
# add the least desirable but sometimes occurring format. # add the least desirable but sometimes occurring format.
groupnames.append('{0}_{1}'.format(role, old_location.course)) groupnames.append(u'{0}_{1}'.format(role, old_location.course)) # pylint: disable=E1101, E1103
super(CourseRole, self).__init__(groupnames) super(CourseRole, self).__init__(groupnames)
...@@ -193,12 +193,13 @@ class OrgRole(GroupBasedRole): ...@@ -193,12 +193,13 @@ class OrgRole(GroupBasedRole):
""" """
def __init__(self, role, location): def __init__(self, role, location):
location = Location(location) location = Location(location)
super(OrgRole, self).__init__(['{}_{}'.format(role, location.org)]) super(OrgRole, self).__init__([u'{}_{}'.format(role, location.org)])
class CourseStaffRole(CourseRole): class CourseStaffRole(CourseRole):
"""A Staff member of a course""" """A Staff member of a course"""
ROLE = 'staff' ROLE = 'staff'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CourseStaffRole, self).__init__(self.ROLE, *args, **kwargs) super(CourseStaffRole, self).__init__(self.ROLE, *args, **kwargs)
......
...@@ -353,7 +353,7 @@ def dashboard(request): ...@@ -353,7 +353,7 @@ def dashboard(request):
course_enrollment_pairs.append((course, enrollment)) course_enrollment_pairs.append((course, enrollment))
except ItemNotFoundError: except ItemNotFoundError:
log.error("User {0} enrolled in non-existent course {1}" log.error(u"User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id)) .format(user.username, enrollment.course_id))
course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True) course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True)
...@@ -495,9 +495,9 @@ def change_enrollment(request): ...@@ -495,9 +495,9 @@ def change_enrollment(request):
org, course_num, run = course_id.split("/") org, course_num, run = course_id.split("/")
dog_stats_api.increment( dog_stats_api.increment(
"common.student.enrollment", "common.student.enrollment",
tags=["org:{0}".format(org), tags=[u"org:{0}".format(org),
"course:{0}".format(course_num), u"course:{0}".format(course_num),
"run:{0}".format(run)] u"run:{0}".format(run)]
) )
CourseEnrollment.enroll(user, course.id, mode=current_mode.slug) CourseEnrollment.enroll(user, course.id, mode=current_mode.slug)
......
...@@ -372,7 +372,7 @@ class LoncapaProblem(object): ...@@ -372,7 +372,7 @@ class LoncapaProblem(object):
# TODO: figure out where to get file submissions when rescoring. # TODO: figure out where to get file submissions when rescoring.
if 'filesubmission' in responder.allowed_inputfields and student_answers is None: if 'filesubmission' in responder.allowed_inputfields and student_answers is None:
_ = self.capa_system.i18n.ugettext _ = self.capa_system.i18n.ugettext
raise Exception(_("Cannot rescore problems with possible file submissions")) raise Exception(_(u"Cannot rescore problems with possible file submissions"))
# use 'student_answers' only if it is provided, and if it might contain a file # use 'student_answers' only if it is provided, and if it might contain a file
# submission that would not exist in the persisted "student_answers". # submission that would not exist in the persisted "student_answers".
......
...@@ -50,7 +50,7 @@ class CorrectMap(object): ...@@ -50,7 +50,7 @@ class CorrectMap(object):
): ):
if answer_id is not None: if answer_id is not None:
self.cmap[str(answer_id)] = { self.cmap[answer_id] = {
'correctness': correctness, 'correctness': correctness,
'npoints': npoints, 'npoints': npoints,
'msg': msg, 'msg': msg,
......
...@@ -62,7 +62,7 @@ class XQueueInterface(object): ...@@ -62,7 +62,7 @@ class XQueueInterface(object):
""" """
def __init__(self, url, django_auth, requests_auth=None): def __init__(self, url, django_auth, requests_auth=None):
self.url = url self.url = unicode(url)
self.auth = django_auth self.auth = django_auth
self.session = requests.Session() self.session = requests.Session()
self.session.auth = requests_auth self.session.auth = requests_auth
......
...@@ -34,7 +34,9 @@ class StaticContent(object): ...@@ -34,7 +34,9 @@ class StaticContent(object):
@staticmethod @staticmethod
def generate_thumbnail_name(original_name): def generate_thumbnail_name(original_name):
return ('{0}' + XASSET_THUMBNAIL_TAIL_NAME).format(os.path.splitext(original_name)[0]) return u"{name_root}{extension}".format(
name_root=os.path.splitext(original_name)[0],
extension=XASSET_THUMBNAIL_TAIL_NAME,)
@staticmethod @staticmethod
def compute_location(org, course, name, revision=None, is_thumbnail=False): def compute_location(org, course, name, revision=None, is_thumbnail=False):
...@@ -64,7 +66,7 @@ class StaticContent(object): ...@@ -64,7 +66,7 @@ class StaticContent(object):
""" """
Returns a boolean if a path is believed to be a c4x link based on the leading element Returns a boolean if a path is believed to be a c4x link based on the leading element
""" """
return path_string.startswith('/{0}/'.format(XASSET_LOCATION_TAG)) return path_string.startswith(u'/{0}/'.format(XASSET_LOCATION_TAG))
@staticmethod @staticmethod
def renamespace_c4x_path(path_string, target_location): def renamespace_c4x_path(path_string, target_location):
...@@ -86,14 +88,14 @@ class StaticContent(object): ...@@ -86,14 +88,14 @@ class StaticContent(object):
the actual /c4x/... path which the client needs to reference static content the actual /c4x/... path which the client needs to reference static content
""" """
if location is not None: if location is not None:
return "/static/{name}".format(**location.dict()) return u"/static/{name}".format(**location.dict())
else: else:
return None return None
@staticmethod @staticmethod
def get_base_url_path_for_course_assets(loc): def get_base_url_path_for_course_assets(loc):
if loc is not None: if loc is not None:
return "/c4x/{org}/{course}/asset".format(**loc.dict()) return u"/c4x/{org}/{course}/asset".format(**loc.dict())
@staticmethod @staticmethod
def get_id_from_location(location): def get_id_from_location(location):
...@@ -237,6 +239,6 @@ class ContentStore(object): ...@@ -237,6 +239,6 @@ class ContentStore(object):
except Exception, e: except Exception, e:
# log and continue as thumbnails are generally considered as optional # log and continue as thumbnails are generally considered as optional
logging.exception("Failed to generate thumbnail for {0}. Exception: {1}".format(content.location, str(e))) logging.exception(u"Failed to generate thumbnail for {0}. Exception: {1}".format(content.location, str(e)))
return thumbnail_content, thumbnail_file_location return thumbnail_content, thumbnail_file_location
...@@ -30,12 +30,12 @@ URL_RE = re.compile(""" ...@@ -30,12 +30,12 @@ URL_RE = re.compile("""
# TODO (cpennington): We should decide whether we want to expand the # TODO (cpennington): We should decide whether we want to expand the
# list of valid characters in a location # list of valid characters in a location
INVALID_CHARS = re.compile(r"[^\w.-]") INVALID_CHARS = re.compile(r"[^\w.%-]", re.UNICODE)
# Names are allowed to have colons. # Names are allowed to have colons.
INVALID_CHARS_NAME = re.compile(r"[^\w.:-]") INVALID_CHARS_NAME = re.compile(r"[^\w.:%-]", re.UNICODE)
# html ids can contain word chars and dashes # html ids can contain word chars and dashes
INVALID_HTML_CHARS = re.compile(r"[^\w-]") INVALID_HTML_CHARS = re.compile(r"[^\w-]", re.UNICODE)
_LocationBase = namedtuple('LocationBase', 'tag org course category name revision') _LocationBase = namedtuple('LocationBase', 'tag org course category name revision')
...@@ -186,14 +186,14 @@ class Location(_LocationBase): ...@@ -186,14 +186,14 @@ class Location(_LocationBase):
elif isinstance(location, basestring): elif isinstance(location, basestring):
match = URL_RE.match(location) match = URL_RE.match(location)
if match is None: if match is None:
log.debug("location %r doesn't match URL", location) log.debug(u"location %r doesn't match URL", location)
raise InvalidLocationError(location) raise InvalidLocationError(location)
groups = match.groupdict() groups = match.groupdict()
check_dict(groups) check_dict(groups)
return _LocationBase.__new__(_cls, **groups) return _LocationBase.__new__(_cls, **groups)
elif isinstance(location, (list, tuple)): elif isinstance(location, (list, tuple)):
if len(location) not in (5, 6): if len(location) not in (5, 6):
log.debug('location has wrong length') log.debug(u'location has wrong length')
raise InvalidLocationError(location) raise InvalidLocationError(location)
if len(location) == 5: if len(location) == 5:
...@@ -216,9 +216,9 @@ class Location(_LocationBase): ...@@ -216,9 +216,9 @@ class Location(_LocationBase):
""" """
Return a string containing the URL for this location Return a string containing the URL for this location
""" """
url = "{0.tag}://{0.org}/{0.course}/{0.category}/{0.name}".format(self) url = u"{0.tag}://{0.org}/{0.course}/{0.category}/{0.name}".format(self)
if self.revision: if self.revision:
url += "@" + self.revision url += u"@{rev}".format(rev=self.revision) # pylint: disable=E1101
return url return url
def html_id(self): def html_id(self):
...@@ -226,7 +226,7 @@ class Location(_LocationBase): ...@@ -226,7 +226,7 @@ class Location(_LocationBase):
Return a string with a version of the location that is safe for use in Return a string with a version of the location that is safe for use in
html id attributes html id attributes
""" """
id_string = "-".join(str(v) for v in self.list() if v is not None) id_string = u"-".join(v for v in self.list() if v is not None)
return Location.clean_for_html(id_string) return Location.clean_for_html(id_string)
def dict(self): def dict(self):
...@@ -240,6 +240,9 @@ class Location(_LocationBase): ...@@ -240,6 +240,9 @@ class Location(_LocationBase):
return list(self) return list(self)
def __str__(self): def __str__(self):
return str(self.url().encode("utf-8"))
def __unicode__(self):
return self.url() return self.url()
def __repr__(self): def __repr__(self):
...@@ -254,7 +257,7 @@ class Location(_LocationBase): ...@@ -254,7 +257,7 @@ class Location(_LocationBase):
Throws an InvalidLocationError is this location does not represent a course. Throws an InvalidLocationError is this location does not represent a course.
""" """
if self.category != 'course': if self.category != 'course':
raise InvalidLocationError('Cannot call course_id for {0} because it is not of category course'.format(self)) raise InvalidLocationError(u'Cannot call course_id for {0} because it is not of category course'.format(self))
return "/".join([self.org, self.course, self.name]) return "/".join([self.org, self.course, self.name])
......
...@@ -88,9 +88,9 @@ class LocMapperStore(object): ...@@ -88,9 +88,9 @@ class LocMapperStore(object):
""" """
if package_id is None: if package_id is None:
if course_location.category == 'course': if course_location.category == 'course':
package_id = "{0.org}.{0.course}.{0.name}".format(course_location) package_id = u"{0.org}.{0.course}.{0.name}".format(course_location)
else: else:
package_id = "{0.org}.{0.course}".format(course_location) package_id = u"{0.org}.{0.course}".format(course_location)
# very like _interpret_location_id but w/o the _id # very like _interpret_location_id but w/o the _id
location_id = self._construct_location_son( location_id = self._construct_location_son(
course_location.org, course_location.course, course_location.org, course_location.course,
...@@ -185,7 +185,6 @@ class LocMapperStore(object): ...@@ -185,7 +185,6 @@ class LocMapperStore(object):
self._cache_location_map_entry(old_style_course_id, location, published_usage, draft_usage) self._cache_location_map_entry(old_style_course_id, location, published_usage, draft_usage)
return result return result
def translate_locator_to_location(self, locator, get_course=False): def translate_locator_to_location(self, locator, get_course=False):
""" """
Returns an old style Location for the given Locator if there's an appropriate entry in the Returns an old style Location for the given Locator if there's an appropriate entry in the
...@@ -331,12 +330,12 @@ class LocMapperStore(object): ...@@ -331,12 +330,12 @@ class LocMapperStore(object):
# strip id envelope if any # strip id envelope if any
entry_id = entry_id.get('_id', entry_id) entry_id = entry_id.get('_id', entry_id)
if entry_id.get('name', False): if entry_id.get('name', False):
return '{0[org]}/{0[course]}/{0[name]}'.format(entry_id) return u'{0[org]}/{0[course]}/{0[name]}'.format(entry_id)
elif entry_id.get('_id.org', False): elif entry_id.get('_id.org', False):
# the odd format one # the odd format one
return '{0[_id.org]}/{0[_id.course]}'.format(entry_id) return u'{0[_id.org]}/{0[_id.course]}'.format(entry_id)
else: else:
return '{0[org]}/{0[course]}'.format(entry_id) return u'{0[org]}/{0[course]}'.format(entry_id)
def _construct_location_son(self, org, course, name=None): def _construct_location_son(self, org, course, name=None):
""" """
...@@ -392,7 +391,7 @@ class LocMapperStore(object): ...@@ -392,7 +391,7 @@ class LocMapperStore(object):
""" """
See if the location x published pair is in the cache. If so, return the mapped locator. See if the location x published pair is in the cache. If so, return the mapped locator.
""" """
entry = self.cache.get('{}+{}'.format(old_course_id, location.url())) entry = self.cache.get(u'{}+{}'.format(old_course_id, location.url()))
if entry is not None: if entry is not None:
if published: if published:
return entry[0] return entry[0]
...@@ -424,7 +423,7 @@ class LocMapperStore(object): ...@@ -424,7 +423,7 @@ class LocMapperStore(object):
See if the package_id is in the cache. If so, return the mapped location to the See if the package_id is in the cache. If so, return the mapped location to the
course root. course root.
""" """
return self.cache.get('courseId+{}'.format(locator_package_id)) return self.cache.get(u'courseId+{}'.format(locator_package_id))
def _cache_course_locator(self, old_course_id, published_course_locator, draft_course_locator): def _cache_course_locator(self, old_course_id, published_course_locator, draft_course_locator):
""" """
...@@ -442,9 +441,9 @@ class LocMapperStore(object): ...@@ -442,9 +441,9 @@ class LocMapperStore(object):
""" """
setmany = {} setmany = {}
if location.category == 'course': if location.category == 'course':
setmany['courseId+{}'.format(published_usage.package_id)] = location setmany[u'courseId+{}'.format(published_usage.package_id)] = location
setmany[unicode(published_usage)] = location setmany[unicode(published_usage)] = location
setmany[unicode(draft_usage)] = location setmany[unicode(draft_usage)] = location
setmany['{}+{}'.format(old_course_id, location.url())] = (published_usage, draft_usage) setmany[u'{}+{}'.format(old_course_id, location.url())] = (published_usage, draft_usage)
setmany[old_course_id] = (published_usage, draft_usage) setmany[old_course_id] = (published_usage, draft_usage)
self.cache.set_many(setmany) self.cache.set_many(setmany)
...@@ -64,13 +64,13 @@ class Locator(object): ...@@ -64,13 +64,13 @@ class Locator(object):
''' '''
str(self) returns something like this: "mit.eecs.6002x" str(self) returns something like this: "mit.eecs.6002x"
''' '''
return unicode(self).encode('utf8') return unicode(self).encode('utf-8')
def __unicode__(self): def __unicode__(self):
''' '''
unicode(self) returns something like this: "mit.eecs.6002x" unicode(self) returns something like this: "mit.eecs.6002x"
''' '''
return self.url() return unicode(self).encode('utf-8')
@abstractmethod @abstractmethod
def version(self): def version(self):
...@@ -199,12 +199,12 @@ class CourseLocator(Locator): ...@@ -199,12 +199,12 @@ class CourseLocator(Locator):
Return a string representing this location. Return a string representing this location.
""" """
if self.package_id: if self.package_id:
result = self.package_id result = unicode(self.package_id)
if self.branch: if self.branch:
result += '/' + BRANCH_PREFIX + self.branch result += '/' + BRANCH_PREFIX + self.branch
return result return result
elif self.version_guid: elif self.version_guid:
return VERSION_PREFIX + str(self.version_guid) return u"{prefix}{guid}".format(prefix=VERSION_PREFIX, guid=self.version_guid)
else: else:
# raise InsufficientSpecificationError("missing package_id or version_guid") # raise InsufficientSpecificationError("missing package_id or version_guid")
return '<InsufficientSpecificationError: missing package_id or version_guid>' return '<InsufficientSpecificationError: missing package_id or version_guid>'
...@@ -213,7 +213,7 @@ class CourseLocator(Locator): ...@@ -213,7 +213,7 @@ class CourseLocator(Locator):
""" """
Return a string containing the URL for this location. Return a string containing the URL for this location.
""" """
return 'edx://' + unicode(self) return u'edx://' + unicode(self)
def _validate_args(self, url, version_guid, package_id): def _validate_args(self, url, version_guid, package_id):
""" """
...@@ -526,7 +526,7 @@ class DefinitionLocator(Locator): ...@@ -526,7 +526,7 @@ class DefinitionLocator(Locator):
Return a string containing the URL for this location. Return a string containing the URL for this location.
url(self) returns something like this: 'defx://version/519665f6223ebd6980884f2b' url(self) returns something like this: 'defx://version/519665f6223ebd6980884f2b'
""" """
return 'defx://' + unicode(self) return u'defx://' + unicode(self)
def version(self): def version(self):
""" """
......
...@@ -7,7 +7,8 @@ BLOCK_PREFIX = r"block/" ...@@ -7,7 +7,8 @@ BLOCK_PREFIX = r"block/"
# Prefix for the version portion of a locator URL, when it is preceded by a course ID # Prefix for the version portion of a locator URL, when it is preceded by a course ID
VERSION_PREFIX = r"version/" VERSION_PREFIX = r"version/"
ALLOWED_ID_CHARS = r'[a-zA-Z0-9_\-~.:]' ALLOWED_ID_CHARS = r'[\w\-~.:]'
URL_RE_SOURCE = r""" URL_RE_SOURCE = r"""
(?P<tag>edx://)? (?P<tag>edx://)?
...@@ -20,7 +21,7 @@ URL_RE_SOURCE = r""" ...@@ -20,7 +21,7 @@ URL_RE_SOURCE = r"""
VERSION_PREFIX=VERSION_PREFIX, BLOCK_PREFIX=BLOCK_PREFIX VERSION_PREFIX=VERSION_PREFIX, BLOCK_PREFIX=BLOCK_PREFIX
) )
URL_RE = re.compile('^' + URL_RE_SOURCE + '$', re.IGNORECASE | re.VERBOSE) URL_RE = re.compile('^' + URL_RE_SOURCE + '$', re.IGNORECASE | re.VERBOSE | re.UNICODE)
def parse_url(string, tag_optional=False): def parse_url(string, tag_optional=False):
...@@ -54,7 +55,7 @@ def parse_url(string, tag_optional=False): ...@@ -54,7 +55,7 @@ def parse_url(string, tag_optional=False):
return matched_dict return matched_dict
BLOCK_RE = re.compile(r'^' + ALLOWED_ID_CHARS + r'+$', re.IGNORECASE) BLOCK_RE = re.compile(r'^' + ALLOWED_ID_CHARS + r'+$', re.IGNORECASE | re.UNICODE)
def parse_block_ref(string): def parse_block_ref(string):
......
...@@ -73,13 +73,13 @@ def rewrite_nonportable_content_links(source_course_id, dest_course_id, text): ...@@ -73,13 +73,13 @@ def rewrite_nonportable_content_links(source_course_id, dest_course_id, text):
# NOTE: ultimately link updating is not a hard requirement, so if something blows up with # NOTE: ultimately link updating is not a hard requirement, so if something blows up with
# the regex subsitution, log the error and continue # the regex subsitution, log the error and continue
try: try:
c4x_link_base = '{0}/'.format(StaticContent.get_base_url_path_for_course_assets(course_location)) c4x_link_base = u'{0}/'.format(StaticContent.get_base_url_path_for_course_assets(course_location))
text = re.sub(_prefix_only_url_replace_regex(c4x_link_base), portable_asset_link_subtitution, text) text = re.sub(_prefix_only_url_replace_regex(c4x_link_base), portable_asset_link_subtitution, text)
except Exception, e: except Exception, e:
logging.warning("Error going regex subtituion %r on text = %r.\n\nError msg = %s", c4x_link_base, text, str(e)) logging.warning("Error going regex subtituion %r on text = %r.\n\nError msg = %s", c4x_link_base, text, str(e))
try: try:
jump_to_link_base = '/courses/{org}/{course}/{run}/jump_to/i4x://{org}/{course}/'.format( jump_to_link_base = u'/courses/{org}/{course}/{run}/jump_to/i4x://{org}/{course}/'.format(
org=org, course=course, run=run org=org, course=course, run=run
) )
text = re.sub(_prefix_and_category_url_replace_regex(jump_to_link_base), portable_jump_to_link_substitution, text) text = re.sub(_prefix_and_category_url_replace_regex(jump_to_link_base), portable_jump_to_link_substitution, text)
...@@ -94,7 +94,7 @@ def rewrite_nonportable_content_links(source_course_id, dest_course_id, text): ...@@ -94,7 +94,7 @@ def rewrite_nonportable_content_links(source_course_id, dest_course_id, text):
# #
if source_course_id != dest_course_id: if source_course_id != dest_course_id:
try: try:
generic_courseware_link_base = '/courses/{org}/{course}/{run}/'.format( generic_courseware_link_base = u'/courses/{org}/{course}/{run}/'.format(
org=org, course=course, run=run org=org, course=course, run=run
) )
text = re.sub(_prefix_only_url_replace_regex(generic_courseware_link_base), portable_asset_link_subtitution, text) text = re.sub(_prefix_only_url_replace_regex(generic_courseware_link_base), portable_asset_link_subtitution, text)
......
...@@ -129,9 +129,9 @@ class ConditionalModuleBasicTest(unittest.TestCase): ...@@ -129,9 +129,9 @@ class ConditionalModuleBasicTest(unittest.TestCase):
html = modules['cond_module'].render('student_view').content html = modules['cond_module'].render('student_view').content
expected = modules['cond_module'].xmodule_runtime.render_template('conditional_ajax.html', { expected = modules['cond_module'].xmodule_runtime.render_template('conditional_ajax.html', {
'ajax_url': modules['cond_module'].xmodule_runtime.ajax_url, 'ajax_url': modules['cond_module'].xmodule_runtime.ajax_url,
'element_id': 'i4x-edX-conditional_test-conditional-SampleConditional', 'element_id': u'i4x-edX-conditional_test-conditional-SampleConditional',
'id': 'i4x://edX/conditional_test/conditional/SampleConditional', 'id': u'i4x://edX/conditional_test/conditional/SampleConditional',
'depends': 'i4x-edX-conditional_test-problem-SampleProblem', 'depends': u'i4x-edX-conditional_test-problem-SampleProblem',
}) })
self.assertEquals(expected, html) self.assertEquals(expected, html)
...@@ -225,9 +225,9 @@ class ConditionalModuleXmlTest(unittest.TestCase): ...@@ -225,9 +225,9 @@ class ConditionalModuleXmlTest(unittest.TestCase):
{ {
# Test ajax url is just usage-id / handler_name # Test ajax url is just usage-id / handler_name
'ajax_url': 'i4x://HarvardX/ER22x/conditional/condone/xmodule_handler', 'ajax_url': 'i4x://HarvardX/ER22x/conditional/condone/xmodule_handler',
'element_id': 'i4x-HarvardX-ER22x-conditional-condone', 'element_id': u'i4x-HarvardX-ER22x-conditional-condone',
'id': 'i4x://HarvardX/ER22x/conditional/condone', 'id': u'i4x://HarvardX/ER22x/conditional/condone',
'depends': 'i4x-HarvardX-ER22x-problem-choiceprob' 'depends': u'i4x-HarvardX-ER22x-problem-choiceprob'
} }
) )
self.assertEqual(html, html_expect) self.assertEqual(html, html_expect)
......
...@@ -223,7 +223,7 @@ class XModuleMixin(XBlockMixin): ...@@ -223,7 +223,7 @@ class XModuleMixin(XBlockMixin):
try: try:
child = self.runtime.get_block(child_loc) child = self.runtime.get_block(child_loc)
except ItemNotFoundError: except ItemNotFoundError:
log.exception('Unable to load item {loc}, skipping'.format(loc=child_loc)) log.exception(u'Unable to load item {loc}, skipping'.format(loc=child_loc))
continue continue
self._child_instances.append(child) self._child_instances.append(child)
...@@ -538,7 +538,6 @@ class ResourceTemplates(object): ...@@ -538,7 +538,6 @@ class ResourceTemplates(object):
template = yaml.safe_load(template_content) template = yaml.safe_load(template_content)
template['template_id'] = template_file template['template_id'] = template_file
templates.append(template) templates.append(template)
return templates return templates
@classmethod @classmethod
...@@ -546,7 +545,7 @@ class ResourceTemplates(object): ...@@ -546,7 +545,7 @@ class ResourceTemplates(object):
if getattr(cls, 'template_dir_name', None): if getattr(cls, 'template_dir_name', None):
dirname = os.path.join('templates', cls.template_dir_name) dirname = os.path.join('templates', cls.template_dir_name)
if not resource_isdir(__name__, dirname): if not resource_isdir(__name__, dirname):
log.warning("No resource directory {dir} found when loading {cls_name} templates".format( log.warning(u"No resource directory {dir} found when loading {cls_name} templates".format(
dir=dirname, dir=dirname,
cls_name=cls.__name__, cls_name=cls.__name__,
)) ))
......
...@@ -677,5 +677,5 @@ def _statsd_tag(course_title): ...@@ -677,5 +677,5 @@ def _statsd_tag(course_title):
""" """
Calculate the tag we will use for DataDog. Calculate the tag we will use for DataDog.
""" """
tag = "course_email:{0}".format(course_title) tag = u"course_email:{0}".format(course_title)
return tag[:200] return tag[:200]
...@@ -343,7 +343,7 @@ def _dispatch(table, action, user, obj): ...@@ -343,7 +343,7 @@ def _dispatch(table, action, user, obj):
action) action)
return result return result
raise ValueError("Unknown action for object type '{0}': '{1}'".format( raise ValueError(u"Unknown action for object type '{0}': '{1}'".format(
type(obj), action)) type(obj), action))
......
...@@ -321,10 +321,10 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours ...@@ -321,10 +321,10 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
org, course_num, run = course_id.split("/") org, course_num, run = course_id.split("/")
tags = [ tags = [
"org:{0}".format(org), u"org:{0}".format(org),
"course:{0}".format(course_num), u"course:{0}".format(course_num),
"run:{0}".format(run), u"run:{0}".format(run),
"score_bucket:{0}".format(score_bucket) u"score_bucket:{0}".format(score_bucket)
] ]
if grade_bucket_type is not None: if grade_bucket_type is not None:
...@@ -443,7 +443,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours ...@@ -443,7 +443,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
make_psychometrics_data_update_handler(course_id, user, descriptor.location.url()) make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())
) )
system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id)) system.set(u'user_is_staff', has_access(user, descriptor.location, u'staff', course_id))
# make an ErrorDescriptor -- assuming that the descriptor's system is ok # make an ErrorDescriptor -- assuming that the descriptor's system is ok
if has_access(user, descriptor.location, 'staff', course_id): if has_access(user, descriptor.location, 'staff', course_id):
......
...@@ -238,7 +238,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -238,7 +238,7 @@ def index(request, course_id, chapter=None, section=None,
registered = registered_for_course(course, user) registered = registered_for_course(course, user)
if not registered: if not registered:
# TODO (vshnayder): do course instructors need to be registered to see course? # TODO (vshnayder): do course instructors need to be registered to see course?
log.debug('User %s tried to view course %s but is not enrolled' % (user, course.location.url())) log.debug(u'User {0} tried to view course {1} but is not enrolled'.format(user, course.location.url()))
return redirect(reverse('about_course', args=[course.id])) return redirect(reverse('about_course', args=[course.id]))
masq = setup_masquerade(request, staff_access) masq = setup_masquerade(request, staff_access)
...@@ -249,8 +249,8 @@ def index(request, course_id, chapter=None, section=None, ...@@ -249,8 +249,8 @@ def index(request, course_id, chapter=None, section=None,
course_module = get_module_for_descriptor(user, request, course, field_data_cache, course.id) course_module = get_module_for_descriptor(user, request, course, field_data_cache, course.id)
if course_module is None: if course_module is None:
log.warning('If you see this, something went wrong: if we got this' log.warning(u'If you see this, something went wrong: if we got this'
' far, should have gotten a course module for this user') u' far, should have gotten a course module for this user')
return redirect(reverse('about_course', args=[course.id])) return redirect(reverse('about_course', args=[course.id]))
if chapter is None: if chapter is None:
...@@ -424,9 +424,9 @@ def jump_to(request, course_id, location): ...@@ -424,9 +424,9 @@ def jump_to(request, course_id, location):
try: try:
(course_id, chapter, section, position) = path_to_location(modulestore(), course_id, location) (course_id, chapter, section, position) = path_to_location(modulestore(), course_id, location)
except ItemNotFoundError: except ItemNotFoundError:
raise Http404("No data at this location: {0}".format(location)) raise Http404(u"No data at this location: {0}".format(location))
except NoPathToItem: except NoPathToItem:
raise Http404("This location is not in any class: {0}".format(location)) raise Http404(u"This location is not in any class: {0}".format(location))
# choose the appropriate view (and provide the necessary args) based on the # choose the appropriate view (and provide the necessary args) based on the
# args provided by the redirect. # args provided by the redirect.
......
...@@ -15,7 +15,8 @@ def cached_has_permission(user, permission, course_id=None): ...@@ -15,7 +15,8 @@ def cached_has_permission(user, permission, course_id=None):
Call has_permission if it's not cached. A change in a user's role or Call has_permission if it's not cached. A change in a user's role or
a role's permissions will only become effective after CACHE_LIFESPAN seconds. a role's permissions will only become effective after CACHE_LIFESPAN seconds.
""" """
key = "permission_%d_%s_%s" % (user.id, str(course_id), permission) key = u"permission_{user_id:d}_{course_id}_{permission}".format(
user_id=user.id, course_id=course_id, permission=permission)
val = CACHE.get(key, None) val = CACHE.get(key, None)
if val not in [True, False]: if val not in [True, False]:
val = has_permission(user, permission, course_id=course_id) val = has_permission(user, permission, course_id=course_id)
......
...@@ -173,7 +173,7 @@ def instructor_dashboard(request, course_id): ...@@ -173,7 +173,7 @@ def instructor_dashboard(request, course_id):
# complete the url using information about the current course: # complete the url using information about the current course:
(org, course_name, _) = course_id.split("/") (org, course_name, _) = course_id.split("/")
return "i4x://" + org + "/" + course_name + "/" + urlname return u"i4x://{org}/{name}/{url}".format(org=org, name=course_name, url=urlname)
def get_student_from_identifier(unique_student_identifier): def get_student_from_identifier(unique_student_identifier):
"""Gets a student object using either an email address or username""" """Gets a student object using either an email address or username"""
...@@ -782,7 +782,7 @@ def instructor_dashboard(request, course_id): ...@@ -782,7 +782,7 @@ def instructor_dashboard(request, course_id):
logs and swallows errors. logs and swallows errors.
""" """
url = settings.ANALYTICS_SERVER_URL + \ url = settings.ANALYTICS_SERVER_URL + \
"get?aname={}&course_id={}&apikey={}".format(analytics_name, u"get?aname={}&course_id={}&apikey={}".format(analytics_name,
course_id, course_id,
settings.ANALYTICS_API_KEY) settings.ANALYTICS_API_KEY)
try: try:
......
...@@ -66,7 +66,7 @@ def staff_grading_notifications(course, user): ...@@ -66,7 +66,7 @@ def staff_grading_notifications(course, user):
def peer_grading_notifications(course, user): def peer_grading_notifications(course, user):
system = LmsModuleSystem( system = LmsModuleSystem(
track_function=None, track_function=None,
get_module = None, get_module=None,
render_template=render_to_string, render_template=render_to_string,
replace_urls=None, replace_urls=None,
) )
...@@ -115,7 +115,7 @@ def combined_notifications(course, user): ...@@ -115,7 +115,7 @@ def combined_notifications(course, user):
#Set up return values so that we can return them for error cases #Set up return values so that we can return them for error cases
pending_grading = False pending_grading = False
img_path = "" img_path = ""
notifications={} notifications = {}
notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications} notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications}
#We don't want to show anonymous users anything. #We don't want to show anonymous users anything.
...@@ -126,7 +126,7 @@ def combined_notifications(course, user): ...@@ -126,7 +126,7 @@ def combined_notifications(course, user):
system = LmsModuleSystem( system = LmsModuleSystem(
static_url="/static", static_url="/static",
track_function=None, track_function=None,
get_module = None, get_module=None,
render_template=render_to_string, render_template=render_to_string,
replace_urls=None, replace_urls=None,
) )
...@@ -159,7 +159,7 @@ def combined_notifications(course, user): ...@@ -159,7 +159,7 @@ def combined_notifications(course, user):
#Non catastrophic error, so no real action #Non catastrophic error, so no real action
#This is a dev_facing_error #This is a dev_facing_error
log.exception( log.exception(
"Problem with getting notifications from controller query service for course {0} user {1}.".format( u"Problem with getting notifications from controller query service for course {0} user {1}.".format(
course_id, student_id)) course_id, student_id))
if pending_grading: if pending_grading:
...@@ -185,7 +185,7 @@ def set_value_in_cache(student_id, course_id, notification_type, value): ...@@ -185,7 +185,7 @@ def set_value_in_cache(student_id, course_id, notification_type, value):
def create_key_name(student_id, course_id, notification_type): def create_key_name(student_id, course_id, notification_type):
key_name = "{prefix}{type}_{course}_{student}".format(prefix=KEY_PREFIX, type=notification_type, course=course_id, key_name = u"{prefix}{type}_{course}_{student}".format(prefix=KEY_PREFIX, type=notification_type, course=course_id,
student=student_id) student=student_id)
return key_name return key_name
......
...@@ -66,7 +66,6 @@ class MockStaffGradingService(object): ...@@ -66,7 +66,6 @@ class MockStaffGradingService(object):
'min_for_ml': 10}) 'min_for_ml': 10})
]}) ]})
def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores, def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores,
submission_flagged): submission_flagged):
return self.get_next(course_id, 'fake location', grader_id) return self.get_next(course_id, 'fake location', grader_id)
...@@ -81,7 +80,7 @@ class StaffGradingService(GradingService): ...@@ -81,7 +80,7 @@ class StaffGradingService(GradingService):
config['system'] = LmsModuleSystem( config['system'] = LmsModuleSystem(
static_url='/static', static_url='/static',
track_function=None, track_function=None,
get_module = None, get_module=None,
render_template=render_to_string, render_template=render_to_string,
replace_urls=None, replace_urls=None,
) )
...@@ -93,7 +92,6 @@ class StaffGradingService(GradingService): ...@@ -93,7 +92,6 @@ class StaffGradingService(GradingService):
self.get_problem_list_url = self.url + '/get_problem_list/' self.get_problem_list_url = self.url + '/get_problem_list/'
self.get_notifications_url = self.url + "/get_notifications/" self.get_notifications_url = self.url + "/get_notifications/"
def get_problem_list(self, course_id, grader_id): def get_problem_list(self, course_id, grader_id):
""" """
Get the list of problems for a given course. Get the list of problems for a given course.
...@@ -113,7 +111,6 @@ class StaffGradingService(GradingService): ...@@ -113,7 +111,6 @@ class StaffGradingService(GradingService):
params = {'course_id': course_id, 'grader_id': grader_id} params = {'course_id': course_id, 'grader_id': grader_id}
return self.get(self.get_problem_list_url, params) return self.get(self.get_problem_list_url, params)
def get_next(self, course_id, location, grader_id): def get_next(self, course_id, location, grader_id):
""" """
Get the next thing to grade. Get the next thing to grade.
...@@ -137,7 +134,6 @@ class StaffGradingService(GradingService): ...@@ -137,7 +134,6 @@ class StaffGradingService(GradingService):
'grader_id': grader_id}) 'grader_id': grader_id})
return json.dumps(self._render_rubric(response)) return json.dumps(self._render_rubric(response))
def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores, def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores,
submission_flagged): submission_flagged):
""" """
......
...@@ -35,7 +35,7 @@ def quote_slashes(text): ...@@ -35,7 +35,7 @@ def quote_slashes(text):
';;'. By making the escape sequence fixed length, and escaping ';;'. By making the escape sequence fixed length, and escaping
identifier character ';', we are able to reverse the escaping. identifier character ';', we are able to reverse the escaping.
""" """
return re.sub(r'[;/]', _quote_slashes, text) return re.sub(ur'[;/]', _quote_slashes, text)
def _unquote_slashes(match): def _unquote_slashes(match):
...@@ -84,7 +84,7 @@ def handler_url(course_id, block, handler, suffix='', query='', thirdparty=False ...@@ -84,7 +84,7 @@ def handler_url(course_id, block, handler, suffix='', query='', thirdparty=False
url = reverse(view_name, kwargs={ url = reverse(view_name, kwargs={
'course_id': course_id, 'course_id': course_id,
'usage_id': quote_slashes(str(block.scope_ids.usage_id)), 'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')),
'handler': handler, 'handler': handler,
'suffix': suffix, 'suffix': suffix,
}) })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment