From 1542a0cdb0cb58e7364cfa37df84a0db2f9e2d49 Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Fri, 11 Dec 2020 16:11:21 -0800 Subject: [PATCH 01/21] UNIFICATION: get_current_organization don't assume single Organization Return the first instead to keep from failing outside of Tahoe SaaS when we may have more than one Organization per Site --- openedx/core/djangoapps/appsembler/sites/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openedx/core/djangoapps/appsembler/sites/utils.py b/openedx/core/djangoapps/appsembler/sites/utils.py index 8f7a3b2063ea..d514f04f474b 100644 --- a/openedx/core/djangoapps/appsembler/sites/utils.py +++ b/openedx/core/djangoapps/appsembler/sites/utils.py @@ -233,9 +233,11 @@ def get_current_organization(failure_return_none=False): ) else: try: - # TODO: Using `get` is expected to fail when multiple-orgs found for a site. - # Maybe catch MultipleObjectsReturned? - current_org = current_site.organizations.get() + # UNIFICATION: + # Tahoe SaaS enforces a single Organization per Site. + # For standalone, we need to be able to assign multiple Orgs. + # At least don't fail when multiple Orgs are linked + current_org = current_site.organizations.first() except Organization.DoesNotExist: if not failure_return_none: raise # Re-raise the exception From b5ab596ca8f4442a3f8eaec12331f5a170080798 Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Mon, 14 Dec 2020 15:37:29 -0800 Subject: [PATCH 02/21] Use FEATURE switch to determine how we get current Org by by Site --- .../core/djangoapps/appsembler/sites/utils.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/openedx/core/djangoapps/appsembler/sites/utils.py b/openedx/core/djangoapps/appsembler/sites/utils.py index d514f04f474b..e7251f06285a 100644 --- a/openedx/core/djangoapps/appsembler/sites/utils.py +++ b/openedx/core/djangoapps/appsembler/sites/utils.py @@ -10,6 +10,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.contrib.sites.models import Site +from django.core.exceptions import ImproperlyConfigured from django.db.models.query import Q from provider.oauth2.models import AccessToken, RefreshToken, Client from django.utils.text import slugify @@ -233,12 +234,22 @@ def get_current_organization(failure_return_none=False): ) else: try: - # UNIFICATION: - # Tahoe SaaS enforces a single Organization per Site. - # For standalone, we need to be able to assign multiple Orgs. - # At least don't fail when multiple Orgs are linked - current_org = current_site.organizations.first() - except Organization.DoesNotExist: + if settings.FEATURES.get('TAHOE_ENABLE_MULTI_ORGS_PER_SITE', False): + if settings.FEATURES.get('APPSEMBLER_MULTI_TENANT_EMAILS', False): + raise ImproperlyConfigured( + 'TAHOE_ENABLE_MULTI_ORGS_PER_SITE and ' + 'APPSEMBLER_MULTI_TENANT_EMAILS are incompatible as ' + 'we are not able to determine the exact Org when more than one ' + 'is associated with a Site.') + current_org = current_site.organizations.first() + if not current_org: + raise Organization.DoesNotExist( + 'TAHOE_ENABLE_MULTI_ORGS_PER_SITE: Could not find current ' + 'organization for site `{}`'.format(repr(current_site)) + ) + else: + current_org = current_site.organizations.get() + except (Organization.DoesNotExist, ImproperlyConfigured): if not failure_return_none: raise # Re-raise the exception else: From 2d216b6ce7f959e9f23adc88ac71f3f2c3a4cf7f Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Thu, 17 Dec 2020 23:48:06 +0000 Subject: [PATCH 03/21] Fix factory.fuzzy import in appsembler api test factories --- openedx/core/djangoapps/appsembler/api/tests/factories.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openedx/core/djangoapps/appsembler/api/tests/factories.py b/openedx/core/djangoapps/appsembler/api/tests/factories.py index bba38534869a..911c8c925311 100644 --- a/openedx/core/djangoapps/appsembler/api/tests/factories.py +++ b/openedx/core/djangoapps/appsembler/api/tests/factories.py @@ -2,6 +2,8 @@ import datetime import factory +import factory.fuzzy + from openedx.core.djangoapps.content.course_overviews.models import ( CourseOverview, ) From d298926c3e04c9fc8da64da1f36e23afafa52e6c Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Thu, 17 Dec 2020 13:32:05 -0800 Subject: [PATCH 04/21] TestCase covering appsembler.sites.utils get_current_organization --- .../appsembler/sites/tests/test_utils.py | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/appsembler/sites/tests/test_utils.py b/openedx/core/djangoapps/appsembler/sites/tests/test_utils.py index 7d5141d1323e..d3ade4455512 100644 --- a/openedx/core/djangoapps/appsembler/sites/tests/test_utils.py +++ b/openedx/core/djangoapps/appsembler/sites/tests/test_utils.py @@ -1,6 +1,14 @@ -from django.test import TestCase +import unittest +from mock import patch -from openedx.core.djangoapps.appsembler.sites.utils import get_initial_page_elements +from django.core.exceptions import ImproperlyConfigured, MultipleObjectsReturned +from django.test import TestCase, override_settings +from django.test.client import RequestFactory + +from openedx.core.djangoapps.appsembler.api.tests.factories import OrganizationFactory +from openedx.core.djangoapps.appsembler.sites.utils import get_current_organization, get_initial_page_elements +from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory +from organizations.models import Organization class JSONMigrationUtilsTestCase(TestCase): @@ -14,3 +22,63 @@ def test_initial_page_elements(self): self.assertEqual(element['options']['text-content'], { 'en': 'Welcome to your Tahoe trial LMS site!', }) + + +class OrganizationByRequestTestCase(TestCase): + def setUp(self): + super(OrganizationByRequestTestCase, self).setUp() + self.siteFoo = SiteFactory.create(domain='foo.dev', name='foo.dev') + self.siteBar = SiteFactory.create(domain='bar.dev', name='bar.dev') + self.siteBaz = SiteFactory.create(domain='baz.dev', name='baz.dev') + self.organizationA = OrganizationFactory(sites=[self.siteFoo]) + self.organizationB = OrganizationFactory(sites=[self.siteFoo]) + self.organizationC = OrganizationFactory(sites=[self.siteBar]) + self.request = RequestFactory().post('dummy_url') + self.request.session = {} + for patch_req in ( + 'openedx.core.djangoapps.appsembler.sites.utils.get_current_request', + 'openedx.core.djangoapps.theming.helpers.get_current_request' + ): + patcher = patch(patch_req) + patched_req = patcher.start() + patched_req.return_value = self.request + self.addCleanup(patcher.stop) + + @unittest.skip + def test_amc_admin_user_no_org_in_request(self): + # TODO: would be good to test + pass + + @patch.dict('django.conf.settings.FEATURES', {'TAHOE_ENABLE_MULTI_ORGS_PER_SITE': False}) + def test_single_organization_multiorg_feature_off(self): + self.request.site = self.siteBar + current_org = get_current_organization() + self.assertEqual(current_org, self.organizationC) + + @patch.dict('django.conf.settings.FEATURES', {'TAHOE_ENABLE_MULTI_ORGS_PER_SITE': False}) + def test_multiple_organization_multiorg_feature_off(self): + self.request.site = self.siteFoo + # fail raising exception if more than one org found for site when feature not enabled + with self.assertRaises(MultipleObjectsReturned): + get_current_organization() + + @patch.dict('django.conf.settings.FEATURES', {'TAHOE_ENABLE_MULTI_ORGS_PER_SITE': True}) + def test_multiple_organizations_multiorg_feature_on(self): + self.request.site = self.siteFoo + # return one org from Site's org relations + current_org = get_current_organization() + self.assertIn(current_org, (self.organizationA, self.organizationB)) + + def test_no_org_for_site(self): + self.request.site = self.siteBaz + with self.assertRaises(Organization.DoesNotExist): + get_current_organization() + + @patch.dict('django.conf.settings.FEATURES', { + 'TAHOE_ENABLE_MULTI_ORGS_PER_SITE': True, + 'APPSEMBLER_MULTI_TENANT_EMAILS': True + }) + def test_raises_if_multiorg_feature_and_multitenant_email_feature_on(self): + self.request.site = self.siteFoo + with self.assertRaises(ImproperlyConfigured): + get_current_organization() From 7db75ba90f4b1886d67534f1ecbdb26fa40c82d3 Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Wed, 23 Dec 2020 06:47:53 +0000 Subject: [PATCH 05/21] Allow auto-generation of certs (e.g., on passing grade) for Honor mode Use FEATURE setting to enable Honor mode auto-certs Committing Omar's suggestions Co-authored-by: Omar Al-Ithawi Fix settings imports for Tahoe certs signals changes for Honor auto-certs, related tests. Add related default FEATURE setting in appsembler.settings --- lms/djangoapps/certificates/signals.py | 7 +++++++ lms/djangoapps/certificates/tests/test_signals.py | 5 ++++- .../core/djangoapps/appsembler/settings/settings/common.py | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/certificates/signals.py b/lms/djangoapps/certificates/signals.py index 9bd92199672a..27841322886d 100644 --- a/lms/djangoapps/certificates/signals.py +++ b/lms/djangoapps/certificates/signals.py @@ -3,6 +3,7 @@ """ import logging +from django.conf import settings from django.db.models.signals import post_save from django.dispatch import receiver @@ -126,6 +127,12 @@ def fire_ungenerated_certificate_task(user, course_key, expected_verification_st CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE, ] + if settings.FEATURES.get('TAHOE_AUTO_GENERATE_HONOR_CERTS', False): + # Appsembler change: allow for Honor mode + allowed_enrollment_modes_list += [ + CourseMode.HONOR + ] + enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(user, course_key) cert = GeneratedCertificate.certificate_for_student(user, course_key) diff --git a/lms/djangoapps/certificates/tests/test_signals.py b/lms/djangoapps/certificates/tests/test_signals.py index 4f371acdbd32..cb85186af73d 100644 --- a/lms/djangoapps/certificates/tests/test_signals.py +++ b/lms/djangoapps/certificates/tests/test_signals.py @@ -5,6 +5,8 @@ import ddt import mock +from django.conf import settings + from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.models import ( CertificateGenerationConfiguration, @@ -311,9 +313,10 @@ def setUp(self): ('no-id-professional', True), ('credit', True), ('audit', False), - ('honor', False), + ('honor', True), # TAHOE_AUTO_GENERATE_HONOR_CERTS feature: allows for honor ) @ddt.unpack + @mock.patch.dict(settings.FEATURES, {'TAHOE_AUTO_GENERATE_HONOR_CERTS': True}) def test_fire_ungenerated_certificate_task_allowed_modes(self, enrollment_mode, should_create): """ Test that certificate generation task is fired for only modes that are diff --git a/openedx/core/djangoapps/appsembler/settings/settings/common.py b/openedx/core/djangoapps/appsembler/settings/settings/common.py index 205375373726..c638410063e1 100644 --- a/openedx/core/djangoapps/appsembler/settings/settings/common.py +++ b/openedx/core/djangoapps/appsembler/settings/settings/common.py @@ -46,3 +46,6 @@ def plugin_settings(settings): settings.CUSTOM_DOMAINS_REDIRECT_CACHE_KEY_PREFIX = 'custom_domains_redirects' settings.COPY_SEGMENT_EVENT_PROPERTIES_TO_TOP_LEVEL = False + + # Appsembler allows generating honor certs + settings.FEATURES['TAHOE_AUTO_GENERATE_HONOR_CERTS'] = True From 42d2a46bde2249995598d384584459a5cda169db Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Tue, 29 Dec 2020 17:28:30 +0300 Subject: [PATCH 06/21] tracks static page --- lms/djangoapps/static_template_view/urls.py | 1 + lms/templates/static_templates/tracks.html | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 lms/templates/static_templates/tracks.html diff --git a/lms/djangoapps/static_template_view/urls.py b/lms/djangoapps/static_template_view/urls.py index 0371b97a561d..10c76bf58ed5 100644 --- a/lms/djangoapps/static_template_view/urls.py +++ b/lms/djangoapps/static_template_view/urls.py @@ -15,6 +15,7 @@ url(r'^500$', views.render_500, name='static_template_view.views.render_500'), url(r'^blog$', views.render, {'template': 'blog.html'}, name="blog"), + url(r'^tracks$', views.render, {'template': 'tracks.html'}, name="tracks"), url(r'^contact$', views.render, {'template': 'contact.html'}, name="contact"), url(r'^donate$', views.render, {'template': 'donate.html'}, name="donate"), url(r'^faq$', views.render, {'template': 'faq.html'}, name="faq"), diff --git a/lms/templates/static_templates/tracks.html b/lms/templates/static_templates/tracks.html new file mode 100644 index 000000000000..9f99d8e0698f --- /dev/null +++ b/lms/templates/static_templates/tracks.html @@ -0,0 +1,16 @@ +<%page expression_filter="h"/> +<%! from django.utils.translation import ugettext as _ %> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("Tracks")} + +
+
+

+ <%block name="pageheader">${page_header or _("Tracks")} +

+

+ <%block name="pagecontent">${page_content or _("This page left intentionally blank. Feel free to add your own content.")} +

+
+
From 17648d78ce9abb59bd064b75d2be2f3400a855ee Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Wed, 6 Jan 2021 07:15:51 +0000 Subject: [PATCH 07/21] Update translations (autogenerated message) --- cms/static/js/i18n/ja-jp/djangojs.js | 2 +- conf/locale/ja_JP/LC_MESSAGES/django.mo | Bin 666052 -> 666045 bytes conf/locale/ja_JP/LC_MESSAGES/django.po | 5 +++-- conf/locale/ja_JP/LC_MESSAGES/djangojs.mo | Bin 233359 -> 233352 bytes conf/locale/ja_JP/LC_MESSAGES/djangojs.po | 5 +++-- lms/static/js/i18n/ja-jp/djangojs.js | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cms/static/js/i18n/ja-jp/djangojs.js b/cms/static/js/i18n/ja-jp/djangojs.js index e9024bee3a81..f7407d180800 100644 --- a/cms/static/js/i18n/ja-jp/djangojs.js +++ b/cms/static/js/i18n/ja-jp/djangojs.js @@ -369,7 +369,7 @@ "Click to remove all chosen %s at once.": "\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u3059\u3079\u3066\u306e %s \u3092\u9078\u629e\u304b\u3089\u524a\u9664\u3057\u307e\u3059\u3002", "Close": "\u9589\u3058\u308b", "Close Calculator": "\u8a08\u7b97\u6a5f\u3092\u9589\u3058\u308b", - "Closed": "\u7d42\u4e86", + "Closed": "\u7de0\u5207\u6e08", "Code": "\u30b3\u30fc\u30c9", "Code Sample (Ctrl+K)": "\u30b3\u30fc\u30c9\u30b5\u30f3\u30d7\u30eb (Ctrl+K)", "Code block": "\u30b3\u30fc\u30c9\u30d6\u30ed\u30c3\u30af", diff --git a/conf/locale/ja_JP/LC_MESSAGES/django.mo b/conf/locale/ja_JP/LC_MESSAGES/django.mo index 2fad57a22d13f25d9d1a9d5158779be023e30a0a..da18286ecefbb77bbb0118ce2bb0e35ae665fc3a 100644 GIT binary patch delta 36729 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 Nzu?Yy??dPE{2!?4DD(gT delta 36722 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%0AO?t`o(cWgt+km@>w~lPIp-um_jleX_%7wF- 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 Dqp>xk diff --git a/conf/locale/ja_JP/LC_MESSAGES/django.po b/conf/locale/ja_JP/LC_MESSAGES/django.po index 9edd7ecdca6a..6b24a8c43abd 100644 --- a/conf/locale/ja_JP/LC_MESSAGES/django.po +++ b/conf/locale/ja_JP/LC_MESSAGES/django.po @@ -44,6 +44,7 @@ # 鈴木 陽一 , 2018 # Toshiboumi Ohta , 2018 # Kyoto University , 2018 +# Omar Al-Ithawi , 2021 # msgid "" msgstr "" @@ -51,7 +52,7 @@ msgstr "" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" "POT-Creation-Date: 2018-08-07 20:01+0000\n" "PO-Revision-Date: 2018-08-07 20:02+0000\n" -"Last-Translator: Kyoto University , 2018\n" +"Last-Translator: Omar Al-Ithawi , 2021\n" "Language-Team: Japanese (Japan) (https://www.transifex.com/open-edx/teams/6205/ja_JP/)\n" "Language: ja_JP\n" "MIME-Version: 1.0\n" @@ -2158,7 +2159,7 @@ msgstr "提出済" #: common/lib/xmodule/xmodule/capa_base.py msgid "Closed" -msgstr "終了" +msgstr "締切済" #: common/lib/xmodule/xmodule/capa_base.py msgid "Finished" diff --git a/conf/locale/ja_JP/LC_MESSAGES/djangojs.mo b/conf/locale/ja_JP/LC_MESSAGES/djangojs.mo index 876871ca03518802242bb2033a20189df5d99ec1..05f7c11ad6601fc14f7c41c3e9f618414391e3a4 100644 GIT binary patch delta 16404 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 delta 16396 zcmXZjd4NvUAII@K&yy{C%-FNbGWLDnm$AzdvM*yd48shQD>U{!3Y9I{h3xy1twK=} zp~w=EvHVJ+-~01?&tI>5&b{}X@A;nPd7e>ImS&i}G{fdTwf$eiJTK-(m>1!BUYna? zUK;H0jK!vu6EPZ3U{@@4E6gj2Gf?$=Q1$t5TfL8ZZzB%Izny*Wgn6kbpTl%`=}wsM z73AVJ6?HM~?_pjHw!l;bwjaw=K6W?EtAx)mE0(zz<`u$*SOW)QN?eUua1*NCBdB(7 zp$1s=k1#JY*2WCj<`3U4dQee@iWn@8%djGTjgc63-<~UlYM>^j##XM}5!K;9)O)jV z4StDl;E)GlUJqP=%GCd`Hn#Ks4D%Xr@jfQtPpH(keHiBD#CK66`v{e?l^BkjUHLf1 zQ9h4)KPoxQ>wrh`EzJ4I-jBuCC?{Y>oPlb`|A-5Xc&n?pg83;w#_mi(_+x7z?um^o z5f#u@R71N_&!55pcn`B<`=|EYP*g{8sOP4;@S&ou6c=mGcKyXuzfrk zwF@3#Rm}U+GSLZ{D=!|^!2)cEJ24txpr)kHe>R2hpaPoXT!zZrW~_%l;E$Ny3;5oz zTs-sw-YPs77V!SY_&^}k!SG-pG&MU>=fE}88p-lXAhdmopfVSYZEzTBq(@Mhyo$Z? zCTej;r3iS_@C$5$wNmA|F&@LJp9`Mdno{3sBJtM4sHPVmo9p^$3 z=gMXo@KGt9hsw-0=U&umKjO+~QH%A0E2qjH2ra@4n2q{3P^qqsY9|V{Hd>=HGZ?#S z|F7htClz5i0^U^YjY|D}R0ExJS_6HYgHUr9iwbChtDo&$=)O< zsDTwhJzw70C@=e84R)d;3&x@%OG1tGIBK!oM0L7$-ofLb%#P#L)3%D3G8 z)cLLBH&B69!;08Czi%%lQK3b%1{KgD)N1_+^)Z^afbIX$s1(*M81Tm8VAN512i0-m zLKaX()QQ&))nNjv-PxD}*Pu?qBR&^uFhyaDuncOI)6V26LeTDT->RG-@gvVhnb5_fNa~-(xZA?}p0kzpRngKyhbTR6|uzQ_&K&xCWwj z$qaY@GAfW{)EWpY8SwI8EmS7Dpz6n>GPeu0CXS%qyN%80-}{RTji7$1fcGvoMMeG% zj>H|2HGC5l$V;4$X)9R$QdEj}q3W+-WgK47?yp5<=m=`pJ*>$7 z*BPCwl35?sz#!)w>`Qq&YA$nCwu7o8YQ%$4+in(Wt!zTg?N`oAsI_$0mAxt!cqV5a zp9_twIBLXYoz{?VU$u@)9azd8*p|N~m&s)Rg)| zxljl1I%nZv%1c~1b+tg~Cs#OXDvF~<+6~q57}VlgflBQi)b@?2ZYi#f>Tn=xfD`ct zoQc!)`QN*SZL0^U?_@P=+Ji}`k<39w`hhF2b8bOpVi#%vXHXfrjG;x1>i8vUO0(4p zcuTMbYWG~k&HDT=Q`LiYIlz-pGHm54b<*=9L4_ENW+`j9M(t8Rclmg<4_IEaxTZL zl((Vg{FL(oY9KdIi|;-vfQV*+&~L#=)bmSF^=nXp9BAfS3eQlX6x~NPl)t$>Py&^z z>Zlj%p}ur>L^U|oxeyiTO4RPyf$HEKYCzXrIaLd5C!4dR&xJ->ANAqV#obu%Jd9ec zKcco->6Z3GA`Z2`5925JN-O)pvJ&-N?$#D)FVsmlA2q-OuKWn~)z8n<#uimO)PrB4 z0(y%2u*vXdz-x@%QJGndI%u|_M*1!4Wc&j)(qe7xcR)*ALHR6dx5Ty!c+GJ&YIi(A z2IhOE+S`bdP#HLf8gc#(R!+dFlux2k|K?lP(L~%!c@3_|J{_$-Qztt|Dxv}!hYEBh zF2I9$6x(;!B4q#Nc-ux)9S?A$Gk%3Rx&*xK_!~~b#a#p5PR!fQej)vcIwvB#+gg~6 z+IH`|@-ozJ+K7>O5)0x}SD&Ltz$>EtUxAA+@NMjZ8GBl)2cza}6lyW8!_am@O~Gkb z{=?n>50#;ey=>9eL2a|fsOJZurgjQyF)v479qi;nsk-OP)Y}@Wh+5^*r~pQwreY%M z>`&h(;I+q!*b1lPBD{=kF|Mx#a0<0X?xET#8f^hoiDv(6d$puOH`+VTn;b!K0|%@RKXwaQB~~I`9VAZc2}OuL>&lwNdSMM71-<=RzZx;nT{b*ztS&_@N3gj#fST>VEF8X+nJn^8ydA?F#?3HmcC06&=vb(m_9&1pW=B5Q&Q z>}^zoV^9rFcJ&`%1mzv>{%L%b@+Id@=R@anXUf6$TxO&n-^w z9lcdh9goLYT!`Z^&2am@Fa-7cntlK0q;0AjI(yqjbi_o zBSk(J>v8MofH#u|x+Mg>Z}AV*Z^Hd!>_=m}v6kARi8g}Ls8rWLjj+C}Z;M*xJzV_+ zR7U5aGI|^}(Bwq+zaGr=j-{#`Dn;$_6YP#k{Yz8^ij1?4YM|b0?#e?^fs8{<;d@vQ zKXm2G&IhPK!`SvRv(JTI(BA=M1!rT_9Cb#0c<7wa3lmYRe!g=Z=AeAol`o?Ld4ziY z8EQaj##=cHs-J?cT*-G8jZi7+ewu-mBZ8+_LevfQY-Zi)&d z(*)}{8>-_sQ0-JhWz=urDmuFd`k)ryXsm@}P$SvxJcVlbCsaefpaQ+??q`{3nJb8@ zFN1o&6E?sBsPB@StlsxZOtRnO)lqZS7w6*|T!S^<3wU$zCa%CyN%jLJ$7K7)lZ4q= zy#Jx5EZbCDWQ9;8Y=zp6eNkWUr=gykg=w_^mxL}nZxt$~pQB#<+LeDrrS1hP)tRQ* zh>D?}uZsF~Ymdcn9xCvCsKwiNx^*}c$5393+700|Xh-`$iVJ=7>4bV=4Qh3Ni|XJJ zDxeHA16~O%gi3vDR6w0k_eY@Kn~HjGJu2`M&P%8@cHfz97W-dwRDugN*v{Dv)lh%b z3)4^?twpVkBd&Z5V<}(7Xlye(;8nq0sP~gm^;zavfMZYr%|Wf9m2=qt`M6l;ZhVb8 zAg-bsNHfb>&s+W_nM zTxfOnLPb0omGZf$&*{ymz>cA&;Ii`x=BAu}f!!~T*(ldR1<(=mVRzJlH6E4G4^Zvx zM)l*Ly4$=3+Pj<55%h z9JP%jKeUebBklX%&s-?tTkghvyWyo^( z-ThP8i~4V{HD}561qVP}DFQ2MQZ3J^sfqjUYyY;9{oJRem%CpQ4 zk~dL-CZPgciu%|-fCaGqatk;f72p)-0`yg}f(xbWbLS4!i1wobIDx70lB>VrypMYS zFVw;H$_nc!64hP}SFVp*bZt@FdjRTZ|NAT0|7zd}6&m4LJcP-p5p7**evP##|K`dC zKedi)pgM{|HQX8%=vdSmnS@&X%TNLCMD3DmSPB37)VBsot+ECyqP|YoLp9ve)wf3l z)C+sySkx}~8MV42KC=KCqZVPTD}R9cjd%pL2J)=7T`(T?nUUFFW50A-q8eO`YT$d+ zT6pH}w^?h?eT?excdUy!*V$hs4U68FGT)SPWV{bD(Ziu?j9Gyme}SZRxS3-x)vaH|Ej7_}{bLACP`6?nQY zZ0_rz0_%^uAA?o3|EF`IResDpa1phQuAw4->F%f97Vy5I{5ooP+(WJ6r>I3%aJzjx zS3|WIjaoAam;>jb2DAa&;W-S~{(obKrKTn-!nV%NSd(&3R6|QqNA)h$2ri%kd4x*s zzo_?L+ZphdVh+^(-KfkQb>;J}d!6>WNwtgIxU_)B*Gv7Q|Di%-nZ+d)WWlk6HHE$Vy-a%2B9o*b&uX9~_5+T=_1B z79lDV`Sx0dYM=sYkLqX;R>pCtHMGOse~4OyDfhAeH77;)Swk&RBN>Qa<4DvRsI%Yd zTcA343l-Q9S03rg(@?3Ni`r((QM=|Gs@?1dEYJd|_pAF{DAIbU2iv;`mY_P?=KKm< zQT_!rC1noUhe<0``3qDg&Y?2;02N5eLuLeOu@*r+*ADfbKa2~_*=!7Th&t&rt2AIbzS}MQz(iBm=%zh6_Dd+1VHs;ajMb zjX(wX4r)!TK&5Vzb2}=KeW>?Np)z_CHR8Wf?H4&}fmA}3o8u|%|1Ml;dlmW8I;xE- z_d!Me4r;N?M;)QtUAgA5fcHD)?x+Bwj$6kaP#p}wBA9?Za2ciu#a$quL&MxaLUp>vIM7pmctsEquE%2dj)?N6?GQ5oxxYG)kk{z}yL+lQK( z|DpOz^9}pI7#BspvB+AZQWk?#aiKHcXR;;j84BOLW9 zx)OWibBx69-`c@6<6Ga(?9ZuC#FtU2^1idLWT{Yd+s4@q_1qv-Cgz|P*#^|NvqHi`>8ekNBctY z5*28ti7wd2=YO1EAM!ee9Z^3Zw|HJNq zA5ocjj2dy(UoEu>sE%f1SzL&N@g(ZGir3v=yRkauQ_d7O0$yp#b)Cae_m^W5UcoW+ z@AdgD;O)k1xE&YWv~APsmW`l4Y7XPE98O0CbO4pwW2kL+%GF;(jr0~OFDj!^?JPij zD6Mkk{m#>O*#G*_xI%@#faLt${(#UBHG*-d4yWKcT#gE``CU7Jx}z59yUtmtz?Qi3 z7E}NSPy;=KBk(SE!fy9``=``h_X6H5DvJNX4+9p_ZtO_;;eGoz->3%xZxH2G*cUVZ zX(!rfY)N?w#-R7mGBM1#0NP%7&wdD$#U4}~ zK&|e)e+9gYI0Ktt-@mPcO{kGRMon4oX8~^t&O(he+dp z(=dg-Hw(v7K7(DcY06;ed;S{yi1G_mdy7-q{WI8G`#&;uFf`Y5Pz|0&Js6QD7+M=m zF`DusR7$;9E#Ug7fZoBW_yab_?rDRe{~%!lDr4d4Ts=0Tya8L_WAwEc>ZT8R`*0%a zfT@!q7+Q=YuodNXm;?Vo&3)F4<|I_WPw*(#%w!`A%N+D>Q|^whlj_E=1-*qBoy9Wr zB5N@8<1-;V=!YUo84(Qq!(j`Y!;803BaP1%3@x&~r~~SjGi~-@XjhcMSn3mz#pL~o z%0Og}VCX;BO+ZaW{hYzjf6lucHFY_2*#K7M^6f@=?qKK##w=8W>GK3bXZ3j0htfXh zU1#>!Z6u9Q&mX~fte7_#T5Ow8Q&TCQYZtY>)8`L{zBx5O?Vf0#3ypLx4#4lR4%R9V z44qs)>L{I!I@va&PQ0U-0l&r9@S5`x>cmV@kksSrsB%rzKssOnoP-L%U(JOYJcs&F zxrS=+77oE@7|Y!CD`X?RR5%#=aoM6sF!XOl_fd;;>>D=X>!|afT2b2Ny`!kL^=&a* zTQiFXL*IxFAt$Nt#g(v;UB)y#nCyIx3M@sW`5I=VoEH^nY1H$TTzwr>id(z;lU;ch z>RdR68d#c=)?aQ6{rCUnxKKm&@KbD#K}<%S(N9r}CQm6#=^9i3C!A+cQ}cuKo-?ep z)n`L>T+&$w)n8jor~TiR3w=5bbPpuy2IWPljy^-pO5A14vQM z7FB-Kc5IFcXe#E%k5G&K02afr^6Y;#T%x?~@3yGb8josluB+eZ%I9490k&arWvLJh z{l4#8(VqVpV|i~ks-4J6w(1+A$|GEP2CCmZmDvB9+v~1kNM&ni0qVnI3u;l_Lv5D= zRf3^UvljR+<@u@=| z#`+pTZz86xW&3q@?a+4dwxYfVWUmtp{YhpZ&Zbz%A0*OYg?hzQ8TUWmd)!|lDpu64u ztEffy!rd?2nCC*D|6FY1MtRisxQ?Zyd(6sGxTp%QfYT1co=W zZI^(0eg|&H%+1-Rtfdo}g7T9V7WhBtYmuaHX)hK*m3yNykce$@2Wo_`wzAX}!+6Tg zFb=;&Ev{;zpdH-4Y)|%#%{DgH57|FBA1~8T8G+( zM^SMF$&b6q6=L+gmGiO_SuL>%&G1wHhqWXQ&mi=Fmi+b&X-eQbLb(E#O zeY0tb`zg=IkFZULVCWyGA7CQo32)i9{13NM?%UC=nNId`UICS%Pf)ugYiA3jwsV5d zg(5kI@8PR&TL5!VtMvqG(LBfO%wfSUc3{=$>h7cF`Z+3d9lBWulQ8r?rs2KkSR7OI zu=+AR?LEH_7g{_AQ6Clsd)df)qf)s6C-CC$_&epneeA1KhrU5?6y@WnuUIvs?L=IH z8fl(>HdRe<9pwpl2ut?2Z%z*|NuU1%2iTFAi~%}YIWQRd-*G-0WFtvEI2ih2(G2U; zz-m-V@1fR0&mk7*64au+h$FGhP+N@0QQP!9>eKHQ*1>Es>WBT;kqa%dJ$M4oV}4vR z%wE`!O63Ebi3NrSz03r<8TH(eSR2`IsI{|dgsp*}oqwZ_?5ra#;Aqs8F2m;9|0lW7 zwg`{2uTa%64;{=yMZ9^G&F!z)kaEV+R^J|#>LmOMw_rX@jJHgFjLOuqgkb0&E+WQQ z>TjUdO82qse_hPvLaTc(Y6Kk<&DmI;@^M!VykqA;Rn&>u(b*rB;&{}T(&?xZa4~8d zFGro2>s@)5D<64>{jVEmsn82QJ8z&Gyo)*U1*)O!a!!p zmE%#*`OXEXDPKR1{jZBJsnFuOf(j@<^_r4WPMs4v0poE3et_vQ=XiU+C|0Ii7ZrE{ zYHDU+Mcj!B@II>DH1FElDb<<_9gR{iwnGKb4>jVEsE)_E`j1hIXB}#4&Y|AVFu{Cn z{^7(dp)YlXs8Ne0P%YI$z1aj6!Z6h8T7b&Uaa71Zqdsp_O|)E=#F~^Vpi&fzWpRhQ ze+Sk6pQuQlO=MDa@q!AilCVj37-vQWR2tP`Jyh;HqW)qy97D?)_1qTJb6=tYzl2(? zf%oiw8C0t3qXKA+1F-XZOpZ3@msI4yTUZeJMep^3La4uuOh)~&Ct|X7&|-=m=5eTw zFJmf9#x?j1qj1I4p!XsEfkp7WX&eZ+0oBie9$aMN;yPx-$2c{>m$~V7)-ISC4E>kH z&rr{8nH3EE-RuZz|GhwEpu%js-x$kL?ug3l^oeW7?OZ!9EU|CNW(hGPWAe2hI$%`a z*bxIpCgiKpw@>^aRmR2F={Kb3fWf7D#|)_znXg=#vQ>6A`7LZ)!xY;m{I(})@+-vy LJ6|atoYeRK+q|Sn diff --git a/conf/locale/ja_JP/LC_MESSAGES/djangojs.po b/conf/locale/ja_JP/LC_MESSAGES/djangojs.po index c3cf3619de47..40c0303b7a31 100644 --- a/conf/locale/ja_JP/LC_MESSAGES/djangojs.po +++ b/conf/locale/ja_JP/LC_MESSAGES/djangojs.po @@ -39,6 +39,7 @@ # Toshiboumi Ohta , 2018 # かわぐちひろふみ , 2018 # Kyoto University , 2018 +# Omar Al-Ithawi , 2021 # msgid "" msgstr "" @@ -46,7 +47,7 @@ msgstr "" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" "POT-Creation-Date: 2018-08-07 20:01+0000\n" "PO-Revision-Date: 2018-08-07 20:02+0000\n" -"Last-Translator: Kyoto University , 2018\n" +"Last-Translator: Omar Al-Ithawi , 2021\n" "Language-Team: Japanese (Japan) (https://www.transifex.com/open-edx/teams/6205/ja_JP/)\n" "Language: ja_JP\n" "MIME-Version: 1.0\n" @@ -7530,7 +7531,7 @@ msgstr "%(author)sが%(time_ago)s前に投稿した質問" #: common/static/common/templates/discussion/thread-show.underscore msgid "Closed" -msgstr "終了" +msgstr "締切済" #: common/static/common/templates/discussion/thread-show.underscore #, python-format diff --git a/lms/static/js/i18n/ja-jp/djangojs.js b/lms/static/js/i18n/ja-jp/djangojs.js index 00ea61abb0bb..d0a5cd8f2c1c 100644 --- a/lms/static/js/i18n/ja-jp/djangojs.js +++ b/lms/static/js/i18n/ja-jp/djangojs.js @@ -369,7 +369,7 @@ "Click to remove all chosen %s at once.": "\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u3059\u3079\u3066\u306e %s \u3092\u9078\u629e\u304b\u3089\u524a\u9664\u3057\u307e\u3059\u3002", "Close": "\u9589\u3058\u308b", "Close Calculator": "\u8a08\u7b97\u6a5f\u3092\u9589\u3058\u308b", - "Closed": "\u7d42\u4e86", + "Closed": "\u7de0\u5207\u6e08", "Code": "\u30b3\u30fc\u30c9", "Code Sample (Ctrl+K)": "\u30b3\u30fc\u30c9\u30b5\u30f3\u30d7\u30eb (Ctrl+K)", "Code block": "\u30b3\u30fc\u30c9\u30d6\u30ed\u30c3\u30af", From 14e11b9b622c08159f36e06499f4601887430a11 Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Tue, 19 Jan 2021 12:02:54 +0300 Subject: [PATCH 08/21] lms organization middleware to make tiers expiration work in lms --- .../appsembler/settings/settings/aws_cms.py | 1 + .../appsembler/settings/settings/aws_lms.py | 5 ++++ .../djangoapps/appsembler/sites/middleware.py | 21 ++++++++++++-- .../sites/tests/test_middlewares.py | 28 +++++++++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 openedx/core/djangoapps/appsembler/sites/tests/test_middlewares.py diff --git a/openedx/core/djangoapps/appsembler/settings/settings/aws_cms.py b/openedx/core/djangoapps/appsembler/settings/settings/aws_cms.py index 29ffbc494c81..9017b7970c98 100644 --- a/openedx/core/djangoapps/appsembler/settings/settings/aws_cms.py +++ b/openedx/core/djangoapps/appsembler/settings/settings/aws_cms.py @@ -33,6 +33,7 @@ def plugin_settings(settings): settings.RAVEN_CONFIG['tags']['app'] = 'cms' settings.MIDDLEWARE_CLASSES += ( + # TODO: OrganizationMiddleware should be added before Tiers middleware in `aws_common.plugin_settings()` 'organizations.middleware.OrganizationMiddleware', ) diff --git a/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py b/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py index 216dc06fbeb0..721b3abae0ef 100644 --- a/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py +++ b/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py @@ -43,6 +43,11 @@ def plugin_settings(settings): This file, however, won't run in test environments. """ + settings.MIDDLEWARE_CLASSES += ( + # LmsCurrentOrganizationMiddleware needs to go before `TiersMiddleware` in aws_common.plugin_settings() + 'openedx.core.djangoapps.appsembler.sites.middleware.LmsCurrentOrganizationMiddleware', + ) + aws_common.plugin_settings(settings) settings.LMS_BASE = settings.ENV_TOKENS.get('LMS_BASE') diff --git a/openedx/core/djangoapps/appsembler/sites/middleware.py b/openedx/core/djangoapps/appsembler/sites/middleware.py index 141748041cd8..1aa5219d99f3 100644 --- a/openedx/core/djangoapps/appsembler/sites/middleware.py +++ b/openedx/core/djangoapps/appsembler/sites/middleware.py @@ -1,12 +1,15 @@ +import beeline +import logging + from django.conf import settings from django.core.cache import cache, caches from django.contrib.redirects.models import Redirect from django.shortcuts import redirect from openedx.core.djangoapps.appsembler.sites.models import AlternativeDomain +from openedx.core.djangoapps.appsembler.sites.utils import get_current_organization + -import beeline -import logging log = logging.getLogger(__name__) @@ -66,3 +69,17 @@ def process_request(self, request): redirect_to = redirects.get(request.path) if redirect_to: return redirect(redirect_to, permanent=True) + + +class LmsCurrentOrganizationMiddleware(object): + """ + Get the current middleware for the LMS. + + This middleware replaces the default `organizations.OrganizationMiddleware` to + use a better get_current_organization() helper. + """ + def process_request(self, request): + # Note: This does _not_ support multiple organizations per user. + organization = get_current_organization(failure_return_none=True) + beeline.add_trace_field('session_current_organization', organization) + request.session['organization'] = organization diff --git a/openedx/core/djangoapps/appsembler/sites/tests/test_middlewares.py b/openedx/core/djangoapps/appsembler/sites/tests/test_middlewares.py new file mode 100644 index 000000000000..0e663975a981 --- /dev/null +++ b/openedx/core/djangoapps/appsembler/sites/tests/test_middlewares.py @@ -0,0 +1,28 @@ +""" +Tests for the sites.middlewares module. +""" + +from mock import patch, Mock +from django.test import TestCase + +from openedx.core.djangoapps.appsembler.sites.middleware import LmsCurrentOrganizationMiddleware + + +@patch('openedx.core.djangoapps.appsembler.sites.middleware.get_current_organization') +class LmsCurrentOrganizationMiddlewareTests(TestCase): + def test_with_organization(self, get_current_organization): + middleware = LmsCurrentOrganizationMiddleware() + request = Mock(session={}) + fake_org = Mock() + get_current_organization.return_value = fake_org + + middleware.process_request(request) + assert request.session['organization'] is fake_org + + def test_with_no_organization(self, get_current_organization): + middleware = LmsCurrentOrganizationMiddleware() + request = Mock(session={}) + get_current_organization.return_value = None + + middleware.process_request(request) + assert request.session['organization'] is None From 8a4c2fa28314c83aacce83a8ea757a63aa0680db Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Tue, 19 Jan 2021 14:46:29 +0300 Subject: [PATCH 09/21] RedirectMiddleware: redirect function variable unshadow PyCharm notified me about this despite we never got this in production. --- openedx/core/djangoapps/appsembler/sites/middleware.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/appsembler/sites/middleware.py b/openedx/core/djangoapps/appsembler/sites/middleware.py index 141748041cd8..9000fb95fc02 100644 --- a/openedx/core/djangoapps/appsembler/sites/middleware.py +++ b/openedx/core/djangoapps/appsembler/sites/middleware.py @@ -61,7 +61,10 @@ def process_request(self, request): cache_key = '{prefix}-{site}'.format(prefix=settings.REDIRECT_CACHE_KEY_PREFIX, site=site.domain) redirects = cache.get(cache_key) if redirects is None: - redirects = {redirect.old_path: redirect.new_path for redirect in Redirect.objects.filter(site=site)} + redirects = { + django_redirect.old_path: django_redirect.new_path + for django_redirect in Redirect.objects.filter(site=site) + } cache.set(cache_key, redirects, settings.REDIRECT_CACHE_TIMEOUT) redirect_to = redirects.get(request.path) if redirect_to: From 26e2503c47d9ad748c03ee4def5b39fc5c3bc99e Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Wed, 20 Jan 2021 15:53:42 +0300 Subject: [PATCH 10/21] RED-1431: bump django-tiers to v0.2.3 for trial expiration fixes --- requirements/edx/appsembler.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/appsembler.txt b/requirements/edx/appsembler.txt index 75172135e116..feb28c116665 100644 --- a/requirements/edx/appsembler.txt +++ b/requirements/edx/appsembler.txt @@ -4,7 +4,7 @@ python-intercom==0.2.13 raven==6.10.0 requests==2.9.1 django-anymail==5.0 -django-tiers==0.2.1 +django-tiers==0.2.3 dj-database-url==0.4.2 psycopg2-binary==2.8.3 django-compat==1.0.14 From efb1e1757a7d973df961513b9285324ce8255b7c Mon Sep 17 00:00:00 2001 From: Anders Pearson Date: Tue, 2 Feb 2021 14:27:25 +0000 Subject: [PATCH 11/21] add honeycomb tracing and context on registration API this seems like an important part of our code to be able to inspect more easily in production. --- .../djangoapps/appsembler/sites/serializers.py | 16 ++++++++++++++++ .../core/djangoapps/appsembler/sites/utils.py | 3 +++ 2 files changed, 19 insertions(+) diff --git a/openedx/core/djangoapps/appsembler/sites/serializers.py b/openedx/core/djangoapps/appsembler/sites/serializers.py index 5621dca76d5c..6273ee0c05e8 100644 --- a/openedx/core/djangoapps/appsembler/sites/serializers.py +++ b/openedx/core/djangoapps/appsembler/sites/serializers.py @@ -1,3 +1,4 @@ +import beeline from django.conf import settings from django.contrib.sites.models import Site from django.db import transaction @@ -31,7 +32,9 @@ class Meta: model = SiteConfiguration fields = ('id', 'values', 'sassVariables', 'pageElements') + @beeline.traced(name="SiteConfigurationSerializer.update") def update(self, instance, validated_data): + beeline.add_context_field("validated_data", validated_data) object = super(SiteConfigurationSerializer, self).update(instance, validated_data) return object @@ -48,13 +51,17 @@ class Meta: model = AlternativeDomain fields = ('id', 'site', 'domain') + @beeline.traced(name="AlternativeDomainSerializer.create") def create(self, validated_data): """ Allow only one alternative domain per Site model. """ + beeline.add_context_field("validated_data", validated_data) domain, created = AlternativeDomain.objects.get_or_create( site=validated_data.get('site', None), defaults={'domain': validated_data.get('domain', None)}) + beeline.add_context_field("domain", domain) + beeline.add_context_field("created", created) if not created: domain.domain = validated_data.get('domain', None) domain.save() @@ -70,11 +77,14 @@ class Meta: model = Site fields = ('id', 'name', 'domain', 'configuration', 'alternativeDomain', 'customDomainStatus') + @beeline.traced(name="SiteSerializer.create") def create(self, validated_data): + beeline.add_context_field("validated_data", validated_data) site = super(SiteSerializer, self).create(validated_data) organization, site, user = bootstrap_site(site) return site + @beeline.traced(name="SiteSerializer.custom_domain_status") def custom_domain_status(self, obj): if not hasattr(obj, 'alternative_domain'): return 'inactive' @@ -86,7 +96,9 @@ class Meta: model = Organization fields = ('id', 'name', 'short_name', 'edx_uuid') + @beeline.traced(name="OrganizationSerializer.create") def create(self, validated_data): + beeline.add_context_field("validated_data", validated_data) return organizations_api.add_organization(**validated_data) @@ -98,7 +110,9 @@ class RegistrationSerializer(serializers.Serializer): password = serializers.CharField(required=False) initial_values = serializers.DictField(required=False) + @beeline.traced(name="RegistrationSerializer.create") def create(self, validated_data): + beeline.add_context_field(validated_data, validated_data) site_data = validated_data.pop('site') site = Site.objects.create(**site_data) organization_data = validated_data.pop('organization') @@ -140,6 +154,8 @@ def create(self, validated_data): # clone course if settings.FEATURES.get("APPSEMBLER_IMPORT_DEFAULT_COURSE_ON_SITE_CREATION", False): + beeline.add_context_field("default_course_on_site_creation_flag", True) + def import_task_on_commit(): """ Run the import task after the commit to avoid Organization.DoesNotExist error on the Celery. diff --git a/openedx/core/djangoapps/appsembler/sites/utils.py b/openedx/core/djangoapps/appsembler/sites/utils.py index 86386024931d..cdc1bb36807b 100644 --- a/openedx/core/djangoapps/appsembler/sites/utils.py +++ b/openedx/core/djangoapps/appsembler/sites/utils.py @@ -438,10 +438,13 @@ def json_to_sass(json_input): def bootstrap_site(site, org_data=None, username=None): from openedx.core.djangoapps.site_configuration.models import SiteConfiguration organization_slug = org_data.get('name') + beeline.add_context_field("org_data", org_data) + beeline.add_context_field("username", username) # don't use create because we need to call save() to set some values automatically site_config = SiteConfiguration(site=site, enabled=True) site_config.save() site.configuration_id = site_config.id + beeline.add_context_field("site_config_id", site_config.id) # temp workarounds while old staging is still up and running if organization_slug: organization_data = org_api.add_organization({ From 6afa0cfca13e502b21d0d7c8c524b9c3e130c1ca Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Thu, 4 Feb 2021 18:12:04 +0300 Subject: [PATCH 12/21] get_active_sites() to return QuerySet - added tests - added order_by parameter - refactored `_get_active_tiers_uuids` to isolate the Tiers import --- .../appsembler/sites/tests/test_utils.py | 48 ++++++++++++++++++- .../core/djangoapps/appsembler/sites/utils.py | 30 ++++++++---- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/openedx/core/djangoapps/appsembler/sites/tests/test_utils.py b/openedx/core/djangoapps/appsembler/sites/tests/test_utils.py index d3ade4455512..61629f123387 100644 --- a/openedx/core/djangoapps/appsembler/sites/tests/test_utils.py +++ b/openedx/core/djangoapps/appsembler/sites/tests/test_utils.py @@ -1,12 +1,18 @@ import unittest + +from django.db.models import QuerySet from mock import patch from django.core.exceptions import ImproperlyConfigured, MultipleObjectsReturned -from django.test import TestCase, override_settings +from django.test import TestCase from django.test.client import RequestFactory from openedx.core.djangoapps.appsembler.api.tests.factories import OrganizationFactory -from openedx.core.djangoapps.appsembler.sites.utils import get_current_organization, get_initial_page_elements +from openedx.core.djangoapps.appsembler.sites.utils import ( + get_current_organization, + get_initial_page_elements, + get_active_sites, +) from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from organizations.models import Organization @@ -24,6 +30,44 @@ def test_initial_page_elements(self): }) +class ActiveSitesTestCase(TestCase): + def setUp(self): + super(ActiveSitesTestCase, self).setUp() + self.siteFoo = SiteFactory.create(domain='foo.dev', name='foo.dev') + self.siteBar = SiteFactory.create(domain='bar.dev', name='bar.dev') + self.organizationA = OrganizationFactory(sites=[self.siteFoo]) + self.organizationB = OrganizationFactory(sites=[self.siteBar]) + + def test_get_active_sites(self): + """ + Basic test for results. + """ + with patch('openedx.core.djangoapps.appsembler.sites.utils.get_active_organizations') as mocked: + mocked.return_value = [self.organizationA, self.organizationB] + active_sites = get_active_sites() + assert len(active_sites) == 2 + assert active_sites[0].domain == 'bar.dev' + assert active_sites[1].domain == 'foo.dev' + + def test_get_active_sites_queryset(self): + """ + Should return QuerySet to work well with ViewSets and other plugins. + """ + with patch('openedx.core.djangoapps.appsembler.sites.utils.get_active_organizations') as mocked: + mocked.return_value = [self.organizationA, self.organizationB] + active_sites = get_active_sites() + assert type(active_sites) == QuerySet + + def test_get_active_sites_ordering(self): + """ + Result ordering is useful for tests but it's worth testing it itself. + """ + with patch('openedx.core.djangoapps.appsembler.sites.utils.get_active_organizations') as mocked: + mocked.return_value = [self.organizationA, self.organizationB] + active_sites = get_active_sites('-domain') + assert active_sites[0].domain == 'foo.dev' + + class OrganizationByRequestTestCase(TestCase): def setUp(self): super(OrganizationByRequestTestCase, self).setUp() diff --git a/openedx/core/djangoapps/appsembler/sites/utils.py b/openedx/core/djangoapps/appsembler/sites/utils.py index cdc1bb36807b..c87fe4b48126 100644 --- a/openedx/core/djangoapps/appsembler/sites/utils.py +++ b/openedx/core/djangoapps/appsembler/sites/utils.py @@ -54,9 +54,9 @@ def get_site_by_organization(org): return org.sites.all()[0] -def get_active_organizations(): +def _get_active_tiers_uuids(): """ - Get active organizations based on Tiers information. + Get active Tier organiation UUIDs from the Tiers (AMC Postgres) database. Note: This mostly a hack that's needed for improving the performance of batch operations by excluding dead sites. @@ -65,20 +65,33 @@ def get_active_organizations(): """ from tiers.models import Tier # This queries the AMC Postgres database - active_tiers = Tier.objects.filter( + active_tiers_uuids = Tier.objects.filter( Q(tier_enforcement_exempt=True) | Q(tier_expires_at__gte=timezone.now()) ).annotate( organization_edx_uuid=F('organization__edx_uuid') ).values_list('organization_edx_uuid', flat=True) + return active_tiers_uuids + + +def get_active_organizations(): + """ + Get active organizations based on Tiers information. + + Note: This mostly a hack that's needed for improving the performance of + batch operations by excluding dead sites. + + TODO: This helper should live in a future Tahoe Sites package. + """ + active_tiers_uuids = _get_active_tiers_uuids() # Now back to the LMS MySQL database return Organization.objects.filter( - edx_uuid__in=[str(edx_uuid) for edx_uuid in active_tiers], + edx_uuid__in=[str(edx_uuid) for edx_uuid in active_tiers_uuids], ) -def get_active_sites(): +def get_active_sites(order_by='domain'): """ Get active sites based on Tiers information. @@ -87,10 +100,9 @@ def get_active_sites(): TODO: This helper should live in a future Tahoe Sites package. """ - sites = [] - for organization in get_active_organizations(): - sites.extend(organization.sites.all()) - return sites + return Site.objects.filter( + organizations__in=get_active_organizations() + ).order_by(order_by) @beeline.traced(name="get_amc_oauth_client") From 6d453b2a3e1c029a8365650f3463591b3afd1160 Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Thu, 4 Feb 2021 18:27:12 +0300 Subject: [PATCH 13/21] beeline: fix unhashable type: 'dict' --- openedx/core/djangoapps/appsembler/sites/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/appsembler/sites/serializers.py b/openedx/core/djangoapps/appsembler/sites/serializers.py index 6273ee0c05e8..f7321870fc10 100644 --- a/openedx/core/djangoapps/appsembler/sites/serializers.py +++ b/openedx/core/djangoapps/appsembler/sites/serializers.py @@ -112,7 +112,7 @@ class RegistrationSerializer(serializers.Serializer): @beeline.traced(name="RegistrationSerializer.create") def create(self, validated_data): - beeline.add_context_field(validated_data, validated_data) + beeline.add_context_field('validated_data', validated_data) site_data = validated_data.pop('site') site = Site.objects.create(**site_data) organization_data = validated_data.pop('organization') From 298d3146d7191ffa7062e72229b5a3e111964860 Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Wed, 3 Feb 2021 19:07:05 -0800 Subject: [PATCH 14/21] FEATURE FLAG-gate the inclusion of Appsembler Redirect middlewares Use the django_sites_extensions RedirectMiddleware, but if TAHOE_ENABLE_DOMAIN_REDIRECT_MIDDLEWARE enabled (default ON), then insert related middleware classes before that standard redirect class. --- lms/envs/common.py | 3 +- .../appsembler/settings/settings/aws_lms.py | 34 ++++++++++++------- .../settings/settings/devstack_lms.py | 3 +- .../djangoapps/appsembler/sites/middleware.py | 14 +------- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index eec1d3868431..e1f24b385b82 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1242,8 +1242,7 @@ def _make_locale_paths(settings): 'django.contrib.sites.middleware.CurrentSiteMiddleware', # Allows us to define redirects via Django admin - 'openedx.core.djangoapps.appsembler.sites.middleware.CustomDomainsRedirectMiddleware', - 'openedx.core.djangoapps.appsembler.sites.middleware.RedirectMiddleware', + 'django_sites_extensions.middleware.RedirectMiddleware', # Instead of SessionMiddleware, we use a more secure version # 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py b/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py index 721b3abae0ef..f35cfae0e07c 100644 --- a/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py +++ b/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py @@ -6,6 +6,9 @@ from openedx.core.djangoapps.appsembler.settings.settings import aws_common +EDX_SITE_REDIRECT_MIDDLEWARE = "django_sites_extensions.middleware.RedirectMiddleware" + + def _add_theme_static_dirs(settings): """ Appsembler Themes static files customizations. @@ -50,6 +53,25 @@ def plugin_settings(settings): aws_common.plugin_settings(settings) + if settings.APPSEMBLER_FEATURES.get("TAHOE_ENABLE_DOMAIN_REDIRECT_MIDDLEWARE", True): + redir_middleware = settings.MIDDLEWARE_CLASSES.index(EDX_SITE_REDIRECT_MIDDLEWARE) + for tahoe_redir_middleware in ( + 'openedx.core.djangoapps.appsembler.sites.middleware.CustomDomainsRedirectMiddleware', + 'openedx.core.djangoapps.appsembler.sites.middleware.RedirectMiddleware' + ): + settings.MIDDLEWARE_CLASSES.insert(redir_middleware, tahoe_redir_middleware) + # This is used in the appsembler_sites.middleware.RedirectMiddleware to exclude certain paths + # from the redirect mechanics. + settings.MAIN_SITE_REDIRECT_WHITELIST = [ + 'api', + 'admin', + 'oauth', + 'status', + '/heartbeat', + '/accounts/manage_user_standing', + '/accounts/disable_account_ajax', + ] + settings.LMS_BASE = settings.ENV_TOKENS.get('LMS_BASE') settings.INSTALLED_APPS += ( @@ -99,18 +121,6 @@ def plugin_settings(settings): if settings.SENTRY_DSN: settings.RAVEN_CONFIG['tags']['app'] = 'lms' - # This is used in the appsembler_sites.middleware.RedirectMiddleware to exclude certain paths - # from the redirect mechanics. - settings.MAIN_SITE_REDIRECT_WHITELIST = [ - 'api', - 'admin', - 'oauth', - 'status', - '/heartbeat', - '/accounts/manage_user_standing', - '/accounts/disable_account_ajax', - ] - settings.USE_S3_FOR_CUSTOMER_THEMES = True _add_theme_static_dirs(settings) diff --git a/openedx/core/djangoapps/appsembler/settings/settings/devstack_lms.py b/openedx/core/djangoapps/appsembler/settings/settings/devstack_lms.py index 0c820cf1698d..9d8c1eef6cb5 100644 --- a/openedx/core/djangoapps/appsembler/settings/settings/devstack_lms.py +++ b/openedx/core/djangoapps/appsembler/settings/settings/devstack_lms.py @@ -35,4 +35,5 @@ def plugin_settings(settings): # This is used in the appsembler_sites.middleware.RedirectMiddleware to exclude certain paths # from the redirect mechanics. - settings.MAIN_SITE_REDIRECT_WHITELIST += ['/media/'] + if settings.APPSEMBLER_FEATURES.get("TAHOE_ENABLE_DOMAIN_REDIRECT_MIDDLEWARE", True): + settings.MAIN_SITE_REDIRECT_WHITELIST += ['/media/'] diff --git a/openedx/core/djangoapps/appsembler/sites/middleware.py b/openedx/core/djangoapps/appsembler/sites/middleware.py index bd911802939b..1da829a5a948 100644 --- a/openedx/core/djangoapps/appsembler/sites/middleware.py +++ b/openedx/core/djangoapps/appsembler/sites/middleware.py @@ -40,8 +40,7 @@ def process_request(self, request): class RedirectMiddleware(object): """ - Redirects requests for URLs persisted using the django.contrib.redirects.models.Redirect model. - With the exception of the main site. + Redirects requests for main site to Tahoe marketing page, except whitelisted. """ def process_request(self, request): """ @@ -61,17 +60,6 @@ def process_request(self, request): # Soooo just in case beeline.add_trace_field("redirect_middleware_exception", True) pass - cache_key = '{prefix}-{site}'.format(prefix=settings.REDIRECT_CACHE_KEY_PREFIX, site=site.domain) - redirects = cache.get(cache_key) - if redirects is None: - redirects = { - django_redirect.old_path: django_redirect.new_path - for django_redirect in Redirect.objects.filter(site=site) - } - cache.set(cache_key, redirects, settings.REDIRECT_CACHE_TIMEOUT) - redirect_to = redirects.get(request.path) - if redirect_to: - return redirect(redirect_to, permanent=True) class LmsCurrentOrganizationMiddleware(object): From f9cae26d4a3e8f4995f41f1aa92cc4ec9b62f976 Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Wed, 3 Feb 2021 19:09:14 -0800 Subject: [PATCH 15/21] Tahoe RedirectMiddleware use TAHOE_MAINSITE_REDIRECT_URL ENV_TOKEN with fallback to https://appsembler.com/tahoe --- .../core/djangoapps/appsembler/settings/settings/aws_lms.py | 4 ++++ openedx/core/djangoapps/appsembler/sites/middleware.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py b/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py index f35cfae0e07c..32de8c796100 100644 --- a/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py +++ b/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py @@ -60,6 +60,10 @@ def plugin_settings(settings): 'openedx.core.djangoapps.appsembler.sites.middleware.RedirectMiddleware' ): settings.MIDDLEWARE_CLASSES.insert(redir_middleware, tahoe_redir_middleware) + + settings.TAHOE_MAIN_SITE_REDIRECT_URL = settings.ENV_TOKENS.get( + 'TAHOE_MAIN_SITE_REDIRECT_URL', 'https://appsembler.com/tahoe/' + ) # This is used in the appsembler_sites.middleware.RedirectMiddleware to exclude certain paths # from the redirect mechanics. settings.MAIN_SITE_REDIRECT_WHITELIST = [ diff --git a/openedx/core/djangoapps/appsembler/sites/middleware.py b/openedx/core/djangoapps/appsembler/sites/middleware.py index 1da829a5a948..f38354860a56 100644 --- a/openedx/core/djangoapps/appsembler/sites/middleware.py +++ b/openedx/core/djangoapps/appsembler/sites/middleware.py @@ -54,7 +54,7 @@ def process_request(self, request): lambda p: p in request.path, settings.MAIN_SITE_REDIRECT_WHITELIST)) if (site.id == settings.SITE_ID) and not in_whitelist: - return redirect("https://appsembler.com/tahoe/") + return redirect(settings.TAHOE_MAIN_SITE_REDIRECT_URL) except Exception: # I'm not entirely sure this middleware get's called only in LMS or in other apps as well. # Soooo just in case From 2fb2af24d8d2774db156bc5929cd508f01505022 Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Thu, 4 Feb 2021 00:18:45 -0800 Subject: [PATCH 16/21] RedirectMiddleware tests, import cleanup --- .../djangoapps/appsembler/sites/middleware.py | 3 +- .../sites/tests/test_middlewares.py | 39 ++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/openedx/core/djangoapps/appsembler/sites/middleware.py b/openedx/core/djangoapps/appsembler/sites/middleware.py index f38354860a56..5d739a2f10aa 100644 --- a/openedx/core/djangoapps/appsembler/sites/middleware.py +++ b/openedx/core/djangoapps/appsembler/sites/middleware.py @@ -2,8 +2,7 @@ import logging from django.conf import settings -from django.core.cache import cache, caches -from django.contrib.redirects.models import Redirect +from django.core.cache import caches from django.shortcuts import redirect from openedx.core.djangoapps.appsembler.sites.models import AlternativeDomain diff --git a/openedx/core/djangoapps/appsembler/sites/tests/test_middlewares.py b/openedx/core/djangoapps/appsembler/sites/tests/test_middlewares.py index 0e663975a981..21ca5a7d5701 100644 --- a/openedx/core/djangoapps/appsembler/sites/tests/test_middlewares.py +++ b/openedx/core/djangoapps/appsembler/sites/tests/test_middlewares.py @@ -4,8 +4,10 @@ from mock import patch, Mock from django.test import TestCase +from django.test.client import RequestFactory -from openedx.core.djangoapps.appsembler.sites.middleware import LmsCurrentOrganizationMiddleware +from openedx.core.djangoapps.appsembler.sites.middleware import LmsCurrentOrganizationMiddleware, RedirectMiddleware +from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory @patch('openedx.core.djangoapps.appsembler.sites.middleware.get_current_organization') @@ -26,3 +28,38 @@ def test_with_no_organization(self, get_current_organization): middleware.process_request(request) assert request.session['organization'] is None + + +@patch('settings.TAHOE_MAIN_SITE_REDIRECT_URL', 'https://foo.bar') +class RedirectMiddlewareTests(TestCase): + def setup(self): + self.default_site = SiteFactory.create() + self.other_site = SiteFactory.create() + patcher = patch("settings.SITE_ID", self.default_site.id) + patcher.start() + self.addCleanup(patcher.stop) + + def test_redirects_default_site(self): + request = RequestFactory().get(self.default_site.domain) + request.site = self.default_site + processed = RedirectMiddleware().process_request(request) + self.assertRedirects(processed, 'https://foo.bar') + + def test_with_no_site_found(self): + request = RequestFactory().get("/") + request.site = None + processed = RedirectMiddleware().process_request(request) + self.assertEquals(processed, None) + + def test_no_redirect_other_site(self): + request = RequestFactory().get(self.other_site.domain) + request.site = self.other_site + processed = RedirectMiddleware().process_request(request) + self.assertEquals(processed, None) + + @patch('settings.MAIN_SITE_REDIRECT_WHITELIST', ['/baz']) + def test_url_in_whitelist(self): + request = RequestFactory().get('{}/baz'.format(self.default_site.domain)) + request.site = self.default_site + processed = RedirectMiddleware().process_request(request) + self.assertEquals(processed, None) From e603c70e860448295e4d6f2a8312ee26c1adf5cb Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Thu, 4 Feb 2021 00:40:35 -0800 Subject: [PATCH 17/21] Tahoe redirect middleware tests updates --- .../sites/tests/test_middlewares.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/openedx/core/djangoapps/appsembler/sites/tests/test_middlewares.py b/openedx/core/djangoapps/appsembler/sites/tests/test_middlewares.py index 21ca5a7d5701..ac86ce4e60ee 100644 --- a/openedx/core/djangoapps/appsembler/sites/tests/test_middlewares.py +++ b/openedx/core/djangoapps/appsembler/sites/tests/test_middlewares.py @@ -3,7 +3,7 @@ """ from mock import patch, Mock -from django.test import TestCase +from django.test import TestCase, override_settings from django.test.client import RequestFactory from openedx.core.djangoapps.appsembler.sites.middleware import LmsCurrentOrganizationMiddleware, RedirectMiddleware @@ -30,36 +30,37 @@ def test_with_no_organization(self, get_current_organization): assert request.session['organization'] is None -@patch('settings.TAHOE_MAIN_SITE_REDIRECT_URL', 'https://foo.bar') +@override_settings(TAHOE_MAIN_SITE_REDIRECT_URL='https://foo.bar') +@override_settings(MAIN_SITE_REDIRECT_WHITELIST=['/baz']) class RedirectMiddlewareTests(TestCase): - def setup(self): + def setUp(self): + super(RedirectMiddlewareTests, self).setUp() self.default_site = SiteFactory.create() self.other_site = SiteFactory.create() - patcher = patch("settings.SITE_ID", self.default_site.id) + patcher = patch("django.conf.settings.SITE_ID", self.default_site.id) patcher.start() self.addCleanup(patcher.stop) def test_redirects_default_site(self): - request = RequestFactory().get(self.default_site.domain) + request = RequestFactory().get('/path', HTTP_HOST=self.default_site.domain) request.site = self.default_site processed = RedirectMiddleware().process_request(request) - self.assertRedirects(processed, 'https://foo.bar') + self.assertEquals(processed.url, 'https://foo.bar') # middleware drops the path def test_with_no_site_found(self): - request = RequestFactory().get("/") + request = RequestFactory().get('/') request.site = None processed = RedirectMiddleware().process_request(request) - self.assertEquals(processed, None) + self.assertIsNone(processed) def test_no_redirect_other_site(self): - request = RequestFactory().get(self.other_site.domain) + request = RequestFactory().get('/', HTTP_HOST=self.other_site.domain) request.site = self.other_site processed = RedirectMiddleware().process_request(request) - self.assertEquals(processed, None) + self.assertIsNone(processed) - @patch('settings.MAIN_SITE_REDIRECT_WHITELIST', ['/baz']) def test_url_in_whitelist(self): - request = RequestFactory().get('{}/baz'.format(self.default_site.domain)) + request = RequestFactory().get('/baz', HTTP_HOST=self.default_site.domain) request.site = self.default_site processed = RedirectMiddleware().process_request(request) - self.assertEquals(processed, None) + self.assertIsNone(processed) From e4bf2b34223782cb9edbcdd35716cfca79e8b83b Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Thu, 4 Feb 2021 20:07:19 +0000 Subject: [PATCH 18/21] constant for TAHOE_MARKETING_SITE_URL in settings --- .../core/djangoapps/appsembler/settings/settings/aws_lms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py b/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py index 32de8c796100..1d9fe93501ca 100644 --- a/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py +++ b/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py @@ -7,6 +7,7 @@ EDX_SITE_REDIRECT_MIDDLEWARE = "django_sites_extensions.middleware.RedirectMiddleware" +TAHOE_MARKETING_SITE_URL = "https://appsembler.com/tahoe" def _add_theme_static_dirs(settings): @@ -62,7 +63,7 @@ def plugin_settings(settings): settings.MIDDLEWARE_CLASSES.insert(redir_middleware, tahoe_redir_middleware) settings.TAHOE_MAIN_SITE_REDIRECT_URL = settings.ENV_TOKENS.get( - 'TAHOE_MAIN_SITE_REDIRECT_URL', 'https://appsembler.com/tahoe/' + 'TAHOE_MAIN_SITE_REDIRECT_URL', TAHOE_MARKETING_SITE_URL ) # This is used in the appsembler_sites.middleware.RedirectMiddleware to exclude certain paths # from the redirect mechanics. From baccd3aa9d14ce5bcea780495d96414386931e22 Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Thu, 18 Feb 2021 21:40:46 -0800 Subject: [PATCH 19/21] Clarify variable name and fix insert ordering of Tahoe redir middleware committing Omar's suggestion Co-authored-by: Omar Al-Ithawi --- .../appsembler/settings/settings/aws_lms.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py b/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py index 1d9fe93501ca..202123b0215b 100644 --- a/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py +++ b/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py @@ -55,12 +55,15 @@ def plugin_settings(settings): aws_common.plugin_settings(settings) if settings.APPSEMBLER_FEATURES.get("TAHOE_ENABLE_DOMAIN_REDIRECT_MIDDLEWARE", True): - redir_middleware = settings.MIDDLEWARE_CLASSES.index(EDX_SITE_REDIRECT_MIDDLEWARE) - for tahoe_redir_middleware in ( - 'openedx.core.djangoapps.appsembler.sites.middleware.CustomDomainsRedirectMiddleware', + redir_middleware_index = settings.MIDDLEWARE_CLASSES.index(EDX_SITE_REDIRECT_MIDDLEWARE) + settings.MIDDLEWARE_CLASSES.insert( + redir_middleware_index, # Insert after Django RedirectMiddleware + 'openedx.core.djangoapps.appsembler.sites.middleware.CustomDomainsRedirectMiddleware' + ) + settings.MIDDLEWARE_CLASSES.insert( + redir_middleware_index + 1, # Insert after CustomDomainsRedirectMiddleware 'openedx.core.djangoapps.appsembler.sites.middleware.RedirectMiddleware' - ): - settings.MIDDLEWARE_CLASSES.insert(redir_middleware, tahoe_redir_middleware) + ) settings.TAHOE_MAIN_SITE_REDIRECT_URL = settings.ENV_TOKENS.get( 'TAHOE_MAIN_SITE_REDIRECT_URL', TAHOE_MARKETING_SITE_URL From 577fd48c4f1c9d5013013f6bf05a834a3fbc66b1 Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Tue, 2 Feb 2021 12:11:26 -0800 Subject: [PATCH 20/21] get_lms_link_from_course_key fallback to LMS base if no Site found Rework get_lms_link_from_course_key using get_site_for_course @OmarIthawi suggestion Co-authored-by: Omar Al-Ithawi Fix import issue in get_lms_link_for_course_key --- openedx/core/djangoapps/appsembler/sites/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openedx/core/djangoapps/appsembler/sites/utils.py b/openedx/core/djangoapps/appsembler/sites/utils.py index c87fe4b48126..3edb1f1a5e0e 100644 --- a/openedx/core/djangoapps/appsembler/sites/utils.py +++ b/openedx/core/djangoapps/appsembler/sites/utils.py @@ -37,12 +37,12 @@ def get_lms_link_from_course_key(base_lms_url, course_key): """ beeline.add_context_field("base_lms_url", base_lms_url) beeline.add_context_field("course_key", course_key) - try: - site_domain = Site.objects.get(name=course_key.org).domain - except Site.DoesNotExist: - site_domain = "{}.{}".format(course_key.org, base_lms_url) - - return site_domain + # avoid circular import + from openedx.core.djangoapps.appsembler.api.sites import get_site_for_course + course_site = get_site_for_course(course_key) + if course_site: + return course_site.domain + return base_lms_url @beeline.traced(name="get_site_by_organization") From f209638d3bb20d3d54bfb3a74052a7a6382f3cdc Mon Sep 17 00:00:00 2001 From: Bryan Wilson Date: Mon, 22 Feb 2021 22:50:16 +0000 Subject: [PATCH 21/21] Removed unused preview property and add unit tests for get_lms_link_from_course_key --- .../appsembler/sites/tests/test_utils.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/openedx/core/djangoapps/appsembler/sites/tests/test_utils.py b/openedx/core/djangoapps/appsembler/sites/tests/test_utils.py index 61629f123387..46f03aae80ac 100644 --- a/openedx/core/djangoapps/appsembler/sites/tests/test_utils.py +++ b/openedx/core/djangoapps/appsembler/sites/tests/test_utils.py @@ -12,8 +12,10 @@ get_current_organization, get_initial_page_elements, get_active_sites, + get_lms_link_from_course_key ) from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory + from organizations.models import Organization @@ -126,3 +128,36 @@ def test_raises_if_multiorg_feature_and_multitenant_email_feature_on(self): self.request.site = self.siteFoo with self.assertRaises(ImproperlyConfigured): get_current_organization() + + +class LMSLinkByCourseOrgTestCase(TestCase): + """ + Exercise getting the appropriate LMS Link for Studio "View in LMS" + based on the organization value set on the course that is being viewed. + (note we don't test with custom domains since that is handled by middleware) + """ + def setUp(self): + super(LMSLinkByCourseOrgTestCase, self).setUp() + self.siteFoo = SiteFactory.create(domain='foo.dev', name='foo.dev') + self.courseKey = "course-v1:org+course+run" + self.base_lms_url = "lms_base.domain" + + @patch('openedx.core.djangoapps.appsembler.api.sites.get_site_for_course') + def test_lms_link_happy_path(self, mocked_get_site_for_course): + mocked_get_site_for_course.return_value = self.siteFoo + url = get_lms_link_from_course_key(self.base_lms_url, self.courseKey) + self.assertEqual(url, "foo.dev") + + @patch('openedx.core.djangoapps.appsembler.api.sites.get_site_for_course') + def test_lms_link_no_site_matching_course(self, mocked_get_site_for_course): + mocked_get_site_for_course.return_value = None + url = get_lms_link_from_course_key(self.base_lms_url, self.courseKey) + self.assertEqual(url, self.base_lms_url) + + @patch.dict('django.conf.settings.FEATURES', { + 'PREVIEW_LMS_BASE': 'preview.lms_base.domain' + }) + def test_lms_link_for_preview_always_return_preview_domain(self): + preview_url = "preview.lms_base.domain" + url = get_lms_link_from_course_key(preview_url, self.courseKey) + self.assertEqual(url, preview_url)