diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0c61d1fb74fa..ed907c803152 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,20 +1,3 @@ - - ## Description Describe what this pull request changes, and why. Include implications for people using this change. diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml deleted file mode 100644 index fec11d6c259b..000000000000 --- a/.github/workflows/commitlint.yml +++ /dev/null @@ -1,10 +0,0 @@ -# Run commitlint on the commit messages in a pull request. - -name: Lint Commit Messages - -on: - - pull_request - -jobs: - commitlint: - uses: openedx/.github/.github/workflows/commitlint.yml@master diff --git a/.github/workflows/deploy-to-ec2-staging.yml b/.github/workflows/deploy-to-ec2-staging.yml new file mode 100644 index 000000000000..2925ddc17ea7 --- /dev/null +++ b/.github/workflows/deploy-to-ec2-staging.yml @@ -0,0 +1,54 @@ +name: Deploy-OpenEdx-to-Staging +on: + workflow_dispatch +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Send noti begining... + uses: 8398a7/action-slack@v3 + with: + status: custom + fields: workflow,job,message,commit,repo,ref,author,took + custom_payload: | + { + username: 'github-action', + icon_emoji: ':octocat:', + attachments: [{ + color: 'good', + text: `${process.env.AS_AUTHOR.split(/[<@]/)[1]} start deploying Openedx "${process.env.AS_MESSAGE}" in ${process.env.AS_REF.replace('refs/heads/', '')}`, + }] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + - name: Deploy docker image to EC2 + uses: appleboy/ssh-action@v0.1.4 + with: + host: ${{ secrets.STAG_IP }} + username: openedx + key: ${{ secrets.STAG_KEY }} + port: 22 + command_timeout: 100m + script: | + export PATH='/home/openedx/.local/bin:/home/openedx/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin' + docker ps + docker images + tutor images build openedx --no-cache + echo -e "y\n\n\n\n\n\ny" | tutor local launch + - name: Send noti + uses: 8398a7/action-slack@v3 + with: + status: custom + fields: workflow,job,message,commit,repo,ref,author,took + custom_payload: | + { + username: 'github-action', + icon_emoji: ':octocat:', + attachments: [{ + color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning', + text: `${process.env.AS_AUTHOR.split(/[<@]/)[1]} Deploy Openedx "${process.env.AS_MESSAGE}" in ${process.env.AS_REF.replace('refs/heads/', '')} ${'${{ job.status }}' === 'success' ? 'succeeded' : '${{ job.status }}' === 'failure' ? 'failed' : '${{ job.status }}'} in ${process.env.AS_TOOK}`, + }] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + if: always() # Pick up events even if the job fails or is canceled. diff --git a/.github/workflows/deploy-to-ec2.yml b/.github/workflows/deploy-to-ec2.yml new file mode 100644 index 000000000000..f4ab33c549e1 --- /dev/null +++ b/.github/workflows/deploy-to-ec2.yml @@ -0,0 +1,53 @@ +name: Deploy to AWS +on: + push: + branches: + - "dev" +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Send noti begining... + uses: 8398a7/action-slack@v3 + with: + status: custom + fields: workflow,job,message,commit,repo,ref,author,took + custom_payload: | + { + username: 'github-action', + icon_emoji: ':octocat:', + attachments: [{ + color: 'good', + text: `${process.env.AS_AUTHOR.split(/[<@]/)[1]} start deploying Openedx "${process.env.AS_MESSAGE}" in ${process.env.AS_REF.replace('refs/heads/', '')}`, + }] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + - name: Deploy docker image to EC2 + uses: appleboy/ssh-action@v0.1.4 + with: + host: ${{ secrets.DEV_IP }} + username: ${{ secrets.DEV_USERNAME }} + key: ${{ secrets.DEV_KEY }} + port: 22 + command_timeout: 100m + script: | + tutor images build openedx --no-cache + echo -e "y\n\n\n\n\n\ny" | tutor local launch + - name: Send noti + uses: 8398a7/action-slack@v3 + with: + status: custom + fields: workflow,job,message,commit,repo,ref,author,took + custom_payload: | + { + username: 'github-action', + icon_emoji: ':octocat:', + attachments: [{ + color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning', + text: `${process.env.AS_AUTHOR.split(/[<@]/)[1]} Deploy Openedx "${process.env.AS_MESSAGE}" in ${process.env.AS_REF.replace('refs/heads/', '')} ${'${{ job.status }}' === 'success' ? 'succeeded' : '${{ job.status }}' === 'failure' ? 'failed' : '${{ job.status }}'} in ${process.env.AS_TOOK}`, + }] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + if: always() # Pick up events even if the job fails or is canceled. diff --git a/.github/workflows/unit-tests-gh-hosted.yml b/.github/workflows/unit-tests-gh-hosted.yml index fd01feeccd99..5edfbff88f9d 100644 --- a/.github/workflows/unit-tests-gh-hosted.yml +++ b/.github/workflows/unit-tests-gh-hosted.yml @@ -9,7 +9,7 @@ on: jobs: run-test: - if: github.repository != 'openedx/edx-platform' && github.repository != 'edx/edx-platform-private' + if: (github.repository != 'openedx/edx-platform' && github.repository != 'edx/edx-platform-private') || (github.repository == 'openedx/edx-platform' && (startsWith(github.base_ref, 'open-release') == true)) runs-on: ubuntu-20.04 strategy: fail-fast: false @@ -31,7 +31,7 @@ jobs: "cms-2", "common-1", "common-2", - "common-3", + "xmodule-1" ] name: gh-hosted-python-${{ matrix.python-version }},django-${{ matrix.django-version }},${{ matrix.shard_name }} steps: @@ -75,7 +75,7 @@ jobs: uses: ./.github/actions/unit-tests collect-and-verify: - if: github.repository != 'openedx/edx-platform' && github.repository != 'edx/edx-platform-private' + if: (github.repository != 'openedx/edx-platform' && github.repository != 'edx/edx-platform-private') || (github.repository == 'openedx/edx-platform' && (startsWith(github.base_ref, 'open-release') == true)) runs-on: ubuntu-20.04 strategy: matrix: diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 51ae55569146..50b896977391 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -9,7 +9,7 @@ on: jobs: run-tests: name: python-${{ matrix.python-version }},django-${{ matrix.django-version }},${{ matrix.shard_name }} - if: github.repository == 'openedx/edx-platform' || github.repository == 'edx/edx-platform-private' + if: (github.repository == 'edx/edx-platform-private') || (github.repository == 'openedx/edx-platform' && (startsWith(github.base_ref, 'open-release') == false)) runs-on: [ edx-platform-runner ] strategy: matrix: @@ -80,7 +80,7 @@ jobs: # https://github.com/orgs/community/discussions/33579 success: name: Tests successful - if: always() + if: (github.repository == 'edx/edx-platform-private') || (github.repository == 'openedx/edx-platform' && (startsWith(github.base_ref, 'open-release') == false)) needs: - run-tests runs-on: ubuntu-latest diff --git a/.github/workflows/verify-gha-unit-tests-count.yml b/.github/workflows/verify-gha-unit-tests-count.yml index 039d45ad9548..c68092942d70 100644 --- a/.github/workflows/verify-gha-unit-tests-count.yml +++ b/.github/workflows/verify-gha-unit-tests-count.yml @@ -8,7 +8,7 @@ on: jobs: collect-and-verify: - if: github.repository == 'openedx/edx-platform' || github.repository == 'edx/edx-platform-private' + if: (github.repository == 'edx/edx-platform-private') || (github.repository == 'openedx/edx-platform' && (startsWith(github.base_ref, 'open-release') == false)) runs-on: [ edx-platform-runner ] steps: - name: sync directory owner diff --git a/.tx/config b/.tx/config index 5ab82585d37b..f9b8d235598e 100644 --- a/.tx/config +++ b/.tx/config @@ -67,3 +67,14 @@ source_file = conf/locale/en/LC_MESSAGES/wiki.po source_lang = en type = PO +[o:open-edx:p:open-edx-releases:r:release-olive] +file_filter = conf/locale//LC_MESSAGES/django.po +source_file = conf/locale/en/LC_MESSAGES/django.po +source_lang = en +type = PO + +[o:open-edx:p:open-edx-releases:r:release-olive-js] +file_filter = conf/locale//LC_MESSAGES/djangojs.po +source_file = conf/locale/en/LC_MESSAGES/djangojs.po +source_lang = en +type = PO diff --git a/cms/djangoapps/contentstore/courseware_index.py b/cms/djangoapps/contentstore/courseware_index.py index d21f9b8ffc73..5ba9d676a260 100644 --- a/cms/djangoapps/contentstore/courseware_index.py +++ b/cms/djangoapps/contentstore/courseware_index.py @@ -49,7 +49,7 @@ def indexing_is_enabled(): """ Checks to see if the indexing feature is enabled """ - return settings.FEATURES.get('ENABLE_COURSEWARE_INDEX', False) + return settings.FEATURES.get('ENABLE_COURSEWARE_INDEX', True) class SearchIndexingError(Exception): diff --git a/cms/djangoapps/contentstore/migrations/0009_unit_time_course.py b/cms/djangoapps/contentstore/migrations/0009_unit_time_course.py new file mode 100644 index 000000000000..2c55e0daeb38 --- /dev/null +++ b/cms/djangoapps/contentstore/migrations/0009_unit_time_course.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.10 on 2023-18-10 11:09 + +from django.conf import settings +from django.db import migrations, models + + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('contentstore', '0008_cleanstalecertificateavailabilitydatesconfig'), + ] + + operations = [ + migrations.CreateModel( + name='CourseUnitTime', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('block_id' , models.CharField(max_length=255)), + ('course_id', models.CharField(max_length=255)), + ('display_name', models.CharField(max_length=255)), + ('total' , models.IntegerField(default=0)) + ], + ) + ] + + + + + + + + + diff --git a/cms/djangoapps/contentstore/models.py b/cms/djangoapps/contentstore/models.py index f3b39397cf99..d05eaade4b17 100644 --- a/cms/djangoapps/contentstore/models.py +++ b/cms/djangoapps/contentstore/models.py @@ -4,7 +4,8 @@ from config_models.models import ConfigurationModel -from django.db.models.fields import IntegerField, TextField +from django.db.models.fields import IntegerField, TextField,CharField +from django.db import models class VideoUploadConfig(ConfigurationModel): @@ -63,3 +64,44 @@ class Meta: "`clean_stale_certificate_available_dates` management command.' See the management command for options." ) ) + +# model course unit time + +# class CourseUnitTime (models.Model): +# block_id = CharField(max_length=255) +# course_id = CharField(max_length=255) +# display_name = CharField(max_length=255) +# total = IntegerField(default=0) + +# def __str__(self): +# return self.block_id + +# @classmethod +# def create_unit_time(self, course_id, block_id, display_name, total ): +# return CourseUnitTime.objects.create(course_id=course_id, block_id=block_id, display_name=display_name, total=total) + +# @classmethod +# def get_unit_time (self, block_id) : +# try: +# return CourseUnitTime.objects.filter( block_id=block_id)[0] +# except : +# return None + + +# @classmethod +# def set_unit_time (self, block_id, total , course_id, display_name): +# try: + +# total_obj = CourseUnitTime.objects.get(block_id=block_id , course_id = course_id) +# total_obj.total = total +# total_obj.save() +# except CourseUnitTime.DoesNotExist: +# return CourseUnitTime.objects.create(block_id=block_id, total=total, course_id=course_id, display_name=display_name) + +# @classmethod +# def remove_unit_time_seuqe (self, block_id): +# try: +# return CourseUnitTime.objects.filter(block_id=block_id).delete() +# except: +# return None + \ No newline at end of file diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index 44bfdea32908..76711eaa4ddc 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -145,7 +145,10 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable= if CoursewareSearchIndexer.indexing_is_enabled() and CourseAboutSearchIndexer.indexing_is_enabled(): update_search_index.delay(course_key_str, datetime.now(UTC).isoformat()) - update_discussions_settings_from_course_task.delay(course_key_str) + update_discussions_settings_from_course_task.apply_async( + args=[course_key_str], + countdown=settings.DISCUSSION_SETTINGS['COURSE_PUBLISH_TASK_DELAY'], + ) # Send to a signal for catalog info changes as well, but only once we know the transaction is committed. transaction.on_commit(lambda: emit_catalog_info_changed_signal(course_key)) diff --git a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py index bfee1d79b906..3f6af8512eee 100644 --- a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py +++ b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py @@ -1,8 +1,8 @@ """ Tests for transcripts_utils. """ - import copy import json +import re import tempfile import textwrap import unittest @@ -15,7 +15,7 @@ from django.test.utils import override_settings from django.utils import translation -from cms.djangoapps.contentstore.tests.utils import mock_requests_get +from cms.djangoapps.contentstore.tests.utils import setup_caption_responses from common.djangoapps.student.tests.factories import UserFactory from xmodule.contentstore.content import StaticContent # lint-amnesty, pylint: disable=wrong-import-order from xmodule.contentstore.django import contentstore # lint-amnesty, pylint: disable=wrong-import-order @@ -222,7 +222,7 @@ def clear_subs_content(self, youtube_subs): def test_success_downloading_subs(self): - response = textwrap.dedent(""" + caption_response_string = textwrap.dedent(""" Test text 1. @@ -233,12 +233,16 @@ def test_success_downloading_subs(self): good_youtube_sub = 'good_id_2' self.clear_sub_content(good_youtube_sub) + language_code = 'en' with patch('xmodule.video_module.transcripts_utils.requests.get') as mock_get: - mock_get.return_value = Mock(status_code=200, text=response, content=response.encode('utf-8')) - # Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown + setup_caption_responses(mock_get, language_code, caption_response_string) transcripts_utils.download_youtube_subs(good_youtube_sub, self.course, settings) - mock_get.assert_any_call('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_id_2'}) + self.assertEqual(2, len(mock_get.mock_calls)) + args, kwargs = mock_get.call_args_list[0] + self.assertEqual(args[0], 'https://www.youtube.com/watch?v=good_id_2') + args, kwargs = mock_get.call_args_list[1] + self.assertTrue(re.match(r"^https://www\.youtube\.com/api/timedtext.*", args[0])) def test_subs_for_html5_vid_with_periods(self): """ @@ -256,7 +260,8 @@ def test_subs_for_html5_vid_with_periods(self): @patch('xmodule.video_module.transcripts_utils.requests.get') def test_fail_downloading_subs(self, mock_get): - mock_get.return_value = Mock(status_code=404, text='Error 404') + track_status_code = 404 + setup_caption_responses(mock_get, 'en', 'Error 404', track_status_code) bad_youtube_sub = 'BAD_YOUTUBE_ID2' self.clear_sub_content(bad_youtube_sub) @@ -287,71 +292,6 @@ def test_success_downloading_chinese_transcripts(self): self.clear_sub_content(good_youtube_sub) - @patch('xmodule.video_module.transcripts_utils.requests.get') - def test_get_transcript_name_youtube_server_success(self, mock_get): - """ - Get transcript name from transcript_list fetch from youtube server api - depends on language code, default language in YOUTUBE Text Api is "en" - """ - youtube_text_api = copy.deepcopy(settings.YOUTUBE['TEXT_API']) - youtube_text_api['params']['v'] = 'dummy_video_id' - response_success = """ - - - - - """ - mock_get.return_value = Mock(status_code=200, text=response_success, content=response_success.encode('utf-8')) - - transcript_name = transcripts_utils.youtube_video_transcript_name(youtube_text_api) - self.assertEqual(transcript_name, 'Custom') - - @patch('xmodule.video_module.transcripts_utils.requests.get') - def test_get_transcript_name_youtube_server_no_transcripts(self, mock_get): - """ - When there are no transcripts of video transcript name will be None - """ - youtube_text_api = copy.deepcopy(settings.YOUTUBE['TEXT_API']) - youtube_text_api['params']['v'] = 'dummy_video_id' - response_success = "" - mock_get.return_value = Mock(status_code=200, text=response_success, content=response_success.encode('utf-8')) - - transcript_name = transcripts_utils.youtube_video_transcript_name(youtube_text_api) - self.assertIsNone(transcript_name) - - @patch('xmodule.video_module.transcripts_utils.requests.get') - def test_get_transcript_name_youtube_server_language_not_exist(self, mock_get): - """ - When the language does not exist in transcript_list transcript name will be None - """ - youtube_text_api = copy.deepcopy(settings.YOUTUBE['TEXT_API']) - youtube_text_api['params']['v'] = 'dummy_video_id' - youtube_text_api['params']['lang'] = 'abc' - response_success = """ - - - - - """ - mock_get.return_value = Mock(status_code=200, text=response_success, content=response_success.encode('utf-8')) - - transcript_name = transcripts_utils.youtube_video_transcript_name(youtube_text_api) - self.assertIsNone(transcript_name) - - @patch('xmodule.video_module.transcripts_utils.requests.get', side_effect=mock_requests_get) - def test_downloading_subs_using_transcript_name(self, mock_get): - """ - Download transcript using transcript name in url - """ - good_youtube_sub = 'good_id_2' - self.clear_sub_content(good_youtube_sub) - - transcripts_utils.download_youtube_subs(good_youtube_sub, self.course, settings) - mock_get.assert_any_call( - 'http://video.google.com/timedtext', - params={'lang': 'en', 'v': 'good_id_2', 'name': 'Custom'} - ) - class TestGenerateSubsFromSource(TestDownloadYoutubeSubs): # lint-amnesty, pylint: disable=test-inherits-tests """Tests for `generate_subs_from_source` function.""" @@ -520,20 +460,21 @@ class TestYoutubeTranscripts(unittest.TestCase): """ @patch('xmodule.video_module.transcripts_utils.requests.get') def test_youtube_bad_status_code(self, mock_get): - mock_get.return_value = Mock(status_code=404, text='test') + track_status_code = 404 + setup_caption_responses(mock_get, 'en', 'test', track_status_code) youtube_id = 'bad_youtube_id' with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException): transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation) @patch('xmodule.video_module.transcripts_utils.requests.get') def test_youtube_empty_text(self, mock_get): - mock_get.return_value = Mock(status_code=200, text='') + setup_caption_responses(mock_get, 'en', '') youtube_id = 'bad_youtube_id' with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException): transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation) def test_youtube_good_result(self): - response = textwrap.dedent(""" + caption_response_string = textwrap.dedent(""" Test text 1. @@ -547,11 +488,17 @@ def test_youtube_good_result(self): 'text': ['Test text 1.', 'Test text 2.', 'Test text 3.'] } youtube_id = 'good_youtube_id' + language_code = 'en' with patch('xmodule.video_module.transcripts_utils.requests.get') as mock_get: - mock_get.return_value = Mock(status_code=200, text=response, content=response.encode('utf-8')) + setup_caption_responses(mock_get, language_code, caption_response_string) transcripts = transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation) + self.assertEqual(transcripts, expected_transcripts) - mock_get.assert_called_with('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_youtube_id'}) + self.assertEqual(2, len(mock_get.mock_calls)) + args, kwargs = mock_get.call_args_list[0] + self.assertEqual(args[0], f'https://www.youtube.com/watch?v={youtube_id}') + args, kwargs = mock_get.call_args_list[1] + self.assertTrue(re.match(r"^https://www\.youtube\.com/api/timedtext.*", args[0])) class TestTranscript(unittest.TestCase): diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index fabfb2c75fd7..b2f6356a2abf 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -4,8 +4,6 @@ import json -import textwrap -from unittest.mock import Mock from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user @@ -19,6 +17,7 @@ from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin from xmodule.modulestore.xml_importer import import_course_from_xml +from xmodule.tests.test_transcripts_utils import YoutubeVideoHTMLResponse from cms.djangoapps.contentstore.utils import reverse_url from common.djangoapps.student.models import Registration @@ -365,33 +364,31 @@ def assertAssetsEqual(self, asset_son, course1_id, course2_id): self.assertEqual(value, course2_asset_attrs[key]) -def mock_requests_get(*args, **kwargs): +class HTTPGetResponse: """ - Returns mock responses for the youtube API. + Generic object used to return results from a mock patch to an HTTP GET request """ - # pylint: disable=unused-argument - response_transcript_list = """ - - - - + def __init__(self, status_code, response_string): + self.status_code = status_code + self.text = response_string + self.content = response_string.encode('utf-8') + + +def setup_caption_responses(mock_get, language_code, caption_response_string, track_status_code=200): + """ + When fetching youtube captions, two calls to requests.get() are required. The first fetches a + captions URL (link) from the video page, applicable to the selected language track. The second + fetches caption timing information from that track's captions URL. + + This helper method assumes that the two operations are performed in order, and is used in conjunction + with mock patch() operations to return appropriate results for each of the two get operations. """ - response_transcript = textwrap.dedent(""" - - subs #1 - subs #2 - subs #3 - - """) - - if kwargs == {'params': {'lang': 'en', 'v': 'good_id_2'}}: - return Mock(status_code=200, text='') - elif kwargs == {'params': {'type': 'list', 'v': 'good_id_2'}}: - return Mock(status_code=200, text=response_transcript_list, content=response_transcript_list) - elif kwargs == {'params': {'lang': 'en', 'v': 'good_id_2', 'name': 'Custom'}}: - return Mock(status_code=200, text=response_transcript, content=response_transcript) - - return Mock(status_code=404, text='') + caption_link_response = YoutubeVideoHTMLResponse.with_caption_track(language_code) + caption_track_response = HTTPGetResponse(track_status_code, caption_response_string) + mock_get.side_effect = [ + caption_link_response, + caption_track_response, + ] def get_url(handler_name, key_value, key_name='usage_key_string', kwargs=None): diff --git a/cms/djangoapps/contentstore/views/certificates.py b/cms/djangoapps/contentstore/views/certificates.py index 6c704197739d..a49c91af148f 100644 --- a/cms/djangoapps/contentstore/views/certificates.py +++ b/cms/djangoapps/contentstore/views/certificates.py @@ -572,3 +572,41 @@ def signatory_detail_handler(request, course_key_string, certificate_id, signato signatory_id=signatory_id ) return JsonResponse(status=204) + +from common.djangoapps.student.models import Survey, SurveyQuestion, SurveyCourse ,SurveyCourseDAO +from django.shortcuts import redirect +from django.urls import reverse + +@login_required +@ensure_csrf_cookie +def setting_survey_form (request, course_id): + course_key = CourseKey.from_string(course_id) + + try: + course = _get_course_and_check_access(course_key, request.user) + except PermissionDenied: + msg = _('PermissionDenied: Failed in authenticating {user}').format(user=request.user) + return JsonResponse({"error": msg}, status=403) + + if course is None : + return JsonResponse( status=403) + + surveys = Survey.objects.all() + + surveyCourse = SurveyCourseDAO.surveyCourse(course_id) + + context = { + 'context_course': course, + 'surveys': surveys, + 'surveyCourse': surveyCourse, + } + + if request.method == 'POST': + survey_id = request.POST.get('survey') + + SurveyCourseDAO.addSurveyCourse(course_id,survey_id) + url = reverse('survey', args=[course_id]) + return redirect(url) + + + return render_to_response('survey_form.html' ,context) \ No newline at end of file diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index da15dc5c84dc..2b905798e8e0 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -28,10 +28,11 @@ from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order - +from common.djangoapps.util.json_request import JsonResponse from ..utils import get_lms_link_for_item, get_sibling_urls, reverse_course_url from .helpers import get_parent_xblock, is_unit, xblock_type_display_name from .item import StudioEditModuleRuntime, add_container_page_publishing_info, create_xblock_info +from openedx.core.djangoapps.content.course_overviews.models import CourseResultLab ,render_type_lab_xblock __all__ = [ 'container_handler', @@ -538,6 +539,37 @@ def component_handler(request, usage_key_string, handler, suffix=''): """ usage_key = UsageKey.from_string(usage_key_string) + if handler == 'lab' : + if request.method == 'POST' : + resutl_lab = request.POST.get('result_lab') + type_lab = request.POST.get('type_lab') + CourseResultLab.createResultLab(block_id = usage_key_string , course_id=str(usage_key.course_key), result=resutl_lab,type=type_lab) + return JsonResponse({"results":'success'}) + + if request.method == "GET" : + try: + lab = CourseResultLab.getResultLab(block_id=usage_key_string) + data = { + "result": "", + "type": "", + "block_id" : usage_key_string + } + + if isinstance(lab, CourseResultLab): + data["result"] = lab.result + data["type"] = lab.type_lab + + return JsonResponse(data) + except CourseResultLab.DoesNotExist: + return JsonResponse({"error": "not lab"}, status=404) + + + + # Addendum: + # TNL 101-62 studio write permission is also checked for editing content. + + if handler == 'submit_studio_edits' and not has_course_author_access(request.user, usage_key.course_key): + raise PermissionDenied("No studio write Permissions") # Let the module handle the AJAX req = django_to_webob_request(request) diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 2f6a6a0c6feb..3f94cfde42d7 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -568,7 +568,7 @@ def format_in_process_course_view(uca): split_archived = settings.FEATURES.get('ENABLE_SEPARATE_ARCHIVED_COURSES', False) active_courses, archived_courses = _process_courses_list(courses_iter, in_process_course_actions, split_archived) in_process_course_actions = [format_in_process_course_view(uca) for uca in in_process_course_actions] - + return render_to_response('index.html', { 'courses': active_courses, 'split_studio_home': split_library_view_on_dashboard(), @@ -690,7 +690,7 @@ def course_index(request, course_key): raise Http404 lms_link = get_lms_link_for_item(course_module.location) reindex_link = None - if settings.FEATURES.get('ENABLE_COURSEWARE_INDEX', False): + if settings.FEATURES.get('ENABLE_COURSEWARE_INDEX', True): if GlobalStaff().has_user(request.user): reindex_link = f"/course/{str(course_key)}/search_reindex" sections = course_module.get_children() diff --git a/cms/djangoapps/contentstore/views/funix_remove_inline_styles.py b/cms/djangoapps/contentstore/views/funix_remove_inline_styles.py new file mode 100644 index 000000000000..6be7e3da7a96 --- /dev/null +++ b/cms/djangoapps/contentstore/views/funix_remove_inline_styles.py @@ -0,0 +1,194 @@ +import json +import re +import bleach +import html +from bleach.css_sanitizer import CSSSanitizer +from common.djangoapps.edxmako.shortcuts import render_to_response +from django.contrib.auth.decorators import login_required +from django.views.decorators.csrf import ensure_csrf_cookie +from django.core.exceptions import PermissionDenied +from cms.djangoapps.contentstore.views.certificates import _get_course_and_check_access +from common.djangoapps.util.json_request import JsonResponse +from opaque_keys.edx.keys import CourseKey +from common.djangoapps.edxmako.shortcuts import render_to_response +from cms.djangoapps.contentstore.views.item import _save_xblock +import openpyxl +from django.views.decorators.http import require_http_methods +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from rest_framework import status +from django.http import JsonResponse +from cms.djangoapps.contentstore.views.item import _save_xblock, _get_xblock +from opaque_keys.edx.keys import CourseKey +from xmodule.modulestore.django import modulestore + +# component class, display text, should render on client side, title attribute on client side +COMPONENT_TYPES = [ + ("HtmlBlockWithMixins", "Text Components", True, 'Remove Text Component Styles'), + ("ProblemBlockWithMixins", "Problem Components", False, 'Remove Problem Component Styles'), + ("LabXBlockWithMixins", "Lab", False, ''), + ("VideoBlockWithMixins", "Video", False, ''), + ("LibraryContentBlockWithMixins", "Library", False, ''), + ("AssignmentXBlockWithMixins", "Learning Project", False, ''), + ("DiscussionXBlockWithMixins", "Discussion", False, ''), + ("OpenAssessmentBlockWithMixins", "OpenAssessment", False, ''), +] + +@require_http_methods(['GET', 'POST']) +@login_required +@ensure_csrf_cookie +def remove_inline_styles(request, course_id): + course_key = CourseKey.from_string(course_id) + + try: + course = _get_course_and_check_access(course_key, request.user) + except PermissionDenied: + return JsonResponse({"message": 'Permission Denied'}, status=403) + + context = { + 'context_course': course, + 'component_types': list(filter(lambda item: item[2], COMPONENT_TYPES)) + } + + if request.method == 'GET': + return render_to_response('funix_remove_inline_style.html', context) + + # PUT + component_types = json.loads(request.body).get('component_types') + if len(component_types) == 0: + component_types = ['HtmlBlockWithMixins'] + + course_overview = CourseOverview.get_from_id(course_id) + if not course_overview: + return JsonResponse(data={ + "message": f"Not found course with course_code '{course_id}'", + }, status=status.HTTP_400_BAD_REQUEST) + + course = course_overview._original_course + sections = course.get_children() + + remove_errors = [] + remove_success = [] + publish_errors = [] + publish_success = [] + + for section in sections: + subsections = section.get_children() + for sub in subsections: + units = sub.get_children() + for unit in units: + components = unit.get_children() + for component in components: + if type(component).__name__ in component_types: + usage_key = component.scope_ids.usage_id + try: + _save_xblock( + request.user, + _get_xblock(usage_key, request.user), + data=_remove_style(component.data), + children_strings=None, + metadata={}, + nullout=None, + grader_type=None, + is_prereq=None, + prereq_usage_key=None, + prereq_min_score=None, + prereq_min_completion=None, + publish=None, + fields=None, + ) + success_msg = f"{sub.display_name}, {unit.display_name}, {component.display_name}" + remove_success.append(success_msg) + except Exception as e: + print(str(e)) + error_msg = f"{sub.display_name}, {unit.display_name}, {component.display_name}: {str(e)}" + print(error_msg, type(component).__name__, component.data) + remove_errors.append(error_msg) + + + if json.loads(request.body).get('publish'): + for section in sections: + try: + modulestore().publish(section.location, request.user.id) + publish_success.append(section.display_name) + except Exception as e: + msg = f"Failed to publish section {section.display_name}: {str(e)}" + print(msg) + publish_errors.append(section.display_name) + + response_msg = _componse_response_msg( + remove_success, \ + remove_errors, \ + publish_success, \ + publish_errors, \ + json.loads(request.body).get('publish') + ) + + return JsonResponse(data={ + "message": response_msg, + "errors": { + "publish_errors": publish_errors, + "remove_errors": remove_errors, + } + }, status=status.HTTP_200_OK) + + + +css_sanitizer = CSSSanitizer(allowed_css_properties=[]) + +all_html_tags = [ + 'noscript','a','abbr','acronym','address','applet','area','article','aside','audio', + 'b','base','basefont','bdi','bdo','big','blockquote','body','br','button','canvas', + 'caption','center','cite','code','col','colgroup','data','datalist','dd','del','details', + 'dfn','dialog','dir','div','dl','dt','em','embed','fieldset','figcaption','figure','font', + 'footer','form','frame','frameset','h1','h2','h3','h4','h5','h6','head','header','hgroup', + 'img','input','ins','kbd','label','legend','li','link','main','map','mark','menu','meta', + 'meter','nav','noframes','noscript','object','ol','optgroup','option','output','p','param', + 'picture','pre','progress','q','rp','rt','ruby','s','samp','script','search','section','select', + 'small','source','span','strike','strong','style','sub','summary','sup','svg','table','tbody', + 'td','template','textarea','tfoot','th','thead','time','title','tr','track','tt','u','ul', + 'var','video','wbr','hr','html','i','iframe','o:p', 'v:shape', 'v:imagedata', 'v:shapetype', + 'v:stroke', 'v:f', 'v:path', 'v:formulas', 'o:lock', 'w:borderright', 'w:bordertop', 'w:borderleft', + 'w:borderbottom', 'google-sheets-html-origin' +] + +tags_will_be_removed = [] + +def _allow_all_attrs_except_style(tag, name, value): + return name != 'style' + +def _remove_style(data): + unescaped_data = html.unescape(data) + for tag in tags_will_be_removed: + unescaped_data = re.sub(tag, '', unescaped_data) + + cleaned_and_escaped_style_data = bleach.clean(unescaped_data, tags=all_html_tags, attributes=_allow_all_attrs_except_style) + + return cleaned_and_escaped_style_data + +def _componse_response_msg(remove_success, remove_errors, publish_success, publish_errors, publish): + response_msg = "" + + if len(remove_success) == 0 and len(remove_errors) == 0: + response_msg = "No remove styles actions happened. " + + elif len(remove_success) > 0 and len(remove_errors) == 0: + response_msg = "Successfully to remove styles on all components. " + + elif len(remove_success) == 0 and len(remove_errors) > 0: + response_msg = "Failed to remove styles on all components. " + else: + response_msg = f"Failed to remove styles on {len(remove_errors)} component(s). " + + if publish: + if len(publish_success) == 0 and len(publish_errors) == 0: + response_msg += "No pulish actions happened." + elif len(publish_success) > 0 and len(publish_errors) == 0: + response_msg += "Published all sections successfully." + elif len(publish_success) == 0 and len(publish_errors) > 0: + response_msg += "Failed to publish all sections." + else: + response_msg += f"Failed to publish {len(publish_errors)} section(s)." + else: + response_msg += "Did not publish." + + return response_msg \ No newline at end of file diff --git a/cms/djangoapps/contentstore/views/import_excel.py b/cms/djangoapps/contentstore/views/import_excel.py new file mode 100644 index 000000000000..6a5e7e0d4b17 --- /dev/null +++ b/cms/djangoapps/contentstore/views/import_excel.py @@ -0,0 +1,94 @@ + +from common.djangoapps.edxmako.shortcuts import render_to_response +from django.contrib.auth.decorators import login_required +from django.views.decorators.csrf import ensure_csrf_cookie +from django.core.exceptions import PermissionDenied +from cms.djangoapps.contentstore.views.certificates import _get_course_and_check_access +from common.djangoapps.util.json_request import JsonResponse +from opaque_keys.edx.keys import CourseKey +from common.djangoapps.edxmako.shortcuts import render_to_response +from cms.djangoapps.contentstore.views.item import create_item_import , _update_with_callback ,_save_xblock +# from cms.djangoapps.contentstore.models import CourseUnitTime +from openedx.core.djangoapps.content.course_overviews.models import CourseUnitTime +import openpyxl + +@login_required +@ensure_csrf_cookie +def viewImportExcel (request, course_id) : + course_key = CourseKey.from_string(course_id) + + try: + course = _get_course_and_check_access(course_key, request.user) + except PermissionDenied: + msg = _('PermissionDenied: Failed in authenticating {user}').format(user=request.user) + return JsonResponse({"error": msg}, status=403) + context = { + 'context_course': course, + } + parts = course_id.split('+') + organization = parts[0].split(":")[1] + course_number = parts[1] + course_run = parts[2] + parent_locator = f'block-v1:{organization}+{course_number}+{course_run}+type@course+block@course' + if request.method == "POST" and request.FILES.get("excel_file"): + excel_file = request.FILES["excel_file"] + + if excel_file.name.endswith('.xlsx'): + workbook = openpyxl.load_workbook(excel_file, data_only=True) + sheets = workbook.sheetnames + Details = workbook['2. Details'] + Duration = workbook['3. Duration'] + section=create_item_import(request, parent_locator, category='chapter', display_name='Mở đầu') + + section_block = create_item_import(request ,parent_locator, category='chapter', display_name='Nội dung khoá học') + + # sheet details + list_video = [] + url_ = '' + list_unit = [] + for data in Details.iter_rows(values_only=True): + + if data[1] is not None : + if ':' in data[1] : + + lesson_block =create_item_import(request, parent_locator=str(section_block.location) , category='sequential', display_name=data[1]) + create_item_import(request, parent_locator=str(lesson_block.location), category='vertical', display_name='Mở đầu') + unit_block=create_item_import(request, parent_locator=str(lesson_block.location), category='vertical', display_name='Nội dung bài học') + html_block =create_item_import(request, parent_locator= str(unit_block.location) ,category='html') + list_unit.append({'unit' : data[1] , "block_id" : str(lesson_block.location) }) + if data[3] is not None and 'video' in data[3].lower() and data[5] is not None: + videos =[] + videos.append(data[5]) + list_video.append({'title' : data[2], 'video' : videos , 'unit' : data[1] , 'block': html_block}) + + newArr = [] + grouped = {} + for item in list_video: + block_value = item['block'] + if block_value not in grouped: + grouped[block_value] = [] + grouped[block_value].append({'title': item['title'], 'video' : item['video']}) # + + + for block_value, content in grouped.items(): + newArr.append({'block' : block_value , 'content' : content}) + + for block in newArr : + url_ ='' + for a in block['content']: + for v in a['video']: + url_ += f'

Video: {a["title"]}

' + _save_xblock(xblock=block['block'] , user=request.user, data=url_) + + # sheet Duration + for data in Duration.iter_rows(values_only=True): + if data[0] is not None: + for u in list_unit : + if data[0] in u['unit']: + + CourseUnitTime.create_unit_time(course_id=course_id, block_id=u['block_id'], display_name=u['unit'], total=data[10]) + + workbook.close() + + + return render_to_response('import_excel.html', context) \ No newline at end of file diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 2ef9b80d38b2..7ab9638c2cbd 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -54,7 +54,9 @@ from xmodule.services import ConfigurationService, SettingsService, TeamsConfigurationService # lint-amnesty, pylint: disable=wrong-import-order from xmodule.tabs import CourseTabList # lint-amnesty, pylint: disable=wrong-import-order from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, STUDIO_VIEW # lint-amnesty, pylint: disable=wrong-import-order - +from openedx.core.djangoapps.content.course_overviews.models import CourseOverviewSubText +# from cms.djangoapps.contentstore.models import CourseUnitTime +from openedx.core.djangoapps.content.course_overviews.models import CourseUnitTime from ..utils import ( ancestor_has_staff_lock, find_release_date_source, @@ -118,6 +120,7 @@ def _is_library_component_limit_reached(usage_key): @login_required @expect_json def xblock_handler(request, usage_key_string=None): + """ The restful handler for xblock requests. @@ -168,9 +171,11 @@ def xblock_handler(request, usage_key_string=None): if duplicate_source_locator is not present The locator (unicode representation of a UsageKey) for the created xblock (minus children) is returned. """ + if usage_key_string: - usage_key = usage_key_with_run(usage_key_string) + usage_key = usage_key_with_run(usage_key_string) + access_check = has_studio_read_access if request.method == 'GET' else has_studio_write_access if not access_check(request.user, usage_key.course_key): raise PermissionDenied() @@ -180,6 +185,7 @@ def xblock_handler(request, usage_key_string=None): if 'application/json' in accept_header: fields = request.GET.get('fields', '').split(',') + if 'graderType' in fields: # right now can't combine output of this w/ output of _get_module_info, but worthy goal return JsonResponse(CourseGradingModel.get_section_grader_type(usage_key)) @@ -195,9 +201,12 @@ def xblock_handler(request, usage_key_string=None): return HttpResponse(status=406) elif request.method == 'DELETE': + + _delete_item(usage_key, request.user) return JsonResponse() else: # Since we have a usage_key, we are updating an existing xblock. + return _save_xblock( request.user, _get_xblock(usage_key, request.user), @@ -214,7 +223,9 @@ def xblock_handler(request, usage_key_string=None): fields=request.json.get('fields'), ) elif request.method in ('PUT', 'POST'): + if 'duplicate_source_locator' in request.json: + parent_usage_key = usage_key_with_run(request.json['parent_locator']) duplicate_source_usage_key = usage_key_with_run(request.json['duplicate_source_locator']) @@ -237,7 +248,7 @@ def xblock_handler(request, usage_key_string=None): }, status=400 ) - + dest_usage_key = _duplicate_item( parent_usage_key, duplicate_source_usage_key, @@ -249,6 +260,7 @@ def xblock_handler(request, usage_key_string=None): 'courseKey': str(dest_usage_key.course_key) }) else: + return _create_item(request) elif request.method == 'PATCH': if 'move_source_locator' in request.json: @@ -512,6 +524,7 @@ def xblock_container_handler(request, usage_key_string): def _update_with_callback(xblock, user, old_metadata=None, old_content=None): + """ Updates the xblock in the modulestore. But before doing so, it calls the xblock's editor_saved callback function. @@ -523,7 +536,7 @@ def _update_with_callback(xblock, user, old_metadata=None, old_content=None): old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content) xblock.xmodule_runtime = StudioEditModuleRuntime(user) xblock.editor_saved(user, old_metadata, old_content) - + # Update after the callback so any changes made in the callback will get persisted. return modulestore().update_item(xblock, user.id) @@ -537,10 +550,11 @@ def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, to default). """ + store = modulestore() # Perform all xblock changes within a (single-versioned) transaction with store.bulk_operations(xblock.location.course_key): - + # Don't allow updating an xblock and discarding changes in a single operation (unsupported by UI). if publish == "discard_changes": store.revert_to_published(xblock.location, user.id) @@ -550,12 +564,13 @@ def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, old_metadata = own_metadata(xblock) old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content) - + if data: # TODO Allow any scope.content fields not just "data" (exactly like the get below this) xblock.data = data else: data = old_content['data'] if 'data' in old_content else None + if fields: for field_name in fields: @@ -592,6 +607,7 @@ def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, # into another parent, then that child would have been deleted from this parent on the server. However, # this is error could occur in modulestores (such as Draft) that do not support atomic write-transactions old_children = set(xblock.children) - set(children) + if any( store.get_parent_location(old_child) == xblock.location for old_child in old_children @@ -630,11 +646,11 @@ def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, return JsonResponse({"error": reason}, 400) field.write_to(xblock, value) - validate_and_update_xblock_due_date(xblock) + # update the xblock and call any xblock callbacks xblock = _update_with_callback(xblock, user, old_metadata, old_content) - + # for static tabs, their containing course also records their display name course = store.get_course(xblock.location.course_key) if xblock.location.block_type == 'static_tab': @@ -710,6 +726,8 @@ def _create_item(request): """View for create items.""" parent_locator = request.json['parent_locator'] usage_key = usage_key_with_run(parent_locator) + # print('=========', usage_key) + # print('===========', request.json) =========== {'parent_locator': 'block-v1:bcd+cde+asd222+type@course+block@course', 'category': 'chapter', 'display_name': 'Section'} if not has_studio_write_access(request.user, usage_key.course_key): raise PermissionDenied() @@ -730,7 +748,7 @@ def _create_item(request): }, status=400 ) - + # print('======json===', request.json) created_block = create_xblock( parent_locator=parent_locator, user=request.user, @@ -738,12 +756,34 @@ def _create_item(request): display_name=request.json.get('display_name'), boilerplate=request.json.get('boilerplate'), ) - + # print('=====created_blockcreated_block===', created_block) return JsonResponse( {'locator': str(created_block.location), 'courseKey': str(created_block.location.course_key)} ) +@login_required +@expect_json +def create_item_import (request,parent_locator, category,display_name=None , boilerplate=None): + + usage_key = usage_key_with_run(parent_locator) + # print('=============',category,display_name ) + created_block = create_xblock( + parent_locator=parent_locator, + user=request.user, + category=category, + display_name=display_name, + boilerplate=boilerplate, + ) + + return created_block + + + + + + + def _get_source_index(source_usage_key, source_parent): """ Get source index position of the XBlock. @@ -977,6 +1017,14 @@ def _delete_item(usage_key, user): If the xblock is a Static Tab, removes it from course.tabs as well. """ store = modulestore() + CourseOverviewSubText.removeSubText(sequence_id=str(usage_key)) + CourseUnitTime.remove_unit_time_seuqe(block_id = str(usage_key)) + + block = _get_xblock(usage_key=usage_key, user=user) + if hasattr(block, 'children') : + for a in block.children : + CourseOverviewSubText.removeSubText(sequence_id=str(a)) + CourseUnitTime.remove_unit_time_seuqe(block_id = str(a)) with store.bulk_operations(usage_key.course_key): # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so @@ -1011,6 +1059,7 @@ def orphan_handler(request, course_key_string): raise PermissionDenied() if request.method == 'DELETE': if request.user.is_staff: + # print('====usage_key=========', course_usage_key) deleted_items = _delete_orphans(course_usage_key, request.user.id, commit=True) return JsonResponse({'deleted': deleted_items}) else: diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 4100c2dd47a0..044eaa5a5cf0 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -48,7 +48,8 @@ from ..utils import get_visibility_partition_info from .access import get_user_role from .session_kv_store import SessionKeyValueStore - +from common.djangoapps.util.json_request import JsonResponse +from openedx.core.djangoapps.content.course_overviews.models import render_type_lab_xblock __all__ = ['preview_handler'] log = logging.getLogger(__name__) @@ -64,6 +65,12 @@ def preview_handler(request, usage_key_string, handler, suffix=''): handler: The handler to execute suffix: The remainder of the url to be passed to the handler """ + if handler == 'upload' : + return JsonResponse({"a":"1"}) + if handler == 'lab' : + data = render_type_lab_xblock(block_id = usage_key_string, email=request.user.email ) + return JsonResponse(data) + usage_key = UsageKey.from_string(usage_key_string) descriptor = modulestore().get_item(usage_key) @@ -71,6 +78,7 @@ def preview_handler(request, usage_key_string, handler, suffix=''): # Let the module handle the AJAX req = django_to_webob_request(request) + try: resp = instance.handle(handler, req, suffix) diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index 055cea6401c2..cf7ef1ecc3a8 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -8,6 +8,7 @@ import ddt from django.conf import settings +from django.core.exceptions import PermissionDenied from django.http import Http404 from django.test import TestCase from django.test.client import RequestFactory @@ -2197,6 +2198,15 @@ def create_response(handler, request, suffix): # lint-amnesty, pylint: disable= self.assertEqual(component_handler(self.request, self.usage_key_string, 'dummy_handler').status_code, status_code) + def test_submit_studio_edits_checks_author_permission(self): + with self.assertRaises(PermissionDenied): + with patch( + 'common.djangoapps.student.auth.has_course_author_access', + return_value=False + ) as mocked_has_course_author_access: + component_handler(self.request, self.usage_key_string, 'submit_studio_edits') + assert mocked_has_course_author_access.called is True + @ddt.data((True, True), (False, False),) @ddt.unpack def test_aside(self, is_xblock_aside, is_get_aside_called): diff --git a/cms/djangoapps/contentstore/views/tests/test_transcripts.py b/cms/djangoapps/contentstore/views/tests/test_transcripts.py index b39535856ad8..fd130917d2e4 100644 --- a/cms/djangoapps/contentstore/views/tests/test_transcripts.py +++ b/cms/djangoapps/contentstore/views/tests/test_transcripts.py @@ -16,7 +16,7 @@ from edxval.api import create_video from opaque_keys.edx.keys import UsageKey -from cms.djangoapps.contentstore.tests.utils import CourseTestCase, mock_requests_get +from cms.djangoapps.contentstore.tests.utils import CourseTestCase, setup_caption_responses from openedx.core.djangoapps.contentserver.caching import del_cached_content from xmodule.contentstore.content import StaticContent # lint-amnesty, pylint: disable=wrong-import-order from xmodule.contentstore.django import contentstore # lint-amnesty, pylint: disable=wrong-import-order @@ -940,7 +940,7 @@ def test_check_youtube(self): } ) - @patch('xmodule.video_module.transcripts_utils.requests.get', side_effect=mock_requests_get) + @patch('xmodule.video_module.transcripts_utils.requests.get') def test_check_youtube_with_transcript_name(self, mock_get): """ Test that the transcripts are fetched correctly when the the transcript name is set @@ -958,6 +958,7 @@ def test_check_youtube_with_transcript_name(self, mock_get): ] } self.save_subs_to_store(subs, 'good_id_2') + setup_caption_responses(mock_get, 'en', 'caption_response_string') link = reverse('check_transcripts') data = { 'locator': str(self.video_usage_key), @@ -969,10 +970,9 @@ def test_check_youtube_with_transcript_name(self, mock_get): } resp = self.client.get(link, {'data': json.dumps(data)}) - mock_get.assert_any_call( - 'http://video.google.com/timedtext', - params={'lang': 'en', 'v': 'good_id_2', 'name': 'Custom'} - ) + self.assertEqual(2, len(mock_get.mock_calls)) + args, kwargs = mock_get.call_args_list[0] + self.assertEqual(args[0], 'https://www.youtube.com/watch?v=good_id_2') self.assertEqual(resp.status_code, 200) diff --git a/cms/djangoapps/contentstore/views/transcripts_ajax.py b/cms/djangoapps/contentstore/views/transcripts_ajax.py index a3b91b6101f6..cf322ff45ba2 100644 --- a/cms/djangoapps/contentstore/views/transcripts_ajax.py +++ b/cms/djangoapps/contentstore/views/transcripts_ajax.py @@ -7,12 +7,10 @@ """ -import copy import json import logging import os -import requests from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied @@ -42,7 +40,7 @@ get_transcript_for_video, get_transcript_from_val, get_transcripts_from_youtube, - youtube_video_transcript_name + get_transcript_link_from_youtube ) __all__ = [ @@ -340,15 +338,7 @@ def check_transcripts(request): # lint-amnesty, pylint: disable=too-many-statem except NotFoundError: log.debug("Can't find transcripts in storage for youtube id: %s", youtube_id) - # youtube server - youtube_text_api = copy.deepcopy(settings.YOUTUBE['TEXT_API']) - youtube_text_api['params']['v'] = youtube_id - youtube_transcript_name = youtube_video_transcript_name(youtube_text_api) - if youtube_transcript_name: - youtube_text_api['params']['name'] = youtube_transcript_name - youtube_response = requests.get('http://' + youtube_text_api['url'], params=youtube_text_api['params']) - - if youtube_response.status_code == 200 and youtube_response.text: + if get_transcript_link_from_youtube(youtube_id): transcripts_presence['youtube_server'] = True #check youtube local and server transcripts for equality if transcripts_presence['youtube_server'] and transcripts_presence['youtube_local']: diff --git a/cms/djangoapps/contentstore/views/unit_time.py b/cms/djangoapps/contentstore/views/unit_time.py new file mode 100644 index 000000000000..ad44a26eaaef --- /dev/null +++ b/cms/djangoapps/contentstore/views/unit_time.py @@ -0,0 +1,49 @@ +from rest_framework.decorators import api_view +from django.contrib.auth.decorators import login_required +from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie +from common.djangoapps.util.json_request import JsonResponse +from openedx.core.djangoapps.content.course_overviews.models import CourseUnitTime +from .helpers import usage_key_with_run +from .item import _get_xblock + +@api_view(['GET']) +@login_required +@ensure_csrf_cookie +def get_time_course_unit (request, block_id): + # usage_key = usage_key_with_run(block_id) + + # block = _get_xblock(usage_key=usage_key, user=request.user) + # for a in block.children : + # print('==============', str(a)) + unit_time = CourseUnitTime.get_unit_time(block_id=block_id) + + if unit_time is not None : + data = { + "id" : block_id, + "total" : unit_time.total, + "title" : unit_time.display_name, + + } + else : + data = { + "id" : block_id, + "total" : "" , + + } + + return JsonResponse(data) + + +@api_view(['POST']) +@login_required +@ensure_csrf_cookie +def set_course_time_unit (request) : + + if request.method == 'POST' : + total = request.data.get('total') + sequence_id = request.data.get('suquence_id') + course_id = request.data.get('courseId') + display_name = request.data.get('title') + + CourseUnitTime.set_unit_time(block_id=sequence_id, total = total, course_id=course_id, display_name=display_name) + return JsonResponse({'a':'a'}) \ No newline at end of file diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index cf4bf95a645d..03ab0816ca37 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -140,11 +140,10 @@ YOUTUBE_HOSTNAME = os.environ.get('BOK_CHOY_HOSTNAME', '127.0.0.1') YOUTUBE['API'] = f"http://{YOUTUBE_HOSTNAME}:{YOUTUBE_PORT}/get_youtube_api/" YOUTUBE['METADATA_URL'] = f"http://{YOUTUBE_HOSTNAME}:{YOUTUBE_PORT}/test_youtube/" -YOUTUBE['TEXT_API']['url'] = f"{YOUTUBE_HOSTNAME}:{YOUTUBE_PORT}/test_transcripts_youtube/" FEATURES['ENABLE_COURSEWARE_INDEX'] = True FEATURES['ENABLE_LIBRARY_INDEX'] = True -FEATURES['ENABLE_CONTENT_LIBRARY_INDEX'] = False +FEATURES['ENABLE_CONTENT_LIBRARY_INDEX'] = True ORGANIZATIONS_AUTOCREATE = False diff --git a/cms/envs/bok_choy_docker.py b/cms/envs/bok_choy_docker.py index 28edfe60d355..878537de008d 100644 --- a/cms/envs/bok_choy_docker.py +++ b/cms/envs/bok_choy_docker.py @@ -24,4 +24,3 @@ BOK_CHOY_HOST = os.environ['BOK_CHOY_HOSTNAME'] YOUTUBE['API'] = f"http://{BOK_CHOY_HOST}:{YOUTUBE_PORT}/get_youtube_api/" YOUTUBE['METADATA_URL'] = f"http://{BOK_CHOY_HOST}:{YOUTUBE_PORT}/test_youtube/" -YOUTUBE['TEXT_API']['url'] = f"{BOK_CHOY_HOST}:{YOUTUBE_PORT}/test_transcripts_youtube/" diff --git a/cms/envs/common.py b/cms/envs/common.py index 27e16e5c430c..a8986599bb3a 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -288,13 +288,13 @@ 'LICENSING': False, # Enable the courseware search functionality - 'ENABLE_COURSEWARE_INDEX': False, + 'ENABLE_COURSEWARE_INDEX': True, # Enable content libraries (modulestore) search functionality - 'ENABLE_LIBRARY_INDEX': False, + 'ENABLE_LIBRARY_INDEX': True, # Enable content libraries (blockstore) indexing - 'ENABLE_CONTENT_LIBRARY_INDEX': False, + 'ENABLE_CONTENT_LIBRARY_INDEX': True, # .. toggle_name: FEATURES['ALLOW_COURSE_RERUNS'] # .. toggle_implementation: DjangoSetting @@ -329,7 +329,7 @@ 'ENABLE_SPECIAL_EXAMS': False, # Show the language selector in the header - 'SHOW_HEADER_LANGUAGE_SELECTOR': False, + 'SHOW_HEADER_LANGUAGE_SELECTOR': True, # At edX it's safe to assume that English transcripts are always available # This is not the case for all installations. @@ -361,7 +361,7 @@ 'ENABLE_GRADE_DOWNLOADS': True, 'ENABLE_MKTG_SITE': False, 'ENABLE_DISCUSSION_HOME_PANEL': True, - 'ENABLE_CORS_HEADERS': False, + 'ENABLE_CORS_HEADERS': True, 'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': False, 'ENABLE_COUNTRY_ACCESS': False, 'ENABLE_CREDIT_API': False, @@ -1210,7 +1210,8 @@ # Locale/Internationalization CELERY_TIMEZONE = 'UTC' TIME_ZONE = 'UTC' -LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'vi' # http://www.i18nguy.com/unicode/language-identifiers.html +# LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGES_BIDI = lms.envs.common.LANGUAGES_BIDI LANGUAGE_COOKIE_NAME = lms.envs.common.LANGUAGE_COOKIE_NAME @@ -1517,14 +1518,10 @@ # URL to get YouTube metadata 'METADATA_URL': 'https://www.googleapis.com/youtube/v3/videos', - # Current youtube api for requesting transcripts. - # For example: http://video.google.com/timedtext?lang=en&v=j_jEn79vS3g. - 'TEXT_API': { - 'url': 'video.google.com/timedtext', - 'params': { - 'lang': 'en', - 'v': 'set_youtube_id_of_11_symbols_here', - }, + # Web page mechanism for scraping transcript information from youtube video pages + 'TRANSCRIPTS': { + 'CAPTION_TRACKS_REGEX': r"captionTracks\"\:\[(?P[^\]]+)", + 'YOUTUBE_URL_BASE': 'https://www.youtube.com/watch?v=', }, 'IMAGE_API': 'http://img.youtube.com/vi/{youtube_id}/0.jpg', # /maxresdefault.jpg for 1920*1080 @@ -1742,6 +1739,11 @@ 'openedx.features.content_type_gating', 'openedx.features.discounts', 'openedx.features.effort_estimation', + 'openedx.features.funix_relative_date', + 'openedx.features.toggle_feature', + 'openedx.features.funix_goal', + 'openedx.features.funix_specialization', + 'openedx.features.upload_file', 'lms.djangoapps.experiments', 'openedx.core.djangoapps.external_user_ids', @@ -1978,10 +1980,8 @@ ] LIBRARY_BLOCK_TYPES = [ - { - 'component': 'library_sourced', - 'boilerplate_name': None - }, + # Per https://github.com/openedx/build-test-release-wg/issues/231 + # we removed the library source content block from defaults until complete. { 'component': 'library_content', 'boilerplate_name': None @@ -2422,13 +2422,13 @@ 'BUCKET': None, 'ROOT_PATH': 'sandbox', } - +CORS_ORIGIN_ALLOW_ALL = True ############# CORS headers for cross-domain requests ################# if FEATURES.get('ENABLE_CORS_HEADERS'): CORS_ALLOW_CREDENTIALS = True CORS_ORIGIN_WHITELIST = () - CORS_ORIGIN_ALLOW_ALL = False - CORS_ALLOW_INSECURE = False + + CORS_ALLOW_INSECURE = True CORS_ALLOW_HEADERS = corsheaders_default_headers + ( 'use-jwt-cookie', ) diff --git a/cms/envs/devstack-experimental.yml b/cms/envs/devstack-experimental.yml index daa1225ee852..f8fdfafdcbe6 100644 --- a/cms/envs/devstack-experimental.yml +++ b/cms/envs/devstack-experimental.yml @@ -18,19 +18,19 @@ # Paver, described here: https://github.com/openedx/devstack/pull/866 # TODO: If the effort described above is abandoned, then this file should # probably be deleted. -ACTIVATION_EMAIL_SUPPORT_LINK: '' +ACTIVATION_EMAIL_SUPPORT_LINK: "" AFFILIATE_COOKIE_NAME: dev_affiliate_id ALTERNATE_WORKER_QUEUES: lms ANALYTICS_DASHBOARD_NAME: Your Platform Name Here Insights ANALYTICS_DASHBOARD_URL: http://localhost:18110/courses AUTH_PASSWORD_VALIDATORS: -- NAME: django.contrib.auth.password_validation.UserAttributeSimilarityValidator -- NAME: common.djangoapps.util.password_policy_validators.MinimumLengthValidator + - NAME: django.contrib.auth.password_validation.UserAttributeSimilarityValidator + - NAME: common.djangoapps.util.password_policy_validators.MinimumLengthValidator OPTIONS: - min_length: 2 -- NAME: common.djangoapps.util.password_policy_validators.MaximumLengthValidator + min_length: 2 + - NAME: common.djangoapps.util.password_policy_validators.MaximumLengthValidator OPTIONS: - max_length: 75 + max_length: 75 AWS_ACCESS_KEY_ID: null AWS_QUERYSTRING_AUTH: false AWS_S3_CUSTOM_DOMAIN: SET-ME-PLEASE (ex. bucket-name.s3.amazonaws.com) @@ -42,124 +42,124 @@ BASE_COOKIE_DOMAIN: localhost BLOCKSTORE_API_URL: http://localhost:18250/api/v1 BLOCKSTORE_PUBLIC_URL_ROOT: http://localhost:18250 BLOCK_STRUCTURES_SETTINGS: - COURSE_PUBLISH_TASK_DELAY: 30 - PRUNING_ACTIVE: false - TASK_DEFAULT_RETRY_DELAY: 30 - TASK_MAX_RETRIES: 5 -BRANCH_IO_KEY: '' + COURSE_PUBLISH_TASK_DELAY: 30 + PRUNING_ACTIVE: false + TASK_DEFAULT_RETRY_DELAY: 30 + TASK_MAX_RETRIES: 5 +BRANCH_IO_KEY: "" BUGS_EMAIL: bugs@example.com BULK_EMAIL_DEFAULT_FROM_EMAIL: no-reply@example.com BULK_EMAIL_EMAILS_PER_TASK: 500 BULK_EMAIL_LOG_SENT_EMAILS: false CACHES: - celery: - BACKEND: django.core.cache.backends.memcached.MemcachedCache - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: celery - LOCATION: - - edx.devstack.memcached:11211 - TIMEOUT: '7200' - configuration: - BACKEND: django.core.cache.backends.memcached.MemcachedCache - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: 78f87108afce - LOCATION: - - edx.devstack.memcached:11211 - course_structure_cache: - BACKEND: django.core.cache.backends.memcached.MemcachedCache - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: course_structure - LOCATION: - - edx.devstack.memcached:11211 - TIMEOUT: '7200' - default: - BACKEND: django.core.cache.backends.memcached.MemcachedCache - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: default - LOCATION: - - edx.devstack.memcached:11211 - VERSION: '1' - general: - BACKEND: django.core.cache.backends.memcached.MemcachedCache - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: general - LOCATION: - - edx.devstack.memcached:11211 - mongo_metadata_inheritance: - BACKEND: django.core.cache.backends.memcached.MemcachedCache - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: mongo_metadata_inheritance - LOCATION: - - edx.devstack.memcached:11211 - TIMEOUT: 300 - staticfiles: - BACKEND: django.core.cache.backends.memcached.MemcachedCache - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: 78f87108afce_general - LOCATION: - - edx.devstack.memcached:11211 -CAS_ATTRIBUTE_CALLBACK: '' -CAS_EXTRA_LOGIN_PARAMS: '' -CAS_SERVER_URL: '' + celery: + BACKEND: django.core.cache.backends.memcached.MemcachedCache + KEY_FUNCTION: common.djangoapps.util.memcache.safe_key + KEY_PREFIX: celery + LOCATION: + - edx.devstack.memcached:11211 + TIMEOUT: "7200" + configuration: + BACKEND: django.core.cache.backends.memcached.MemcachedCache + KEY_FUNCTION: common.djangoapps.util.memcache.safe_key + KEY_PREFIX: 78f87108afce + LOCATION: + - edx.devstack.memcached:11211 + course_structure_cache: + BACKEND: django.core.cache.backends.memcached.MemcachedCache + KEY_FUNCTION: common.djangoapps.util.memcache.safe_key + KEY_PREFIX: course_structure + LOCATION: + - edx.devstack.memcached:11211 + TIMEOUT: "7200" + default: + BACKEND: django.core.cache.backends.memcached.MemcachedCache + KEY_FUNCTION: common.djangoapps.util.memcache.safe_key + KEY_PREFIX: default + LOCATION: + - edx.devstack.memcached:11211 + VERSION: "1" + general: + BACKEND: django.core.cache.backends.memcached.MemcachedCache + KEY_FUNCTION: common.djangoapps.util.memcache.safe_key + KEY_PREFIX: general + LOCATION: + - edx.devstack.memcached:11211 + mongo_metadata_inheritance: + BACKEND: django.core.cache.backends.memcached.MemcachedCache + KEY_FUNCTION: common.djangoapps.util.memcache.safe_key + KEY_PREFIX: mongo_metadata_inheritance + LOCATION: + - edx.devstack.memcached:11211 + TIMEOUT: 300 + staticfiles: + BACKEND: django.core.cache.backends.memcached.MemcachedCache + KEY_FUNCTION: common.djangoapps.util.memcache.safe_key + KEY_PREFIX: 78f87108afce_general + LOCATION: + - edx.devstack.memcached:11211 +CAS_ATTRIBUTE_CALLBACK: "" +CAS_EXTRA_LOGIN_PARAMS: "" +CAS_SERVER_URL: "" CELERYBEAT_SCHEDULER: celery.beat:PersistentScheduler CELERY_BROKER_HOSTNAME: localhost -CELERY_BROKER_PASSWORD: '' +CELERY_BROKER_PASSWORD: "" CELERY_BROKER_TRANSPORT: redis -CELERY_BROKER_USER: '' +CELERY_BROKER_USER: "" CELERY_BROKER_USE_SSL: false -CELERY_BROKER_VHOST: '' +CELERY_BROKER_VHOST: "" CELERY_EVENT_QUEUE_TTL: null CELERY_QUEUES: -- edx.cms.core.default -- edx.cms.core.high + - edx.cms.core.default + - edx.cms.core.high CELERY_TIMEZONE: UTC CERTIFICATE_TEMPLATE_LANGUAGES: - en: English - es: Español + en: English + es: Español CERT_QUEUE: certificates CMS_BASE: edx.devstack.studio:18010 CODE_JAIL: - limits: - CPU: 1 - FSIZE: 1048576 - PROXY: 0 - REALTIME: 3 - VMEM: 536870912 - python_bin: /edx/app/edxapp/venvs/edxapp-sandbox/bin/python - user: sandbox + limits: + CPU: 1 + FSIZE: 1048576 + PROXY: 0 + REALTIME: 3 + VMEM: 536870912 + python_bin: /edx/app/edxapp/venvs/edxapp-sandbox/bin/python + user: sandbox COMMENTS_SERVICE_KEY: password COMMENTS_SERVICE_URL: http://localhost:18080 COMPREHENSIVE_THEME_DIRS: -- '' + - "" COMPREHENSIVE_THEME_LOCALE_PATHS: [] CONTACT_EMAIL: info@example.com CONTENTSTORE: - ADDITIONAL_OPTIONS: {} - DOC_STORE_CONFIG: - authsource: '' - collection: modulestore - connectTimeoutMS: 2000 - db: edxapp - host: - - edx.devstack.mongo - password: password - port: 27017 - read_preference: PRIMARY - replicaSet: '' - socketTimeoutMS: 3000 - ssl: false - user: edxapp - ENGINE: xmodule.contentstore.mongo.MongoContentStore - OPTIONS: - auth_source: '' - db: edxapp - host: - - edx.devstack.mongo - password: password - port: 27017 - ssl: false - user: edxapp -CORS_ORIGIN_ALLOW_ALL: false + ADDITIONAL_OPTIONS: {} + DOC_STORE_CONFIG: + authsource: "" + collection: modulestore + connectTimeoutMS: 2000 + db: edxapp + host: + - edx.devstack.mongo + password: password + port: 27017 + read_preference: PRIMARY + replicaSet: "" + socketTimeoutMS: 3000 + ssl: false + user: edxapp + ENGINE: xmodule.contentstore.mongo.MongoContentStore + OPTIONS: + auth_source: "" + db: edxapp + host: + - edx.devstack.mongo + password: password + port: 27017 + ssl: false + user: edxapp +CORS_ORIGIN_ALLOW_ALL: True CORS_ORIGIN_WHITELIST: [] COURSES_WITH_UNSAFE_CODE: [] COURSE_ABOUT_VISIBILITY_PERMISSION: see_exists @@ -167,90 +167,90 @@ COURSE_AUTHORING_MICROFRONTEND_URL: null COURSE_CATALOG_API_URL: http://localhost:8008/api/v1 COURSE_CATALOG_URL_ROOT: http://localhost:8008 COURSE_CATALOG_VISIBILITY_PERMISSION: see_exists -COURSE_IMPORT_EXPORT_BUCKET: '' +COURSE_IMPORT_EXPORT_BUCKET: "" CREDENTIALS_INTERNAL_SERVICE_URL: http://localhost:8005 CREDENTIALS_PUBLIC_SERVICE_URL: http://localhost:8005 CREDIT_PROVIDER_SECRET_KEYS: {} -CROSS_DOMAIN_CSRF_COOKIE_DOMAIN: '' -CROSS_DOMAIN_CSRF_COOKIE_NAME: '' +CROSS_DOMAIN_CSRF_COOKIE_DOMAIN: "" +CROSS_DOMAIN_CSRF_COOKIE_NAME: "" CSRF_COOKIE_SECURE: false CSRF_TRUSTED_ORIGINS: [] DASHBOARD_COURSE_LIMIT: null DATABASES: - default: - ATOMIC_REQUESTS: true - CONN_MAX_AGE: 0 - ENGINE: django.db.backends.mysql - HOST: edx.devstack.mysql57 - NAME: edxapp - OPTIONS: - isolation_level: read committed - PASSWORD: password - PORT: '3306' - USER: edxapp001 - read_replica: - CONN_MAX_AGE: 0 - ENGINE: django.db.backends.mysql - HOST: edx.devstack.mysql57 - NAME: edxapp - OPTIONS: - isolation_level: read committed - PASSWORD: password - PORT: '3306' - USER: edxapp001 - student_module_history: - CONN_MAX_AGE: 0 - ENGINE: django.db.backends.mysql - HOST: edx.devstack.mysql57 - NAME: edxapp_csmh - OPTIONS: - isolation_level: read committed - PASSWORD: password - PORT: '3306' - USER: edxapp001 + default: + ATOMIC_REQUESTS: true + CONN_MAX_AGE: 0 + ENGINE: django.db.backends.mysql + HOST: edx.devstack.mysql57 + NAME: edxapp + OPTIONS: + isolation_level: read committed + PASSWORD: password + PORT: "3306" + USER: edxapp001 + read_replica: + CONN_MAX_AGE: 0 + ENGINE: django.db.backends.mysql + HOST: edx.devstack.mysql57 + NAME: edxapp + OPTIONS: + isolation_level: read committed + PASSWORD: password + PORT: "3306" + USER: edxapp001 + student_module_history: + CONN_MAX_AGE: 0 + ENGINE: django.db.backends.mysql + HOST: edx.devstack.mysql57 + NAME: edxapp_csmh + OPTIONS: + isolation_level: read committed + PASSWORD: password + PORT: "3306" + USER: edxapp001 DATA_DIR: /edx/var/edxapp DEFAULT_COURSE_VISIBILITY_IN_CATALOG: both DEFAULT_FEEDBACK_EMAIL: feedback@example.com DEFAULT_FILE_STORAGE: django.core.files.storage.FileSystemStorage DEFAULT_FROM_EMAIL: registration@example.com DEFAULT_JWT_ISSUER: - AUDIENCE: lms-key - ISSUER: http://edx.devstack.lms:18000/oauth2 - SECRET_KEY: lms-secret + AUDIENCE: lms-key + ISSUER: http://edx.devstack.lms:18000/oauth2 + SECRET_KEY: lms-secret DEFAULT_MOBILE_AVAILABLE: false -DEFAULT_SITE_THEME: '' +DEFAULT_SITE_THEME: "" DEPRECATED_ADVANCED_COMPONENT_TYPES: [] DJFS: - directory_root: /edx/var/edxapp/django-pyfs/static/django-pyfs - type: osfs - url_root: /static/django-pyfs + directory_root: /edx/var/edxapp/django-pyfs/static/django-pyfs + type: osfs + url_root: /static/django-pyfs DOC_STORE_CONFIG: - authsource: '' - collection: modulestore - connectTimeoutMS: 2000 - db: edxapp - host: + authsource: "" + collection: modulestore + connectTimeoutMS: 2000 + db: edxapp + host: - edx.devstack.mongo - password: password - port: 27017 - read_preference: PRIMARY - replicaSet: '' - socketTimeoutMS: 3000 - ssl: false - user: edxapp + password: password + port: 27017 + read_preference: PRIMARY + replicaSet: "" + socketTimeoutMS: 3000 + ssl: false + user: edxapp ECOMMERCE_API_SIGNING_KEY: lms-secret ECOMMERCE_API_URL: http://localhost:8002/api/v2 ECOMMERCE_PUBLIC_URL_ROOT: http://localhost:8002 EDXMKTG_USER_INFO_COOKIE_NAME: edx-user-info EDX_PLATFORM_REVISION: master ELASTIC_SEARCH_CONFIG: -- host: edx.devstack.elasticsearch + - host: edx.devstack.elasticsearch port: 9200 use_ssl: false EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend EMAIL_HOST: localhost -EMAIL_HOST_PASSWORD: '' -EMAIL_HOST_USER: '' +EMAIL_HOST_PASSWORD: "" +EMAIL_HOST_USER: "" EMAIL_PORT: 25 EMAIL_USE_TLS: false ENABLE_COMPREHENSIVE_THEMING: false @@ -264,79 +264,79 @@ FACEBOOK_API_VERSION: v2.1 FACEBOOK_APP_ID: FACEBOOK_APP_ID FACEBOOK_APP_SECRET: FACEBOOK_APP_SECRET FEATURES: - AUTH_USE_OPENID_PROVIDER: true - AUTOMATIC_AUTH_FOR_TESTING: false - CUSTOM_COURSES_EDX: false - ENABLE_BULK_ENROLLMENT_VIEW: false - ENABLE_COMBINED_LOGIN_REGISTRATION: true - ENABLE_CORS_HEADERS: false - ENABLE_COUNTRY_ACCESS: false - ENABLE_CREDIT_API: false - ENABLE_CREDIT_ELIGIBILITY: false - ENABLE_CROSS_DOMAIN_CSRF_COOKIE: false - ENABLE_CSMH_EXTENDED: true - ENABLE_DISCUSSION_HOME_PANEL: true - ENABLE_DISCUSSION_SERVICE: true - ENABLE_EDXNOTES: true - ENABLE_ENROLLMENT_RESET: false - ENABLE_EXPORT_GIT: false - ENABLE_GRADE_DOWNLOADS: true - ENABLE_LTI_PROVIDER: false - ENABLE_MKTG_SITE: false - ENABLE_MOBILE_REST_API: false - ENABLE_OAUTH2_PROVIDER: false - ENABLE_PUBLISHER: false - ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES: true - ENABLE_SPECIAL_EXAMS: false - ENABLE_SYSADMIN_DASHBOARD: false - ENABLE_THIRD_PARTY_AUTH: true - ENABLE_VIDEO_UPLOAD_PIPELINE: false - PREVIEW_LMS_BASE: preview.localhost:18000 - SHOW_FOOTER_LANGUAGE_SELECTOR: false - SHOW_HEADER_LANGUAGE_SELECTOR: false -FEEDBACK_SUBMISSION_EMAIL: '' + AUTH_USE_OPENID_PROVIDER: true + AUTOMATIC_AUTH_FOR_TESTING: false + CUSTOM_COURSES_EDX: false + ENABLE_BULK_ENROLLMENT_VIEW: true + ENABLE_COMBINED_LOGIN_REGISTRATION: true + ENABLE_CORS_HEADERS: false + ENABLE_COUNTRY_ACCESS: false + ENABLE_CREDIT_API: false + ENABLE_CREDIT_ELIGIBILITY: false + ENABLE_CROSS_DOMAIN_CSRF_COOKIE: false + ENABLE_CSMH_EXTENDED: true + ENABLE_DISCUSSION_HOME_PANEL: true + ENABLE_DISCUSSION_SERVICE: true + ENABLE_EDXNOTES: true + ENABLE_ENROLLMENT_RESET: false + ENABLE_EXPORT_GIT: false + ENABLE_GRADE_DOWNLOADS: true + ENABLE_LTI_PROVIDER: false + ENABLE_MKTG_SITE: false + ENABLE_MOBILE_REST_API: false + ENABLE_OAUTH2_PROVIDER: false + ENABLE_PUBLISHER: false + ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES: true + ENABLE_SPECIAL_EXAMS: false + ENABLE_SYSADMIN_DASHBOARD: false + ENABLE_THIRD_PARTY_AUTH: true + ENABLE_VIDEO_UPLOAD_PIPELINE: false + PREVIEW_LMS_BASE: preview.localhost:18000 + SHOW_FOOTER_LANGUAGE_SELECTOR: false + SHOW_HEADER_LANGUAGE_SELECTOR: false +FEEDBACK_SUBMISSION_EMAIL: "" FERNET_KEYS: -- DUMMY KEY CHANGE BEFORE GOING TO PRODUCTION + - DUMMY KEY CHANGE BEFORE GOING TO PRODUCTION FILE_UPLOAD_STORAGE_BUCKET_NAME: SET-ME-PLEASE (ex. bucket-name) FILE_UPLOAD_STORAGE_PREFIX: submissions_attachments FINANCIAL_REPORTS: - BUCKET: null - ROOT_PATH: sandbox - STORAGE_TYPE: localfs + BUCKET: null + ROOT_PATH: sandbox + STORAGE_TYPE: localfs FOOTER_ORGANIZATION_IMAGE: images/logo.png GITHUB_REPO_ROOT: /edx/var/edxapp/data GIT_REPO_EXPORT_DIR: /edx/var/edxapp/export_course_repos GOOGLE_ANALYTICS_ACCOUNT: null GRADES_DOWNLOAD: - BUCKET: '' - ROOT_PATH: '' - STORAGE_CLASS: django.core.files.storage.FileSystemStorage - STORAGE_KWARGS: - location: /tmp/edx-s3/grades - STORAGE_TYPE: '' + BUCKET: "" + ROOT_PATH: "" + STORAGE_CLASS: django.core.files.storage.FileSystemStorage + STORAGE_KWARGS: + location: /tmp/edx-s3/grades + STORAGE_TYPE: "" HELP_TOKENS_BOOKS: - course_author: http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course - learner: http://edx.readthedocs.io/projects/open-edx-learner-guide + course_author: http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course + learner: http://edx.readthedocs.io/projects/open-edx-learner-guide ICP_LICENSE: null ICP_LICENSE_INFO: {} IDA_LOGOUT_URI_LIST: [] -ID_VERIFICATION_SUPPORT_LINK: '' +ID_VERIFICATION_SUPPORT_LINK: "" INTEGRATED_CHANNELS_API_CHUNK_TRANSMISSION_LIMIT: - SAP: 1 + SAP: 1 JWT_AUTH: - JWT_AUDIENCE: lms-key - JWT_AUTH_COOKIE_HEADER_PAYLOAD: edx-jwt-cookie-header-payload - JWT_AUTH_COOKIE_SIGNATURE: edx-jwt-cookie-signature - JWT_AUTH_REFRESH_COOKIE: edx-jwt-refresh-cookie - JWT_ISSUER: http://edx.devstack.lms:18000/oauth2 - JWT_ISSUERS: - - AUDIENCE: lms-key - ISSUER: http://edx.devstack.lms:18000/oauth2 - SECRET_KEY: lms-secret - JWT_PRIVATE_SIGNING_JWK: None - JWT_PUBLIC_SIGNING_JWK_SET: '' - JWT_SECRET_KEY: lms-secret - JWT_SIGNING_ALGORITHM: null + JWT_AUDIENCE: lms-key + JWT_AUTH_COOKIE_HEADER_PAYLOAD: edx-jwt-cookie-header-payload + JWT_AUTH_COOKIE_SIGNATURE: edx-jwt-cookie-signature + JWT_AUTH_REFRESH_COOKIE: edx-jwt-refresh-cookie + JWT_ISSUER: http://edx.devstack.lms:18000/oauth2 + JWT_ISSUERS: + - AUDIENCE: lms-key + ISSUER: http://edx.devstack.lms:18000/oauth2 + SECRET_KEY: lms-secret + JWT_PRIVATE_SIGNING_JWK: None + JWT_PUBLIC_SIGNING_JWK_SET: "" + JWT_SECRET_KEY: lms-secret + JWT_SIGNING_ALGORITHM: null JWT_EXPIRATION: 30 JWT_ISSUER: http://edx.devstack.lms:18000/oauth2 JWT_PRIVATE_SIGNING_KEY: null @@ -357,117 +357,117 @@ MKTG_URLS: {} MKTG_URL_LINK_MAP: {} MOBILE_STORE_ACE_URLS: {} MODULESTORE: - default: - ENGINE: xmodule.modulestore.mixed.MixedModuleStore - OPTIONS: - mappings: {} - stores: - - DOC_STORE_CONFIG: - authsource: '' - collection: modulestore - connectTimeoutMS: 2000 - db: edxapp - host: - - edx.devstack.mongo - password: password - port: 27017 - read_preference: PRIMARY - replicaSet: '' - socketTimeoutMS: 3000 - ssl: false - user: edxapp - ENGINE: xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore - NAME: split - OPTIONS: - default_class: xmodule.hidden_module.HiddenDescriptor - fs_root: /edx/var/edxapp/data - render_template: common.djangoapps.edxmako.shortcuts.render_to_string - - DOC_STORE_CONFIG: - authsource: '' - collection: modulestore - connectTimeoutMS: 2000 - db: edxapp - host: - - edx.devstack.mongo - password: password - port: 27017 - read_preference: PRIMARY - replicaSet: '' - socketTimeoutMS: 3000 - ssl: false - user: edxapp - ENGINE: xmodule.modulestore.mongo.DraftMongoModuleStore - NAME: draft - OPTIONS: - default_class: xmodule.hidden_module.HiddenDescriptor - fs_root: /edx/var/edxapp/data - render_template: common.djangoapps.edxmako.shortcuts.render_to_string + default: + ENGINE: xmodule.modulestore.mixed.MixedModuleStore + OPTIONS: + mappings: {} + stores: + - DOC_STORE_CONFIG: + authsource: "" + collection: modulestore + connectTimeoutMS: 2000 + db: edxapp + host: + - edx.devstack.mongo + password: password + port: 27017 + read_preference: PRIMARY + replicaSet: "" + socketTimeoutMS: 3000 + ssl: false + user: edxapp + ENGINE: xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore + NAME: split + OPTIONS: + default_class: xmodule.hidden_module.HiddenDescriptor + fs_root: /edx/var/edxapp/data + render_template: common.djangoapps.edxmako.shortcuts.render_to_string + - DOC_STORE_CONFIG: + authsource: "" + collection: modulestore + connectTimeoutMS: 2000 + db: edxapp + host: + - edx.devstack.mongo + password: password + port: 27017 + read_preference: PRIMARY + replicaSet: "" + socketTimeoutMS: 3000 + ssl: false + user: edxapp + ENGINE: xmodule.modulestore.mongo.DraftMongoModuleStore + NAME: draft + OPTIONS: + default_class: xmodule.hidden_module.HiddenDescriptor + fs_root: /edx/var/edxapp/data + render_template: common.djangoapps.edxmako.shortcuts.render_to_string ORA2_FILE_PREFIX: default_env-default_deployment/ora2 PARSE_KEYS: {} -PARTNER_SUPPORT_EMAIL: '' +PARTNER_SUPPORT_EMAIL: "" PASSWORD_POLICY_COMPLIANCE_ROLLOUT_CONFIG: - ENFORCE_COMPLIANCE_ON_LOGIN: false -PASSWORD_RESET_SUPPORT_LINK: '' + ENFORCE_COMPLIANCE_ON_LOGIN: false +PASSWORD_RESET_SUPPORT_LINK: "" PAYMENT_SUPPORT_EMAIL: billing@example.com PLATFORM_DESCRIPTION: Your Platform Description Here PLATFORM_FACEBOOK_ACCOUNT: http://www.facebook.com/YourPlatformFacebookAccount PLATFORM_NAME: Your Platform Name Here -PLATFORM_TWITTER_ACCOUNT: '@YourPlatformTwitterAccount' +PLATFORM_TWITTER_ACCOUNT: "@YourPlatformTwitterAccount" POLICY_CHANGE_GRADES_ROUTING_KEY: edx.lms.core.default SINGLE_LEARNER_COURSE_REGRADE_ROUTING_KEY: edx.lms.core.default PREPEND_LOCALE_PATHS: [] PRESS_EMAIL: press@example.com PROCTORING_BACKENDS: - DEFAULT: 'null' - 'null': {} + DEFAULT: "null" + "null": {} PROCTORING_SETTINGS: {} REGISTRATION_EXTRA_FIELDS: - city: hidden - confirm_email: hidden - country: required - gender: optional - goals: optional - honor_code: required - level_of_education: optional - mailing_address: hidden - terms_of_service: hidden - year_of_birth: optional + city: hidden + confirm_email: hidden + country: required + gender: optional + goals: optional + honor_code: required + level_of_education: optional + mailing_address: hidden + terms_of_service: hidden + year_of_birth: optional RETIRED_EMAIL_DOMAIN: retired.invalid RETIRED_EMAIL_PREFIX: retired__user_ RETIRED_USERNAME_PREFIX: retired__user_ RETIRED_USER_SALTS: -- OVERRIDE ME WITH A RANDOM VALUE -- ROTATE SALTS BY APPENDING NEW VALUES + - OVERRIDE ME WITH A RANDOM VALUE + - ROTATE SALTS BY APPENDING NEW VALUES RETIREMENT_SERVICE_WORKER_USERNAME: retirement_worker RETIREMENT_STATES: -- PENDING -- ERRORED -- ABORTED -- COMPLETE + - PENDING + - ERRORED + - ABORTED + - COMPLETE SECRET_KEY: DUMMY KEY ONLY FOR TO DEVSTACK SEGMENT_KEY: null SERVER_EMAIL: sre@example.com -SESSION_COOKIE_DOMAIN: '' +SESSION_COOKIE_DOMAIN: "" SESSION_COOKIE_NAME: sessionid SESSION_COOKIE_SECURE: false SESSION_SAVE_EVERY_REQUEST: false SITE_NAME: localhost -SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: '' +SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: "" SOCIAL_AUTH_SAML_SP_PRIVATE_KEY_DICT: {} -SOCIAL_AUTH_SAML_SP_PUBLIC_CERT: '' +SOCIAL_AUTH_SAML_SP_PUBLIC_CERT: "" SOCIAL_AUTH_SAML_SP_PUBLIC_CERT_DICT: {} SOCIAL_MEDIA_FOOTER_URLS: {} SOCIAL_SHARING_SETTINGS: - CERTIFICATE_FACEBOOK: false - CERTIFICATE_TWITTER: false - CUSTOM_COURSE_URLS: false - DASHBOARD_FACEBOOK: false - DASHBOARD_TWITTER: false + CERTIFICATE_FACEBOOK: false + CERTIFICATE_TWITTER: false + CUSTOM_COURSE_URLS: false + DASHBOARD_FACEBOOK: false + DASHBOARD_TWITTER: false STATIC_ROOT_BASE: /edx/var/edxapp/staticfiles STATIC_URL_BASE: /static/ STUDIO_NAME: Studio STUDIO_SHORT_NAME: Studio -SUPPORT_SITE_LINK: '' +SUPPORT_SITE_LINK: "" SWIFT_AUTH_URL: null SWIFT_AUTH_VERSION: null SWIFT_KEY: null @@ -478,7 +478,7 @@ SWIFT_TENANT_ID: null SWIFT_TENANT_NAME: null SWIFT_USERNAME: null SWIFT_USE_TEMP_URLS: false -SYSLOG_SERVER: '' +SYSLOG_SERVER: "" SYSTEM_WIDE_ROLE_CLASSES: [] TECH_SUPPORT_EMAIL: technical@example.com TIME_ZONE: America/New_York @@ -486,39 +486,39 @@ UNIVERSITY_EMAIL: university@example.com USERNAME_REPLACEMENT_WORKER: OVERRIDE THIS WITH A VALID USERNAME VIDEO_IMAGE_MAX_AGE: 31536000 VIDEO_IMAGE_SETTINGS: - DIRECTORY_PREFIX: video-images/ - STORAGE_KWARGS: - base_url: /media/ - location: /edx/var/edxapp/media// - VIDEO_IMAGE_MAX_BYTES: 2097152 - VIDEO_IMAGE_MIN_BYTES: 2048 + DIRECTORY_PREFIX: video-images/ + STORAGE_KWARGS: + base_url: /media/ + location: /edx/var/edxapp/media// + VIDEO_IMAGE_MAX_BYTES: 2097152 + VIDEO_IMAGE_MIN_BYTES: 2048 VIDEO_TRANSCRIPTS_MAX_AGE: 31536000 VIDEO_TRANSCRIPTS_SETTINGS: - DIRECTORY_PREFIX: video-transcripts/ - STORAGE_KWARGS: - base_url: /media/ - location: /edx/var/edxapp/media// - VIDEO_TRANSCRIPTS_MAX_BYTES: 3145728 + DIRECTORY_PREFIX: video-transcripts/ + STORAGE_KWARGS: + base_url: /media/ + location: /edx/var/edxapp/media// + VIDEO_TRANSCRIPTS_MAX_BYTES: 3145728 VIDEO_UPLOAD_PIPELINE: - BUCKET: '' - ROOT_PATH: '' + BUCKET: "" + ROOT_PATH: "" WIKI_ENABLED: true XBLOCK_FS_STORAGE_BUCKET: null XBLOCK_FS_STORAGE_PREFIX: null XBLOCK_SETTINGS: {} XQUEUE_INTERFACE: - basic_auth: + basic_auth: - edx - edx - django_auth: - password: password - username: lms - url: http://edx.devstack.xqueue:18040 + django_auth: + password: password + username: lms + url: http://edx.devstack.xqueue:18040 X_FRAME_OPTIONS: DENY YOUTUBE_API_KEY: PUT_YOUR_API_KEY_HERE -ZENDESK_API_KEY: '' +ZENDESK_API_KEY: "" ZENDESK_CUSTOM_FIELDS: {} ZENDESK_GROUP_ID_MAPPING: {} -ZENDESK_OAUTH_ACCESS_TOKEN: '' -ZENDESK_URL: '' -ZENDESK_USER: '' +ZENDESK_OAUTH_ACCESS_TOKEN: "" +ZENDESK_URL: "" +ZENDESK_USER: "" diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index c76c24bac8cc..9e80078b1f29 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -111,10 +111,11 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing-function-docstring # We always want the toolbar on devstack unless running tests from another Docker container - hostname = request.get_host() - if hostname.startswith('edx.devstack.studio:') or hostname.startswith('studio.devstack.edx:'): - return False - return True + # hostname = request.get_host() + # if hostname.startswith('edx.devstack.studio:') or hostname.startswith('studio.devstack.edx:'): + # return False + # return True + return False ################################ MILESTONES ################################ @@ -136,13 +137,22 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing XBLOCK_SETTINGS.update({'VideoBlock': {'licensing_enabled': True}}) ################################ SEARCH INDEX ################################ -FEATURES['ENABLE_COURSEWARE_INDEX'] = False -FEATURES['ENABLE_LIBRARY_INDEX'] = False -FEATURES['ENABLE_CONTENT_LIBRARY_INDEX'] = False +FEATURES['ENABLE_COURSEWARE_INDEX'] = True +FEATURES['ENABLE_LIBRARY_INDEX'] = True +FEATURES['ENABLE_CONTENT_LIBRARY_INDEX'] = True SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" +ELASTIC_SEARCH_CONFIG = [ + { + 'use_ssl': False, + 'host': '13.214.22.79', + 'port': 9201 + } +] + + ################################ COURSE DISCUSSIONS ########################### -FEATURES['ENABLE_DISCUSSION_SERVICE'] = True +FEATURES['ENABLE_DISCUSSION_SERVICE'] = False ################################ CREDENTIALS ########################### CREDENTIALS_SERVICE_USERNAME = 'credentials_worker' diff --git a/cms/envs/devstack_docker.py b/cms/envs/devstack_docker.py index 2eece814deae..0b9a768552c9 100644 --- a/cms/envs/devstack_docker.py +++ b/cms/envs/devstack_docker.py @@ -1,3 +1,12 @@ """ Overrides for Docker-based devstack. """ from .devstack import * # pylint: disable=wildcard-import, unused-wildcard-import + +LOGGING['handlers']['tracking'] = { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': '/edx/var/log/tracking/tracking.log', + 'formatter': 'raw', +} + +LOGGING['loggers']['tracking']['handlers'] = ['tracking'] diff --git a/cms/envs/production.py b/cms/envs/production.py index 6431ad95c15d..d962033c7756 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -580,14 +580,14 @@ def get_env_setting(setting): # This is at the bottom because it is going to load more settings after base settings are loaded add_plugins(__name__, ProjectType.CMS, SettingsType.PRODUCTION) - +CORS_ORIGIN_ALLOW_ALL = True ############# CORS headers for cross-domain requests ################# if FEATURES.get('ENABLE_CORS_HEADERS'): CORS_ALLOW_CREDENTIALS = True - CORS_ORIGIN_WHITELIST = ENV_TOKENS.get('CORS_ORIGIN_WHITELIST', ()) + CORS_ORIGIN_WHITELIST = () + - CORS_ORIGIN_ALLOW_ALL = ENV_TOKENS.get('CORS_ORIGIN_ALLOW_ALL', False) - CORS_ALLOW_INSECURE = ENV_TOKENS.get('CORS_ALLOW_INSECURE', False) + CORS_ALLOW_INSECURE = ENV_TOKENS.get('CORS_ALLOW_INSECURE', True) CORS_ALLOW_HEADERS = corsheaders_default_headers + ( 'use-jwt-cookie', ) diff --git a/cms/envs/test.py b/cms/envs/test.py index c31635d6bf58..8593f9686788 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -268,7 +268,7 @@ # Courseware Search Index FEATURES['ENABLE_COURSEWARE_INDEX'] = True FEATURES['ENABLE_LIBRARY_INDEX'] = True -FEATURES['ENABLE_CONTENT_LIBRARY_INDEX'] = False +FEATURES['ENABLE_CONTENT_LIBRARY_INDEX'] = True SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine" FEATURES['ENABLE_ENROLLMENT_TRACK_USER_PARTITION'] = True diff --git a/cms/static/images/favicon.ico b/cms/static/images/favicon.ico new file mode 100644 index 000000000000..d6442392e271 Binary files /dev/null and b/cms/static/images/favicon.ico differ diff --git a/cms/static/images/logo-large.png b/cms/static/images/logo-large.png new file mode 100644 index 000000000000..a860dc344bc0 Binary files /dev/null and b/cms/static/images/logo-large.png differ diff --git a/cms/static/images/logo.png b/cms/static/images/logo.png new file mode 100644 index 000000000000..94db2e8a2158 Binary files /dev/null and b/cms/static/images/logo.png differ diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 464f68690688..7da998eb781e 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -44,6 +44,10 @@ button { outline: none; } +.none { + display: none !important; +} + a { @include transition(color $tmg-f2 ease-in-out 0s); diff --git a/cms/static/sass/_funix-studio.scss b/cms/static/sass/_funix-studio.scss new file mode 100644 index 000000000000..38c899f373d6 --- /dev/null +++ b/cms/static/sass/_funix-studio.scss @@ -0,0 +1,500 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto+Serif:opsz,wght@8..144,500&family=Roboto:wght@400;500&display=swap'); + +.wrapper-xblock.level-element article.xblock-render { + --color-active: #007ae6; + --color-active-light: #f2f9ff; + --color-hover: #025dae; + --background-color-hover: #eef7ff; + --color-white: #ffffff; + --background-content: #fafbfb; + --color-stroke: #d7d7d7; + --color-disable: #c5c5c5; + --color-text-inactive: #7b7b7b; + --color-text-default: #2c3744; + --color-notification-critical: #d82c0d; + --color-warning: #ffb934; + --color-warning-light: #fffcf5; + --color-success: #5aa447; + --color-success-light: #f2ffee; + --background-selected: #f1f2f3; + --color-subdued: #576f8a; + --color-subdued-light: rgba(87, 111, 138, 0.05); + --border-radius: 6px; + + --color-critical: #d82c0d; + --color-critical-light: #fff1f1; + --color-leaf: #06aaa5; + --color-leaf-light: #faffff; + + font-size: 16px; + + button, + input, + select, + textarea, + form#wiki_revision .CodeMirror { + font-family: 'Roboto', sans-serif; + } + + *, + .xmodule_display.xmodule_HtmlBlock * { + font-family: 'Roboto', sans-serif; + color: var(--color-text-default); + } + + .xmodule_display.xmodule_HtmlBlock h1, + .xmodule_display.xmodule_HtmlBlock h2, + .xmodule_display.xmodule_HtmlBlock h3, + .xmodule_display.xmodule_HtmlBlock h4, + .xmodule_display.xmodule_HtmlBlock h5, + .xmodule_display.xmodule_HtmlBlock h6, + .xmodule_display.xmodule_HtmlBlock strong, + .xmodule_display.xmodule_HtmlBlock i, + .xmodule_display.xmodule_HtmlBlock a, + .xmodule_display.xmodule_HtmlBlock p { + line-height: 1.5; + } + + strong span, + b span, + .xmodule_display.xmodule_HtmlBlock strong span, + .xmodule_display.xmodule_HtmlBlock b span { + font-weight: 500; + } + + + h1, + .course-wrapper .course-content h1, + .course-wrapper .courseware-results-wrapper h1, + .xmodule_display.xmodule_HtmlBlock h1 { + font-size: 2em; + font-weight: 500; + font-family: 'Roboto Serif', sans-serif; + color: var(--color-text-default); + font: 500 2em 'Roboto Serif', sans-serif; + margin-bottom: 2em; + line-height: 1.5; + } + + h2, + .xmodule_display.xmodule_HtmlBlock h2, + .xblock .xblock h2, + .hd-2, + .xblock .xblock h2 { + font-size: 1.5em; + font-weight: 500; + font-family: 'Roboto', sans-serif; + color: var(--color-text-default); + font: 500 1.5em 'Roboto', sans-serif; + margin: 0 0 2em 0; + line-height: 1.5; + } + + h3, + .xmodule_display.xmodule_HtmlBlock h3 { + font-size: 1.25em; + font-weight: 500; + font-family: 'Roboto', sans-serif; + color: var(--color-text-default); + font: 500 1.25em 'Roboto', sans-serif; + line-height: 1.5; + margin: 0 0 .5em 0; + } + + h4, + .xmodule_display.xmodule_HtmlBlock h4 { + font-size: 1.125em; + font-weight: 500; + font-family: 'Roboto', sans-serif; + color: var(--color-text-default); + font: 500 1.125em 'Roboto', sans-serif; + line-height: 1.5; + } + + p, + .xmodule_display.xmodule_HtmlBlock p { + font-size: 1em; + font-weight: 400; + font-family: 'Roboto', sans-serif; + color: var(--color-text-default); + font: 400 1em 'Roboto', sans-serif; + margin-bottom: 1em; + line-height: 1.5; + } + + span, + i, + .xmodule_display.xmodule_HtmlBlock i, + .xmodule_display.xmodule_HtmlBlock span { + font-size: inherit; + font-weight: inherit; + font-family: inherit; + color: inherit; + font: inherit; + margin-bottom: 0; + line-height: inherit; + } + + a, + .xmodule_display.xmodule_HtmlBlock a:link, + .xmodule_display.xmodule_HtmlBlock a:visited, + .xmodule_display.xmodule_HtmlBlock a:active, + .xmodule_display.xmodule_HtmlBlock a:focus, + .xmodule_display.xmodule_HtmlBlock a * { + font-weight: inherit; + color: var(--color-active); + text-decoration: underline; + font-size: inherit; + font-family: inherit; + font: inherit; + cursor: pointer; + } + + a:hover, + a:hover:not(.btn), + a:focus:not(.btn), + .xmodule_display.xmodule_HtmlBlock a:hover, + .xmodule_display.xmodule_HtmlBlock a:hover * { + color: var(--color-hover); + } + + + ul, + .course-wrapper .course-content .vert-mod>div ul, + .course-wrapper .courseware-results-wrapper .vert-mod>div ul, + .xmodule_display.xmodule_HtmlBlock ul { + padding: 0 0 0 32px; + margin-bottom: 1em; + margin-top: 0; + list-style-type: disc; + } + + ul ul, + ul, + .course-wrapper .course-content .vert-mod>div ul ul, + .course-wrapper .courseware-results-wrapper .vert-mod>div ul ul, + .xmodule_display.xmodule_HtmlBlock ul ul { + list-style-type: circle; + margin-bottom: 0; + } + + .course-wrapper .course-content ul li, + .course-wrapper .courseware-results-wrapper ul li { + margin-bottom: 0.375em; + } + + .course-wrapper .course-content ul li:last-child, + .course-wrapper .courseware-results-wrapper ul li:last-child { + margin-bottom: 0; + } + + + .xmodule_display.xmodule_HtmlBlock p+p, + .xmodule_display.xmodule_HtmlBlock ul+p, + .xmodule_display.xmodule_HtmlBlock ol+p { + margin-top: 0; + } + + .xmodule_display.xmodule_HtmlBlock p+h2, + .xmodule_display.xmodule_HtmlBlock ul+h2, + .xmodule_display.xmodule_HtmlBlock ol+h2 { + margin-top: 1em; + } + + // image + img, + .xmodule_display.xmodule_HtmlBlock img { + display: block; + border: 1px solid var(--color-stroke); + border-radius: var(--border-radius); + overflow: hidden; + margin-top: .5em; + margin: .5rem auto 1rem; + max-width: 100%; + box-sizing: border-box; + height: auto; + } + + // table + table, + .xmodule_display.xmodule_HtmlBlock table { + box-sizing: border-box; + width: 100%; + } + + tr:nth-child(odd), + .xmodule_display.xmodule_HtmlBlock table tr:nth-child(odd) { + background: #f6f6f7; + border: none; + + } + + tr:nth-child(even), + .xmodule_display.xmodule_HtmlBlock table tr:nth-child(even) { + background: white; + border: none; + } + + th, + .xmodule_display.xmodule_HtmlBlock table th { + background: #e6e6e8; + } + + td, + th, + .xmodule_display.xmodule_HtmlBlock table td, + .xmodule_display.xmodule_HtmlBlock table th { + border-right: 1px solid var(--color-stroke); + border-left: 1px solid transparent; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + } + + td:last-child, + th:last-child, + .xmodule_display.xmodule_HtmlBlock table td:last-child, + .xmodule_display.xmodule_HtmlBlock table th:last-child { + border-right: 1px solid transparent; + } + + td:last-child, + th:last-child, + .xmodule_display.xmodule_HtmlBlock table tr:last-child td { + border-bottom: 1px solid var(--color-stroke); + } + + // details - summary + details, + .xmodule_display.xmodule_HtmlBlock details { + margin-bottom: 1em; + padding: 0; + border-radius: var(--border-radius); + overflow: hidden; + transition: .2s; + max-height: none; + + * { + line-height: 1.5 !important; + } + + // -- auto subdued -- + background: var(--color-subdued-light); + border-left: 2px solid var(--color-subdued); + + >summary { + color: var(--color-subdued); + } + + // --- + + >summary { + list-style: none; + line-height: 1.5; + font: 500 1em 'Roboto', sans-serif !important; + margin: 0 !important; + cursor: pointer; + padding: .75em 1em .75em 1em; + position: relative; + + &::-webkit-details-marker { + display: none; + } + + &::after { + position: absolute; + display: block; + content: ''; + width: .5em; + height: .5em; + border-right: 2px solid var(--color-text-default); + border-bottom: 2px solid var(--color-text-default); + right: 1em; + top: 50%; + transform: translateY(-50%) rotate(-45deg); + transition: .2s; + } + } + + &[open] { + >summary { + + &::after { + transform: translateY(-50%) rotate(45deg); + } + } + } + + >div { + padding: 0 1em 0 1em; + } + + &.details-primary { + background: var(--color-active-light); + border-left: 2px solid var(--color-active); + + >summary { + color: var(--color-active); + } + } + + &.details-subdued { + background: var(--color-subdued-light); + border-left: 2px solid var(--color-subdued); + + >summary { + color: var(--color-subdued); + } + } + + &.details-success { + background: var(--color-success-light); + border-left: 2px solid var(--color-success); + + >summary { + color: var(--color-success); + } + } + + &.details-warning { + background: var(--color-warning-light); + border-left: 2px solid var(--color-warning); + + >summary { + color: var(--color-warning); + } + } + + &.details-critical { + background: var(--color-critical-light); + border-left: 2px solid var(--color-critical); + + >summary { + color: var(--color-critical); + } + } + + &.details-leaf { + background: var(--color-leaf-light); + border-left: 2px solid var(--color-leaf); + + >summary { + color: var(--color-leaf); + } + } + } + + // =================== components ========================== + // =================== info box ========================== + .informa { + border-radius: var(--border-radius); + overflow: hidden; + padding: .75em 1em; + margin-bottom: 1em; + + >p { + font: 500 1em 'Roboto', sans-serif !important; + margin-bottom: 0 !important; + } + + >div { + margin: 0; + padding: 0; + + p { + font: 400 1em 'Roboto', sans-serif; + color: var(--text-color-default); + line-height: 1.5; + } + + p, + ul { + &:last-child { + margin-bottom: 0; + } + } + + i, + span, + a { + font-size: inherit; + color: inherit; + font-weight: inherit; + font-family: inherit; + } + + a { + color: var(--color-active); + font-weight: 500; + text-decoration: underline; + + &:hover { + color: var(--color-hover); + } + } + } + + // -- auto subdued -- + >p { + color: var(--color-subdued); + } + + border-left: 2px solid var(--color-subdued); + background: var(--color-subdued-light); + + // ---- + + &.informa-primary { + >p { + color: var(--color-active); + } + + border-left: 2px solid var(--color-active); + background: var(--color-active-light); + } + + &.informa-warning { + >p { + color: var(--color-warning); + } + + border-left: 2px solid var(--color-warning); + background: var(--color-warning-light); + } + + &.informa-success { + >p { + color: var(--color-success); + } + + border-left: 2px solid var(--color-success); + background: var(--color-success-light); + } + + &.informa-critical { + >p { + color: var(--color-critical); + } + + border-left: 2px solid var(--color-critical); + background: var(--color-critical-light); + } + + &.informa-subdued { + >p { + color: var(--color-subdued); + } + + border-left: 2px solid var(--color-subdued); + background: var(--color-subdued-light); + } + + &.informa-leaf { + >p { + color: var(--color-leaf); + } + + border-left: 2px solid var(--color-leaf); + background: var(--color-leaf-light); + } + } +} \ No newline at end of file diff --git a/cms/static/sass/_reset.scss b/cms/static/sass/_reset.scss index daaf34d842d3..aec875d84d4f 100644 --- a/cms/static/sass/_reset.scss +++ b/cms/static/sass/_reset.scss @@ -7,6 +7,9 @@ // } // better text rendering/kerning through SVG +.none{ + display: none !important; +} * { text-rendering: optimizeLegibility; } diff --git a/cms/static/sass/funix/base/README.md b/cms/static/sass/funix/base/README.md new file mode 100755 index 000000000000..3bffe2e44a5b --- /dev/null +++ b/cms/static/sass/funix/base/README.md @@ -0,0 +1 @@ +This folder contains files used for all over the page such as unit, header, footer, index page. diff --git a/cms/static/sass/funix/base/_build.scss b/cms/static/sass/funix/base/_build.scss new file mode 100755 index 000000000000..6dbb6fcd230b --- /dev/null +++ b/cms/static/sass/funix/base/_build.scss @@ -0,0 +1,5 @@ +@import 'variables'; +@import 'mixins'; +@import 'reset'; + +// @include base; \ No newline at end of file diff --git a/cms/static/sass/funix/base/_buttons.scss b/cms/static/sass/funix/base/_buttons.scss new file mode 100755 index 000000000000..b48050e91e6f --- /dev/null +++ b/cms/static/sass/funix/base/_buttons.scss @@ -0,0 +1,8 @@ +.btn, +button.submit, +.btn-default, +.btn-primary, +.btn-brand, +.btn-upgrade { + @include btn-primary; +} \ No newline at end of file diff --git a/cms/static/sass/funix/base/_mixins.scss b/cms/static/sass/funix/base/_mixins.scss new file mode 100755 index 000000000000..0df59b71a57f --- /dev/null +++ b/cms/static/sass/funix/base/_mixins.scss @@ -0,0 +1,157 @@ +@mixin base { + + h1, + h2, + h3, + h4, + h5, + h6, + p, + label { + text-align: left; + font-family: $font-family-default; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + font-weight: $fw-medium; + } + + h1 { + font-size: $fz-h1; + font-family: $font-family-default; + } + + h2 { + font-size: $fz-h2; + } + + h3 { + font-size: $fz-h3; + } + + h4 { + font-size: $fz-h4; + } + + h5 { + font-size: $fz-h5; + } + + h6 { + font-size: $fz-h6; + } + + p { + font-size: $fz-p; + } + + strong, + b { + font-weight: $fw-medium; + } + + button, + input, + select, + textarea { + font-family: inherit; + } + + .btn, + button.submit, + .btn-default, + .btn-primary, + .btn-brand, + .btn-upgrade, + input[type=submit] { + @include btn-primary; + } + + a:not(.btn):hover, + a:not(.btn):focus, + a:visited:not(.btn):hover, + a:visited:not(.btn):focus { + color: $color-hover; + font-weight: $fw-medium; + text-decoration: none; + } +} + +@mixin btn { + height: 2.375rem; + padding: 0 1.25rem; + line-height: 2.375rem; + border-radius: 4px; + display: inline-block; + font-weight: 500; + font-family: $font-family-default; + + &:hover { + text-decoration: none + } + + &:disabled { + position: none; + } +} + +@mixin btn-primary { + @include btn; + + color: white; + background: $color-active; + border: 1px solid $color-active; + + &:hover { + background: $color-hover; + border: 1px solid $color-hover; + } + + &:disabled { + background: $color-disable; + border: 1px solid $color-disable; + color: white; + } +} + +@mixin btn-secondary { + @include btn; + + color: $color-active; + background: white; + border: 1px solid $color-active; + + &:hover { + color: $color-hover; + border: 1px solid $color-hover; + } + + &:disabled { + color: white; + border: 1px solid $color-text-inactive; + } +} + +@mixin btn-text { + @include btn; + + border: none; + background: white; + color: $color-active; + + &:hover { + color: $color-hover; + border: none; + background: white; + } + + &:disabled { + color: $color-disable; + border: none; + } +} \ No newline at end of file diff --git a/cms/static/sass/funix/base/_reset.scss b/cms/static/sass/funix/base/_reset.scss new file mode 100755 index 000000000000..8cd5677c800f --- /dev/null +++ b/cms/static/sass/funix/base/_reset.scss @@ -0,0 +1,14 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto+Serif:opsz,wght@8..144,500&family=Roboto:wght@400;500&display=swap'); + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + line-height: 1.5; + color: $color-text-default; + font-size: 16px; + font-family: $font-family-default; +} \ No newline at end of file diff --git a/cms/static/sass/funix/base/_variables.scss b/cms/static/sass/funix/base/_variables.scss new file mode 100755 index 000000000000..e37316c2ab04 --- /dev/null +++ b/cms/static/sass/funix/base/_variables.scss @@ -0,0 +1,47 @@ +$color-active: #007ae6; +$color-active-light: #f2f9ff; +$color-hover: #025dae; + +$background-color-hover: #eef7ff; +$background-content: #fafbfb; +$background-selected: #f1f2f3; + +$color-white: #ffffff; +$color-stroke: #d7d7d7; + +$color-disable: #c5c5c5; +$color-text-inactive: #7b7b7b; +$color-text-default: #2c3744; + +$color-warning: #ffb934; +$color-warning-light: #fffcf5; + +$color-success: #5aa447; +$color-success-light: #f2ffee; + +$color-subdued: #576f8a; +$color-subdued-light: rgba(87, 111, 138, 0.05); + +$color-critical: #d82c0d; +$color-critical-light: #fff1f1; + +$color-leaf: #06aaa5; +$color-leaf-light: #faffff; + +$spacing-1: 24px; +$spacing-2: 48px; + +$border-radius: 6px; + +$fw-medium: 500; +$fw-normal: 400; + +$font-family-default: 'Roboto', sans-serif; + +$fz-h1: 2.75em; +$fz-h2: 2.38em; +$fz-h3: 1.75em; +$fz-h4: 1.56em; +$fz-h5: 1.31em; +$fz-h6: 1.06em; +$fz-p: 1em; diff --git a/cms/static/sass/funix/unit/README.md b/cms/static/sass/funix/unit/README.md new file mode 100755 index 000000000000..d7f9f5c56949 --- /dev/null +++ b/cms/static/sass/funix/unit/README.md @@ -0,0 +1 @@ +This folder contains scss files used for only unit content. \ No newline at end of file diff --git a/cms/static/sass/funix/unit/_build.scss b/cms/static/sass/funix/unit/_build.scss new file mode 100755 index 000000000000..b636eb202505 --- /dev/null +++ b/cms/static/sass/funix/unit/_build.scss @@ -0,0 +1,13 @@ +@import '../base/build'; + +@import 'mixins/typography'; +@import 'mixins/problem'; +@import 'mixins/table'; +@import 'mixins/details'; +@import 'mixins/image'; +@import 'mixins/buttons'; +@import 'mixins/common'; + +@import 'components/xmodule-HtmlBlock'; +@import 'components/xmodule-ProblemBlock'; +@import 'components/xmodule-VideoBlock'; diff --git a/cms/static/sass/funix/unit/components/README.md b/cms/static/sass/funix/unit/components/README.md new file mode 100755 index 000000000000..8e088e07c8f9 --- /dev/null +++ b/cms/static/sass/funix/unit/components/README.md @@ -0,0 +1 @@ +Scss for specific component to avoid breaking other component such as video editor in Vdieo Component. \ No newline at end of file diff --git a/cms/static/sass/funix/unit/components/_xmodule-HtmlBlock.scss b/cms/static/sass/funix/unit/components/_xmodule-HtmlBlock.scss new file mode 100755 index 000000000000..d68570aced6c --- /dev/null +++ b/cms/static/sass/funix/unit/components/_xmodule-HtmlBlock.scss @@ -0,0 +1,7 @@ +.wrapper-xblock.level-element article.xblock-render { + .xmodule_display.xmodule_HtmlBlock { + + @include base; + @include unit-common; + } +} \ No newline at end of file diff --git a/cms/static/sass/funix/unit/components/_xmodule-ProblemBlock.scss b/cms/static/sass/funix/unit/components/_xmodule-ProblemBlock.scss new file mode 100755 index 000000000000..e5a383819912 --- /dev/null +++ b/cms/static/sass/funix/unit/components/_xmodule-ProblemBlock.scss @@ -0,0 +1,7 @@ +.wrapper-xblock.level-element article.xblock-render { + .xmodule_display.xmodule_ProblemBlock { + + @include base; + @include unit-common; + } +} \ No newline at end of file diff --git a/cms/static/sass/funix/unit/components/_xmodule-VideoBlock.scss b/cms/static/sass/funix/unit/components/_xmodule-VideoBlock.scss new file mode 100755 index 000000000000..c2bd013b73e1 --- /dev/null +++ b/cms/static/sass/funix/unit/components/_xmodule-VideoBlock.scss @@ -0,0 +1,20 @@ +.wrapper-xblock.level-element article.xblock-render { + .xmodule_display.xmodule_VideoBlock { + + h3.hd.hd-2 { + margin-bottom: $spacing-1; + margin-top: $spacing-1; + font-weight: $fw-medium; + font-family: $font-family-default; + color: $color-text-default; + font-size: $fz-h3; + } + + .video { + padding: 0; + margin: 0 0 $spacing-1 0; + border-radius: 8px; + overflow: hidden; + } + } +} \ No newline at end of file diff --git a/cms/static/sass/funix/unit/mixins/_buttons.scss b/cms/static/sass/funix/unit/mixins/_buttons.scss new file mode 100755 index 000000000000..961a20dbda8e --- /dev/null +++ b/cms/static/sass/funix/unit/mixins/_buttons.scss @@ -0,0 +1,10 @@ +@mixin unit-buttons { + + button.submit, + .btn-default, + .btn-primary, + .btn-brand, + .btn-upgrade { + @include btn-primary; + } +} \ No newline at end of file diff --git a/cms/static/sass/funix/unit/mixins/_common.scss b/cms/static/sass/funix/unit/mixins/_common.scss new file mode 100755 index 000000000000..7d2551311408 --- /dev/null +++ b/cms/static/sass/funix/unit/mixins/_common.scss @@ -0,0 +1,10 @@ +// used for many components + +@mixin unit-common { + @include unit-table; + @include unit-details; + @include unit-buttons; + @include unit-image; + @include unit-problem; + @include unit-typography; +} \ No newline at end of file diff --git a/cms/static/sass/funix/unit/mixins/_details.scss b/cms/static/sass/funix/unit/mixins/_details.scss new file mode 100755 index 000000000000..f572f65552a7 --- /dev/null +++ b/cms/static/sass/funix/unit/mixins/_details.scss @@ -0,0 +1,234 @@ +@mixin unit-details { + + // details - summary + details { + margin-bottom: 1em; + padding: 0 1em; + border-radius: $border-radius; + overflow: hidden; + transition: .2s; + max-height: none; + + * { + line-height: 1.5 !important; + font-size: 16px; + } + + // -- auto subdued -- + background: $color-subdued-light; + border-left: 2px solid $color-subdued; + + >summary { + color: $color-subdued; + } + + // --- + + >summary { + list-style: none; + line-height: 1.5; + font: 500 1em $font-family-default !important; + margin: 0 -1em !important; + cursor: pointer; + padding: .75em 1em .75em 1em; + position: relative; + + &::-webkit-details-marker { + display: none; + } + + &::after { + position: absolute; + display: block; + content: ''; + width: .5em; + height: .5em; + border-right: 2px solid $color-text-default; + border-bottom: 2px solid $color-text-default; + right: 1em; + top: 50%; + transform: translateY(-50%) rotate(-45deg); + transition: .2s; + } + } + + &[open] { + >summary { + + &::after { + transform: translateY(-50%) rotate(45deg); + } + } + } + + + &.details-primary { + background: $color-active-light; + border-left: 2px solid $color-active; + + >summary { + color: $color-active; + } + } + + &.details-subdued { + background: $color-subdued-light; + border-left: 2px solid $color-subdued; + + >summary { + color: $color-subdued; + } + } + + &.details-success { + background: $color-success-light; + border-left: 2px solid $color-success; + + >summary { + color: $color-success; + } + } + + &.details-warning { + background: $color-warning-light; + border-left: 2px solid $color-warning; + + >summary { + color: $color-warning; + } + } + + &.details-critical { + background: $color-critical-light; + border-left: 2px solid $color-critical; + + >summary { + color: $color-critical; + } + } + + &.details-leaf { + background: $color-leaf-light; + border-left: 2px solid $color-leaf; + + >summary { + color: $color-leaf; + } + } + } + + // =================== components ========================== + // =================== info box ========================== + .informa { + border-radius: $border-radius; + overflow: hidden; + padding: .75em 1em; + margin-bottom: 1em; + + >p { + font: 500 1em $font-family-default !important; + margin-bottom: 0 !important; + } + + >div { + margin: 0; + padding: 0; + + p { + font: 400 1em $font-family-default; + color: $color-text-default; + line-height: 1.5; + } + + p, + ul { + &:last-child { + margin-bottom: 0; + } + } + + i, + span, + a { + font-size: inherit; + color: inherit; + font-weight: inherit; + font-family: inherit; + } + + a { + color: $color-active; + font-weight: 500; + text-decoration: underline; + + &:hover { + color: $color-hover; + } + } + } + + // -- auto subdued -- + >p { + color: $color-subdued; + } + + border-left: 2px solid $color-subdued; + background: $color-subdued-light; + + // ---- + + &.informa-primary { + >p { + color: $color-active; + } + + border-left: 2px solid $color-active; + background: $color-active-light; + } + + &.informa-warning { + >p { + color: $color-warning; + } + + border-left: 2px solid $color-warning; + background: $color-warning-light; + } + + &.informa-success { + >p { + color: $color-success; + } + + border-left: 2px solid $color-success; + background: $color-success-light; + } + + &.informa-critical { + >p { + color: $color-critical; + } + + border-left: 2px solid $color-critical; + background: $color-critical-light; + } + + &.informa-subdued { + >p { + color: $color-subdued; + } + + border-left: 2px solid $color-subdued; + background: $color-subdued-light; + } + + &.informa-leaf { + >p { + color: $color-leaf; + } + + border-left: 2px solid $color-leaf; + background: $color-leaf-light; + } + } +} \ No newline at end of file diff --git a/cms/static/sass/funix/unit/mixins/_image.scss b/cms/static/sass/funix/unit/mixins/_image.scss new file mode 100755 index 000000000000..c539c2f0316f --- /dev/null +++ b/cms/static/sass/funix/unit/mixins/_image.scss @@ -0,0 +1,12 @@ +@mixin unit-image { + img { + display: block; + border: 1px solid $color-stroke; + border-radius: $border-radius; + overflow: hidden; + max-width: 100%; + margin: $spacing-1 auto $spacing-1; + box-sizing: border-box; + height: auto; + } +} \ No newline at end of file diff --git a/cms/static/sass/funix/unit/mixins/_problem.scss b/cms/static/sass/funix/unit/mixins/_problem.scss new file mode 100755 index 000000000000..2724413d9807 --- /dev/null +++ b/cms/static/sass/funix/unit/mixins/_problem.scss @@ -0,0 +1,200 @@ +@mixin unit-problem { + .problem { + padding: 0; + } + + .wrapper-problem-response { + fieldset { + margin-bottom: 0; + padding: 0; + } + } + + + .indicator-container { + margin: 0; + + .status { + display: none; + } + } + + .messages-box { + padding: 0 1rem .75rem 1rem; + border-radius: $border-radius; + margin-bottom: 0; + border-left: solid 2px $color-success; + background-color: $color-success-light; + + .explanation-title { + padding-top: 0.75rem; + margin-bottom: 0.5rem; + font-weight: 500; + color: $color-success; + padding-bottom: 0; + } + + .hint-text { + margin: 0; + } + } + + .problem-action-buttons-wrapper { + margin: 0; + padding: 0; + } + + .submit-attempt-container { + margin: 0; + padding: 0; + + .submission-feedback { + display: none; + } + } + + .notification-btn-wrapper { + display: none; + } + + .error-problem-answer { + background-color: $color-critical-light; + border-left: solid 2px $color-critical; + + .explanation-title { + color: $color-critical; + } + } + + .choicegroup .field { + width: 100%; + overflow: hidden; + border-radius: $border-radius; + position: relative; + margin-bottom: 0.5rem; + + >input { + position: absolute; + width: 100%; + height: 20px; + max-width: 20px; + position: absolute; + left: .75rem; + top: calc(.75rem + 1px); + margin: 0; + accent-color: $color-active; + + &:checked~label { + background: $background-color-hover; + border: none; + } + } + + >label { + padding: .75rem .75rem .75rem 3rem; + display: block; + width: 100%; + border: none; + margin: 0; + + >div { + margin-bottom: 0; + } + + &:hover { + background: $background-color-hover; + border: none; + + } + } + } + + .success-problem { + accent-color: $color-success; + } + + // + .container-problem { + display: flex; + gap: 10px; + padding-bottom: 10px; + } + + .problem-question-number { + padding: 4px 10px; + background-color: #f6f6f7; + text-align: center; + color: #8097b1; + font-size: 14px; + border-radius: 4px; + } + + .active-number { + border-bottom: solid 2px $color-active ; + background-color: $color-active-light; + color: $color-active ; + } + + .submitted-question { + color: $color-success; + border-bottom: solid 2px $color-success; + background-color: $color-success-light; + } + + .err-number-qusetion { + border-bottom: solid 2px $color-critical; + background-color: $color-critical-light; + color: $color-critical; + } + + .btn-custom { + border: none !important; + background-color: $color-active; + color: #fff; + background-image: none !important; + text-shadow: none !important; + box-shadow: none !important; + } + + .btn-custom:hover { + background: $color-hover; + } + + .btn-custom[disabled] { + background-color: #c5c5c5; + } + + .btn-prev { + border: 1px solid $color-active ; + background: none; + color: $color-active; + background-image: none; + text-shadow: none; + box-shadow: none; + } + + .btn-prev:hover { + background: #eef7ff !important; + } + + .btn-span { + display: flex; + align-items: center; + + gap: 5px; + } + + .btn-span>i { + font-size: 18px + } + + + .message { + border-top: 1px solid #d7d7d7; + padding-top: 20px; + } + + .problem-question-number:hover { + cursor: pointer; + } +} \ No newline at end of file diff --git a/cms/static/sass/funix/unit/mixins/_table.scss b/cms/static/sass/funix/unit/mixins/_table.scss new file mode 100755 index 000000000000..9f9571d54b99 --- /dev/null +++ b/cms/static/sass/funix/unit/mixins/_table.scss @@ -0,0 +1,24 @@ +@mixin unit-table { + table { + tr { + &:first-child { + background: #e6e6e8 !important; + + * { + font-weight: 500; + } + } + + &:nth-child(odd) { + background: #f6f6f7; + } + + td { + padding: .75em; + font-size: $fz-p; + } + } + + margin-bottom: $spacing-1; + } +} \ No newline at end of file diff --git a/cms/static/sass/funix/unit/mixins/_typography.scss b/cms/static/sass/funix/unit/mixins/_typography.scss new file mode 100755 index 000000000000..d548f31b272e --- /dev/null +++ b/cms/static/sass/funix/unit/mixins/_typography.scss @@ -0,0 +1,187 @@ +@mixin unit-typography { + + font-size: 16px; + + * { + font-size: 16px; + } + + h1, + h2, + h3, + h4, + h5, + h6, + .hd-1, + .hd-2, + .hd-3, + .hd-4, + .hd-5, + .hd-6 { + margin-bottom: $spacing-1; + font-weight: $fw-medium; + font-family: $font-family-default; + color: $color-text-default; + line-height: 1.5; + } + + p { + font-size: $fz-p; + font-weight: $fw-normal; + line-height: 1.5; + margin: 0 0 $spacing-1 0; + } + + div, section, article, aside { + &> h1, + &> h2, + &> h3, + &> h4, + &> h5, + &> h6, + &> .hd-1, + &> .hd-2, + &> .hd-3, + &> .hd-4, + &> .hd-5, + &> .hd-6, + p { + margin-top: $spacing-1; + } + } + + &> h1, + &> h2, + &> h3, + &> h4, + &> h5, + &> h6, + &> .hd-1, + &> .hd-2, + &> .hd-3, + &> .hd-4, + &> .hd-5, + &> .hd-6, + p { + margin-top: $spacing-1; + } + + + *:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not(.hd-1):not(.hd-2):not(.hd-3):not(.hd-4):not(.hd-5):not(.hd-6) { + + &+h1, + &+h2, + &+h3, + &+h4, + &+h5, + &+h6 , + &+.hd-1, + &+.hd-2, + &+.hd-3, + &+.hd-4, + &+.hd-5, + &+.hd-6 { + margin-top: $spacing-2; + } + } + + h1,.hd-1 { + font-size: $fz-h1; + } + + h2,.hd-2 { + font-size: $fz-h2; + } + + h3,.hd-3 { + font-size: $fz-h3; + } + + h4,.hd-4 { + font-size: $fz-h4; + } + + h5,.hd-5 { + font-size: $fz-h5; + } + + h6,.hd-6 { + font-size: $fz-h6; + } + + div, + article, + section, + aside, + fieldset, + ul, + ol { + margin: 0 0 1.5em 0; + } + + ul, + ol { + padding: 0 0 0 $spacing-1; + + li { + margin-bottom: 0.375em; + + &:last-child { + margin-bottom: 0; + } + } + + ul, + ol { + margin-bottom: 0; + } + } + + ul { + list-style-type: disc; + + ul { + list-style-type: circle; + } + } + + span, + label, + i, + u, + strong, + b { + font-size: inherit; + color: inherit; + font-family: inherit; + } + + strong, + b { + font-weight: $fw-medium; + + * { + font-weight: inherit; + } + } + + a { + color: $color-active; + font-weight: $fw-medium; + cursor: pointer; + text-decoration: none; + + &:hover { + color: $color-hover; + text-decoration: none; + + } + } + + p+p, + ul+p, + ol+p { + margin-top: 0; + } + +} \ No newline at end of file diff --git a/cms/static/sass/studio-main-v1-rtl.scss b/cms/static/sass/studio-main-v1-rtl.scss index dde051df3c83..db2eeabdc697 100644 --- a/cms/static/sass/studio-main-v1-rtl.scss +++ b/cms/static/sass/studio-main-v1-rtl.scss @@ -17,3 +17,7 @@ @import 'bourbon/bourbon'; // lib - bourbon @import 'vendor/bi-app/bi-app-rtl'; // set the layout for right to left languages @import 'build-v1'; // shared app style assets/rendering +.none { + display: none !important; + } + \ No newline at end of file diff --git a/cms/static/sass/studio-main-v1.scss b/cms/static/sass/studio-main-v1.scss index ac649970d644..00054bf85638 100644 --- a/cms/static/sass/studio-main-v1.scss +++ b/cms/static/sass/studio-main-v1.scss @@ -18,3 +18,9 @@ @import 'bourbon/bourbon'; // lib - bourbon @import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages @import 'build-v1'; // shared app style assets/rendering + +@import 'funix/unit/build'; + +.none { + display: none !important; +} \ No newline at end of file diff --git a/cms/templates/base.html b/cms/templates/base.html index 2e420568ef75..9172720de7b4 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -43,6 +43,8 @@ jsi18n_path = "js/i18n/{language}/djangojs.js".format(language=LANGUAGE_CODE) %> + + % if getattr(settings, 'CAPTURE_CONSOLE_LOG', False): + + diff --git a/cms/templates/funix_specialization.html b/cms/templates/funix_specialization.html new file mode 100644 index 000000000000..ab5b226df9c0 --- /dev/null +++ b/cms/templates/funix_specialization.html @@ -0,0 +1,156 @@ +<%page expression_filter="h"/> +<%inherit file="survey-base.html" /> + +<%namespace name='static' file='static_content.html'/> + + + +<%! +from cms.djangoapps.contentstore import utils +from django.utils.translation import gettext as _ +from openedx.core.djangolib.markup import HTML, Text +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) +import six +from six.moves.urllib.parse import quote +%> + +<%block name="title">Specialization + + + + +<%block name="content"> + + +
+
+

+ ${_("Settings")} + > Specialization +

+ +
+
+
+ +
+
+
+
+
+
+ + + +
+ + + + + +
+ + + +
+
+ + + + +
+ +
+ +
+ +
+ +
+ + + + % for c in list_spec_course: + + + + + % endfor + +
${c.spec} + +
+
+ + +
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/cms/templates/import_excel.html b/cms/templates/import_excel.html new file mode 100644 index 000000000000..cd72c7c4e2ae --- /dev/null +++ b/cms/templates/import_excel.html @@ -0,0 +1,49 @@ +<%page expression_filter="h"/> +<%inherit file="survey-base.html" /> +<%def name="online_help_token()"><% return "certificates" %> +<%namespace name='static' file='static_content.html'/> + + + +<%! +from cms.djangoapps.contentstore import utils +from django.utils.translation import gettext as _ +from openedx.core.djangolib.markup import HTML, Text +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) +import six +from six.moves.urllib.parse import quote +%> + +<%block name="title">Import + + + + +<%block name="content"> +
+
+

+ + > Import +

+ +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+ diff --git a/cms/templates/js/release-date-editor.underscore b/cms/templates/js/release-date-editor.underscore index cc62f36bf5e6..9ee948354684 100644 --- a/cms/templates/js/release-date-editor.underscore +++ b/cms/templates/js/release-date-editor.underscore @@ -26,3 +26,29 @@ <% } %> +
+
+
+ + + +
+
+
+ + +
+ +
+ + + +
+ +
+ + + % endif + + + + + + <% favicon_url = branding_api.get_favicon_url() %> + + <%static:css group='style-vendor'/> + <%static:css group='style-vendor-tinymce-content'/> + <%static:css group='style-vendor-tinymce-skin'/> + + % if uses_bootstrap: + + % else: + <%static:css group='${self.attr.main_css}'/> + % endif + + <%include file="widgets/segment-io.html" /> + + <%block name="header_extras"> + + + + <%block name="view_notes"> + + ${_("Skip to main content")} + + <%static:js group='base_vendor'/> + + <%static:webpack entry="commons"/> + + + + + + +
+ <% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %> + <%include file="widgets/header.html" args="online_help_token=online_help_token" /> + + <% + banner_messages = list(PageLevelMessages.user_messages(request)) + %> + + % if banner_messages: +
+
+ % for message in banner_messages: + + % endfor +
+
+ % endif + +
+ <%include file="widgets/deprecated-course-key-warning.html" args="course=context_course" /> + <%block name="page_alert"> +
+ +
+
+ <%block name="content"> +
+
+ + % if user.is_authenticated: + <%include file="widgets/sock.html" args="online_help_token=online_help_token" /> + % endif + <%include file="widgets/footer.html" /> + +
+
+ +
+ + <%block name="modal_placeholder"> + + <%block name="jsextra"> + + % if context_course: + <%static:webpack entry="js/factories/context_course"/> + + % endif + % if user.is_authenticated: + <%static:webpack entry='js/sock'/> + % endif + <%block name='page_bundle'> + + + <%include file="widgets/segment-io-footer.html" /> + + + diff --git a/cms/templates/survey_form.html b/cms/templates/survey_form.html new file mode 100644 index 000000000000..b8c5ac33d4d7 --- /dev/null +++ b/cms/templates/survey_form.html @@ -0,0 +1,80 @@ +<%page expression_filter="h"/> +<%inherit file="survey-base.html" /> +<%def name="online_help_token()"><% return "certificates" %> +<%namespace name='static' file='static_content.html'/> + + + +<%! +from cms.djangoapps.contentstore import utils +from django.utils.translation import gettext as _ +from openedx.core.djangolib.markup import HTML, Text +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) +import six +from six.moves.urllib.parse import quote +%> + +<%block name="title">Survey Form + + + + +<%block name="content"> +
+
+

+ ${_("Settings")} + > Survey +

+ +
+
+
+
+
+
+
+
+ % if error_message : + Error:${error_message} + %endif +
+
+ + Add Survey + +
+ + +
+
+
+ +
+ +
+
+
+
+
+
+
+ diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index b6c7d2f45eb0..0bc71e70e380 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -20,16 +20,7 @@