diff --git a/.gitignore b/.gitignore index b13a128a63..8fb170c30f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ cover_html/ .idea/ .redcar/ chromedriver.log +/nbproject ghostdriver.log diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 2597690572..05d26a69ca 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -310,11 +310,26 @@ class CapaModule(XModule): is_survey_question = (self.max_attempts == 0) needs_reset = self.is_completed() and self.rerandomize == "always" + # If the student has unlimited attempts, and their answers + # are not randomized, then we do not need a save button + # because they can use the "Check" button without consequences. + # + # The consequences we want to avoid are: + # * Using up an attempt (if max_attempts is set) + # * Changing the current problem, and no longer being + # able to view it (if rerandomize is "always") + # + # In those cases. the if statement below is false, + # and the save button can still be displayed. + # + if self.max_attempts is None and self.rerandomize != "always": + return False + # If the problem is closed (and not a survey question with max_attempts==0), - # then do NOT show the reset button + # then do NOT show the save button # If we're waiting for the user to reset a randomized problem - # then do NOT show the reset button - if (self.closed() and not is_survey_question) or needs_reset: + # then do NOT show the save button + elif (self.closed() and not is_survey_question) or needs_reset: return False else: return True @@ -403,7 +418,7 @@ class CapaModule(XModule): # if we want to show a check button, and False otherwise # This works because non-empty strings evaluate to True if self.should_show_check_button(): - check_button = self.check_button_name() + check_button = self.check_button_name() else: check_button = False @@ -556,7 +571,7 @@ class CapaModule(XModule): else: answers = self.lcp.get_question_answers() - # answers (eg ) may have embedded images + # answers (eg ) may have embedded images # but be careful, some problems are using non-string answer dicts new_answers = dict() for answer_id in answers: @@ -606,7 +621,7 @@ class CapaModule(XModule): to 'input_1' in the returned dict) ''' answers = dict() - + for key in get: # e.g. input_resistor_1 ==> resistor_1 _, _, name = key.partition('_') @@ -729,7 +744,7 @@ class CapaModule(XModule): event_info['answers'] = answers # Too late. Cannot submit - if self.closed() and not self.max_attempts==0: + if self.closed() and not self.max_attempts ==0: event_info['failure'] = 'closed' self.system.track_function('save_problem_fail', event_info) return {'success': False, @@ -747,7 +762,7 @@ class CapaModule(XModule): self.system.track_function('save_problem_success', event_info) msg = "Your answers have been saved" - if not self.max_attempts==0: + if not self.max_attempts ==0: msg += " but not graded. Hit 'Check' to grade them." return {'success': True, 'msg': msg} @@ -784,7 +799,7 @@ class CapaModule(XModule): # reset random number generator seed (note the self.lcp.get_state() # in next line) self.lcp.seed = None - + self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), self.lcp.get_state(), @@ -793,7 +808,7 @@ class CapaModule(XModule): event_info['new_state'] = self.lcp.get_state() self.system.track_function('reset_problem', event_info) - return { 'success': True, + return {'success': True, 'html': self.get_problem_html(encapsulate=False)} @@ -821,13 +836,13 @@ class CapaDescriptor(RawDescriptor): def get_context(self): _context = RawDescriptor.get_context(self) _context.update({'markdown': self.metadata.get('markdown', ''), - 'enable_markdown' : 'markdown' in self.metadata}) + 'enable_markdown': 'markdown' in self.metadata}) return _context @property def editable_metadata_fields(self): """Remove any metadata from the editable fields which have their own editor or shouldn't be edited by user.""" - subset = [field for field in super(CapaDescriptor,self).editable_metadata_fields + subset = [field for field in super(CapaDescriptor, self).editable_metadata_fields if field not in ['markdown', 'empty']] return subset diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py index 37255bd5cb..88e29b4203 100644 --- a/common/lib/xmodule/xmodule/foldit_module.py +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -87,7 +87,7 @@ class FolditModule(XModule): from foldit.models import Score leaders = [(e['username'], e['score']) for e in Score.get_tops_n(10)] - leaders.sort(key=lambda x: x[1]) + leaders.sort(key=lambda x: -x[1]) return leaders diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index cb77921957..19e17db1ab 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -356,7 +356,7 @@ class CapaModuleTest(unittest.TestCase): valid_get_dict = self._querydict_from_dict({'input_2[]': ['test1', 'test2']}) result = CapaModule.make_dict_of_responses(valid_get_dict) self.assertTrue('2' in result) - self.assertEqual(['test1','test2'], result['2']) + self.assertEqual(['test1', 'test2'], result['2']) # If we use [] at the end of a key name, we should always # get a list, even if there's just one value @@ -374,7 +374,7 @@ class CapaModuleTest(unittest.TestCase): # One of the values would overwrite the other, so detect this # and raise an exception invalid_get_dict = self._querydict_from_dict({'input_1[]': 'test 1', - 'input_1': 'test 2' }) + 'input_1': 'test 2'}) with self.assertRaises(ValueError): result = CapaModule.make_dict_of_responses(invalid_get_dict) @@ -412,7 +412,7 @@ class CapaModuleTest(unittest.TestCase): mock_html.return_value = "Test HTML" # Check the problem - get_request_dict = { CapaFactory.input_key(): '3.14' } + get_request_dict = { CapaFactory.input_key(): '3.14'} result = module.check_problem(get_request_dict) # Expect that the problem is marked correct @@ -434,7 +434,7 @@ class CapaModuleTest(unittest.TestCase): mock_is_correct.return_value = False # Check the problem - get_request_dict = { CapaFactory.input_key(): '0' } + get_request_dict = { CapaFactory.input_key(): '0'} result = module.check_problem(get_request_dict) # Expect that the problem is marked correct @@ -452,7 +452,7 @@ class CapaModuleTest(unittest.TestCase): with patch('xmodule.capa_module.CapaModule.closed') as mock_closed: mock_closed.return_value = True with self.assertRaises(xmodule.exceptions.NotFoundError): - get_request_dict = { CapaFactory.input_key(): '3.14' } + get_request_dict = { CapaFactory.input_key(): '3.14'} module.check_problem(get_request_dict) # Expect that number of attempts NOT incremented @@ -468,7 +468,7 @@ class CapaModuleTest(unittest.TestCase): # Expect that we cannot submit with self.assertRaises(xmodule.exceptions.NotFoundError): - get_request_dict = { CapaFactory.input_key(): '3.14' } + get_request_dict = { CapaFactory.input_key(): '3.14'} module.check_problem(get_request_dict) # Expect that number of attempts NOT incremented @@ -483,7 +483,7 @@ class CapaModuleTest(unittest.TestCase): module.lcp.done = True # Expect that we can submit successfully - get_request_dict = { CapaFactory.input_key(): '3.14' } + get_request_dict = { CapaFactory.input_key(): '3.14'} result = module.check_problem(get_request_dict) self.assertEqual(result['success'], 'correct') @@ -504,7 +504,7 @@ class CapaModuleTest(unittest.TestCase): mock_is_queued.return_value = True mock_get_queuetime.return_value = datetime.datetime.now() - get_request_dict = { CapaFactory.input_key(): '3.14' } + get_request_dict = { CapaFactory.input_key(): '3.14'} result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' @@ -521,7 +521,7 @@ class CapaModuleTest(unittest.TestCase): with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') - get_request_dict = { CapaFactory.input_key(): '3.14' } + get_request_dict = { CapaFactory.input_key(): '3.14'} result = module.check_problem(get_request_dict) # Expect an AJAX alert message in 'success' @@ -595,11 +595,11 @@ class CapaModuleTest(unittest.TestCase): module.lcp.done = False # Save the problem - get_request_dict = { CapaFactory.input_key(): '3.14' } + get_request_dict = { CapaFactory.input_key(): '3.14'} result = module.save_problem(get_request_dict) # Expect that answers are saved to the problem - expected_answers = { CapaFactory.answer_key(): '3.14' } + expected_answers = { CapaFactory.answer_key(): '3.14'} self.assertEqual(module.lcp.student_answers, expected_answers) # Expect that the result is success @@ -617,7 +617,7 @@ class CapaModuleTest(unittest.TestCase): mock_closed.return_value = True # Try to save the problem - get_request_dict = { CapaFactory.input_key(): '3.14' } + get_request_dict = { CapaFactory.input_key(): '3.14'} result = module.save_problem(get_request_dict) # Expect that the result is failure @@ -631,7 +631,7 @@ class CapaModuleTest(unittest.TestCase): module.lcp.done = True # Try to save - get_request_dict = { CapaFactory.input_key(): '3.14' } + get_request_dict = { CapaFactory.input_key(): '3.14'} result = module.save_problem(get_request_dict) # Expect that we cannot save @@ -645,7 +645,7 @@ class CapaModuleTest(unittest.TestCase): module.lcp.done = True # Try to save - get_request_dict = { CapaFactory.input_key(): '3.14' } + get_request_dict = { CapaFactory.input_key(): '3.14'} result = module.save_problem(get_request_dict) # Expect that we succeed @@ -657,7 +657,7 @@ class CapaModuleTest(unittest.TestCase): # Just in case, we also check what happens if we have # more attempts than allowed. attempts = random.randint(1, 10) - module = CapaFactory.create(attempts=attempts-1, max_attempts=attempts) + module = CapaFactory.create(attempts=attempts -1, max_attempts=attempts) self.assertEqual(module.check_button_name(), "Final Check") module = CapaFactory.create(attempts=attempts, max_attempts=attempts) @@ -667,14 +667,14 @@ class CapaModuleTest(unittest.TestCase): self.assertEqual(module.check_button_name(), "Final Check") # Otherwise, button name is "Check" - module = CapaFactory.create(attempts=attempts-2, max_attempts=attempts) + module = CapaFactory.create(attempts=attempts -2, max_attempts=attempts) self.assertEqual(module.check_button_name(), "Check") - module = CapaFactory.create(attempts=attempts-3, max_attempts=attempts) + module = CapaFactory.create(attempts=attempts -3, max_attempts=attempts) self.assertEqual(module.check_button_name(), "Check") # If no limit on attempts, then always show "Check" - module = CapaFactory.create(attempts=attempts-3) + module = CapaFactory.create(attempts=attempts -3) self.assertEqual(module.check_button_name(), "Check") module = CapaFactory.create(attempts=0) @@ -682,7 +682,7 @@ class CapaModuleTest(unittest.TestCase): def test_should_show_check_button(self): - attempts = random.randint(1,10) + attempts = random.randint(1, 10) # If we're after the deadline, do NOT show check button module = CapaFactory.create(due=self.yesterday_str) @@ -718,7 +718,7 @@ class CapaModuleTest(unittest.TestCase): def test_should_show_reset_button(self): - attempts = random.randint(1,10) + attempts = random.randint(1, 10) # If we're after the deadline, do NOT show the reset button module = CapaFactory.create(due=self.yesterday_str) @@ -755,7 +755,7 @@ class CapaModuleTest(unittest.TestCase): def test_should_show_save_button(self): - attempts = random.randint(1,10) + attempts = random.randint(1, 10) # If we're after the deadline, do NOT show the save button module = CapaFactory.create(due=self.yesterday_str) @@ -772,13 +772,24 @@ class CapaModuleTest(unittest.TestCase): module.lcp.done = True self.assertFalse(module.should_show_save_button()) + # If the user has unlimited attempts and we are not randomizing, + # then do NOT show a save button + # because they can keep using "Check" + module = CapaFactory.create(max_attempts=None, rerandomize="never") + module.lcp.done = False + self.assertFalse(module.should_show_save_button()) + + module = CapaFactory.create(max_attempts=None, rerandomize="never") + module.lcp.done = True + self.assertFalse(module.should_show_save_button()) + # Otherwise, DO show the save button module = CapaFactory.create() module.lcp.done = False self.assertTrue(module.should_show_save_button()) - # If we're not randomizing, then we can re-save - module = CapaFactory.create(rerandomize="never") + # If we're not randomizing and we have limited attempts, then we can save + module = CapaFactory.create(rerandomize="never", max_attempts=2) module.lcp.done = True self.assertTrue(module.should_show_save_button()) @@ -797,7 +808,7 @@ class CapaModuleTest(unittest.TestCase): self.assertFalse(module.should_show_save_button()) # If the user is out of attempts, do NOT show the save button - attempts = random.randint(1,10) + attempts = random.randint(1, 10) module = CapaFactory.create(attempts=attempts, max_attempts=attempts, force_save_button="true") @@ -823,9 +834,9 @@ class CapaModuleTest(unittest.TestCase): # We've tested the show/hide button logic in other tests, # so here we hard-wire the values - show_check_button = bool(random.randint(0,1) % 2) - show_reset_button = bool(random.randint(0,1) % 2) - show_save_button = bool(random.randint(0,1) % 2) + show_check_button = bool(random.randint(0, 1) % 2) + show_reset_button = bool(random.randint(0, 1) % 2) + show_save_button = bool(random.randint(0, 1) % 2) module.should_show_check_button = Mock(return_value=show_check_button) module.should_show_reset_button = Mock(return_value=show_reset_button) @@ -848,7 +859,7 @@ class CapaModuleTest(unittest.TestCase): self.assertEqual(html, "
Test Template HTML
") # Check the rendering context - render_args,_ = module.system.render_template.call_args + render_args, _ = module.system.render_template.call_args self.assertEqual(len(render_args), 2) template_name = render_args[0] @@ -889,7 +900,7 @@ class CapaModuleTest(unittest.TestCase): html = module.get_problem_html() # Check the rendering context - render_args,_ = module.system.render_template.call_args + render_args, _ = module.system.render_template.call_args context = render_args[1] self.assertTrue("error" in context['problem']['html']) diff --git a/common/static/coffee/src/discussion/content.coffee b/common/static/coffee/src/discussion/content.coffee index 4e612dfc40..00c34df686 100644 --- a/common/static/coffee/src/discussion/content.coffee +++ b/common/static/coffee/src/discussion/content.coffee @@ -79,6 +79,17 @@ if Backbone? @getContent(id).updateInfo(info) $.extend @contentInfos, infos + pinThread: -> + pinned = @get("pinned") + @set("pinned",pinned) + @trigger "change", @ + + unPinThread: -> + pinned = @get("pinned") + @set("pinned",pinned) + @trigger "change", @ + + class @Thread extends @Content urlMappers: 'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id) @@ -91,6 +102,8 @@ if Backbone? 'delete' : -> DiscussionUtil.urlFor('delete_thread', @id) 'follow' : -> DiscussionUtil.urlFor('follow_thread', @id) 'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id) + 'pinThread' : -> DiscussionUtil.urlFor("pin_thread", @id) + 'unPinThread' : -> DiscussionUtil.urlFor("un_pin_thread", @id) initialize: -> @set('thread', @) diff --git a/common/static/coffee/src/discussion/discussion.coffee b/common/static/coffee/src/discussion/discussion.coffee index 9cee068b74..83e25e1da7 100644 --- a/common/static/coffee/src/discussion/discussion.coffee +++ b/common/static/coffee/src/discussion/discussion.coffee @@ -58,10 +58,31 @@ if Backbone? @current_page = response.page sortByDate: (thread) -> - thread.get("created_at") + # + #The comment client asks each thread for a value by which to sort the collection + #and calls this sort routine regardless of the order returned from the LMS/comments service + #so, this takes advantage of this per-thread value and returns tomorrow's date + #for pinned threads, ensuring that they appear first, (which is the intent of pinned threads) + # + if thread.get('pinned') + #use tomorrow's date + today = new Date(); + new Date(today.getTime() + (24 * 60 * 60 * 1000)); + else + thread.get("created_at") + sortByDateRecentFirst: (thread) -> - -(new Date(thread.get("created_at")).getTime()) + # + #Same as above + #but negative to flip the order (newest first) + # + if thread.get('pinned') + #use tomorrow's date + today = new Date(); + -(new Date(today.getTime() + (24 * 60 * 60 * 1000))); + else + -(new Date(thread.get("created_at")).getTime()) #return String.fromCharCode.apply(String, # _.map(thread.get("created_at").split(""), # ((c) -> return 0xffff - c.charChodeAt())) diff --git a/common/static/coffee/src/discussion/utils.coffee b/common/static/coffee/src/discussion/utils.coffee index 6b2714dc54..41f52f1711 100644 --- a/common/static/coffee/src/discussion/utils.coffee +++ b/common/static/coffee/src/discussion/utils.coffee @@ -50,6 +50,8 @@ class @DiscussionUtil delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete" upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote" downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote" + pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/pin" + un_pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unpin" undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote" follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow" unfollow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unfollow" diff --git a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee index 6320c3d1e3..56525af347 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee @@ -3,6 +3,7 @@ if Backbone? events: "click .discussion-vote": "toggleVote" + "click .admin-pin": "togglePin" "click .action-follow": "toggleFollowing" "click .action-edit": "edit" "click .action-delete": "delete" @@ -24,6 +25,7 @@ if Backbone? @delegateEvents() @renderDogear() @renderVoted() + @renderPinned() @renderAttrs() @$("span.timeago").timeago() @convertMath() @@ -41,8 +43,20 @@ if Backbone? else @$("[data-role=discussion-vote]").removeClass("is-cast") + renderPinned: => + if @model.get("pinned") + @$("[data-role=thread-pin]").addClass("pinned") + @$("[data-role=thread-pin]").removeClass("notpinned") + @$(".discussion-pin .pin-label").html("Pinned") + else + @$("[data-role=thread-pin]").removeClass("pinned") + @$("[data-role=thread-pin]").addClass("notpinned") + @$(".discussion-pin .pin-label").html("Pin Thread") + + updateModelDetails: => @renderVoted() + @renderPinned() @$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"]) convertMath: -> @@ -99,6 +113,36 @@ if Backbone? delete: (event) -> @trigger "thread:delete", event + togglePin: (event) -> + event.preventDefault() + if @model.get('pinned') + @unPin() + else + @pin() + + pin: -> + url = @model.urlFor("pinThread") + DiscussionUtil.safeAjax + $elem: @$(".discussion-pin") + url: url + type: "POST" + success: (response, textStatus) => + if textStatus == 'success' + @model.set('pinned', true) + error: => + $('.admin-pin').text("Pinning not currently available") + + unPin: -> + url = @model.urlFor("unPinThread") + DiscussionUtil.safeAjax + $elem: @$(".discussion-pin") + url: url + type: "POST" + success: (response, textStatus) => + if textStatus == 'success' + @model.set('pinned', false) + + toggleClosed: (event) -> $elem = $(event.target) url = @model.urlFor('close') @@ -137,3 +181,5 @@ if Backbone? if @model.get('username')? params = $.extend(params, user:{username: @model.username, user_url: @model.user_url}) Mustache.render(@template, params) + + \ No newline at end of file diff --git a/common/static/js/capa/genex/2DDA730EDABB80B88A6B0DFA3AFEACA2.cache.html b/common/static/js/capa/genex/3F4ADBED36D589545A9300A1EA686D36.cache.html similarity index 91% rename from common/static/js/capa/genex/2DDA730EDABB80B88A6B0DFA3AFEACA2.cache.html rename to common/static/js/capa/genex/3F4ADBED36D589545A9300A1EA686D36.cache.html index 743492768b..c5ad0d1b89 100644 --- a/common/static/js/capa/genex/2DDA730EDABB80B88A6B0DFA3AFEACA2.cache.html +++ b/common/static/js/capa/genex/3F4ADBED36D589545A9300A1EA686D36.cache.html @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/common/static/js/capa/genex/4EEB1DCF4B30D366C27968D1B5C0BD04.cache.html b/common/static/js/capa/genex/FF175D5583BDD5ACF40C7F0AFF9A374B.cache.html similarity index 87% rename from common/static/js/capa/genex/4EEB1DCF4B30D366C27968D1B5C0BD04.cache.html rename to common/static/js/capa/genex/FF175D5583BDD5ACF40C7F0AFF9A374B.cache.html index 4aa12e55d4..ca07bf3292 100644 --- a/common/static/js/capa/genex/4EEB1DCF4B30D366C27968D1B5C0BD04.cache.html +++ b/common/static/js/capa/genex/FF175D5583BDD5ACF40C7F0AFF9A374B.cache.html @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/common/static/js/capa/genex/genex.css b/common/static/js/capa/genex/genex.css index a05f31110b..459c854f92 100644 --- a/common/static/js/capa/genex/genex.css +++ b/common/static/js/capa/genex/genex.css @@ -57,10 +57,15 @@ pre, #dna-strand { background: white; } .gwt-DialogBox .dialogBottomCenter { + background: url(images/hborder.png) repeat-x 0px -2945px; + -background: url(images/hborder_ie6.png) repeat-x 0px -2144px; } .gwt-DialogBox .dialogMiddleLeft { + background: url(images/vborder.png) repeat-y -31px 0px; } .gwt-DialogBox .dialogMiddleRight { + background: url(images/vborder.png) repeat-y -32px 0px; + -background: url(images/vborder_ie6.png) repeat-y -32px 0px; } .gwt-DialogBox .dialogTopLeftInner { width: 10px; @@ -82,12 +87,20 @@ pre, #dna-strand { zoom: 1; } .gwt-DialogBox .dialogTopLeft { + background: url(images/circles.png) no-repeat -20px 0px; + -background: url(images/circles_ie6.png) no-repeat -20px 0px; } .gwt-DialogBox .dialogTopRight { + background: url(images/circles.png) no-repeat -28px 0px; + -background: url(images/circles_ie6.png) no-repeat -28px 0px; } .gwt-DialogBox .dialogBottomLeft { + background: url(images/circles.png) no-repeat 0px -36px; + -background: url(images/circles_ie6.png) no-repeat 0px -36px; } .gwt-DialogBox .dialogBottomRight { + background: url(images/circles.png) no-repeat -8px -36px; + -background: url(images/circles_ie6.png) no-repeat -8px -36px; } * html .gwt-DialogBox .dialogTopLeftInner { width: 10px; diff --git a/common/static/js/capa/genex/genex.nocache.js b/common/static/js/capa/genex/genex.nocache.js index 07da038234..fe892a53dc 100644 --- a/common/static/js/capa/genex/genex.nocache.js +++ b/common/static/js/capa/genex/genex.nocache.js @@ -1,4 +1,4 @@ -function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='026A6180B5959B8660E084245FEE5E9E',Rb='1F433010E1134C95BF6CB43F552F3019',Sb='2DDA730EDABB80B88A6B0DFA3AFEACA2',Tb='4EEB1DCF4B30D366C27968D1B5C0BD04',Ub='5033ABB047340FB9346B622E2CC7107D',Wb=':',pb='::',dc='