Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
9bcb1166
Commit
9bcb1166
authored
Jan 15, 2016
by
Matt Drayer
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #11073 from edx/ziafazal/WL-245
Ziafazal/wl-245: multiple backend support for microsite
parents
fdf540d2
4742e661
Show whitespace changes
Inline
Side-by-side
Showing
28 changed files
with
1921 additions
and
179 deletions
+1921
-179
cms/envs/aws.py
+13
-3
cms/envs/common.py
+19
-0
cms/envs/test.py
+41
-3
common/djangoapps/edxmako/paths.py
+13
-0
common/djangoapps/microsite_configuration/__init__.py
+31
-0
common/djangoapps/microsite_configuration/admin.py
+83
-0
common/djangoapps/microsite_configuration/backends/__init__.py
+7
-0
common/djangoapps/microsite_configuration/backends/base.py
+340
-0
common/djangoapps/microsite_configuration/backends/database.py
+211
-0
common/djangoapps/microsite_configuration/backends/filebased.py
+26
-0
common/djangoapps/microsite_configuration/microsite.py
+102
-83
common/djangoapps/microsite_configuration/migrations/0001_initial.py
+107
-0
common/djangoapps/microsite_configuration/migrations/__init__.py
+0
-0
common/djangoapps/microsite_configuration/models.py
+181
-0
common/djangoapps/microsite_configuration/tests/backends/__init__.py
+0
-0
common/djangoapps/microsite_configuration/tests/backends/test_base.py
+132
-0
common/djangoapps/microsite_configuration/tests/backends/test_database.py
+220
-0
common/djangoapps/microsite_configuration/tests/backends/test_filebased.py
+116
-0
common/djangoapps/microsite_configuration/tests/factories.py
+79
-0
common/djangoapps/microsite_configuration/tests/test_logic.py
+28
-8
common/djangoapps/microsite_configuration/tests/test_microsites.py
+42
-7
common/djangoapps/microsite_configuration/tests/test_middleware.py
+37
-16
common/djangoapps/microsite_configuration/tests/tests.py
+41
-0
common/djangoapps/util/url.py
+7
-0
lms/envs/aws.py
+12
-3
lms/envs/common.py
+16
-0
lms/envs/test.py
+5
-4
lms/startup.py
+12
-52
No files found.
cms/envs/aws.py
View file @
9bcb1166
...
...
@@ -312,9 +312,6 @@ VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', [])
MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED
=
ENV_TOKENS
.
get
(
"MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED"
,
5
)
MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS
=
ENV_TOKENS
.
get
(
"MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS"
,
15
*
60
)
MICROSITE_CONFIGURATION
=
ENV_TOKENS
.
get
(
'MICROSITE_CONFIGURATION'
,
{})
MICROSITE_ROOT_DIR
=
path
(
ENV_TOKENS
.
get
(
'MICROSITE_ROOT_DIR'
,
''
))
#### PASSWORD POLICY SETTINGS #####
PASSWORD_MIN_LENGTH
=
ENV_TOKENS
.
get
(
"PASSWORD_MIN_LENGTH"
)
PASSWORD_MAX_LENGTH
=
ENV_TOKENS
.
get
(
"PASSWORD_MAX_LENGTH"
)
...
...
@@ -365,6 +362,19 @@ XBLOCK_SETTINGS.setdefault("VideoModule", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.g
PROCTORING_BACKEND_PROVIDER
=
AUTH_TOKENS
.
get
(
"PROCTORING_BACKEND_PROVIDER"
,
PROCTORING_BACKEND_PROVIDER
)
PROCTORING_SETTINGS
=
ENV_TOKENS
.
get
(
"PROCTORING_SETTINGS"
,
PROCTORING_SETTINGS
)
################# MICROSITE ####################
# microsite specific configurations.
MICROSITE_CONFIGURATION
=
ENV_TOKENS
.
get
(
'MICROSITE_CONFIGURATION'
,
{})
MICROSITE_ROOT_DIR
=
path
(
ENV_TOKENS
.
get
(
'MICROSITE_ROOT_DIR'
,
''
))
# this setting specify which backend to be used when pulling microsite specific configuration
MICROSITE_BACKEND
=
ENV_TOKENS
.
get
(
"MICROSITE_BACKEND"
,
MICROSITE_BACKEND
)
# this setting specify which backend to be used when loading microsite specific templates
MICROSITE_TEMPLATE_BACKEND
=
ENV_TOKENS
.
get
(
"MICROSITE_TEMPLATE_BACKEND"
,
MICROSITE_TEMPLATE_BACKEND
)
# TTL for microsite database template cache
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
=
ENV_TOKENS
.
get
(
"MICROSITE_DATABASE_TEMPLATE_CACHE_TTL"
,
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
)
############################ OAUTH2 Provider ###################################
# OpenID Connect issuer ID. Normally the URL of the authentication endpoint.
...
...
cms/envs/common.py
View file @
9bcb1166
...
...
@@ -827,6 +827,10 @@ INSTALLED_APPS = (
# other apps that are. Django 1.8 wants to have imported models supported
# by installed apps.
'lms.djangoapps.verify_student'
,
# Microsite configuration application
'microsite_configuration'
,
)
...
...
@@ -1129,6 +1133,21 @@ DEPRECATED_BLOCK_TYPES = [
'graphical_slider_tool'
,
]
################################ Settings for Microsites ################################
### Select an implementation for the microsite backend
# for MICROSITE_BACKEND possible choices are
# 1. microsite_configuration.backends.filebased.FilebasedMicrositeBackend
# 2. microsite_configuration.backends.database.DatabaseMicrositeBackend
MICROSITE_BACKEND
=
'microsite_configuration.backends.filebased.FilebasedMicrositeBackend'
# for MICROSITE_TEMPLATE_BACKEND possible choices are
# 1. microsite_configuration.backends.filebased.FilebasedMicrositeTemplateBackend
# 2. microsite_configuration.backends.database.DatabaseMicrositeTemplateBackend
MICROSITE_TEMPLATE_BACKEND
=
'microsite_configuration.backends.filebased.FilebasedMicrositeTemplateBackend'
# TTL for microsite database template cache
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
=
5
*
60
#### PROCTORING CONFIGURATION DEFAULTS
PROCTORING_BACKEND_PROVIDER
=
{
...
...
cms/envs/test.py
View file @
9bcb1166
...
...
@@ -214,6 +214,8 @@ FEATURES['ENABLE_SERVICE_STATUS'] = True
FEATURES
[
'EMBARGO'
]
=
True
# set up some testing for microsites
FEATURES
[
'USE_MICROSITES'
]
=
True
MICROSITE_ROOT_DIR
=
COMMON_ROOT
/
'test'
/
'test_microsites'
MICROSITE_CONFIGURATION
=
{
"test_microsite"
:
{
"domain_prefix"
:
"testmicrosite"
,
...
...
@@ -231,15 +233,51 @@ MICROSITE_CONFIGURATION = {
"show_homepage_promo_video"
:
False
,
"course_index_overlay_text"
:
"This is a Test Microsite Overlay Text."
,
"course_index_overlay_logo_file"
:
"test_microsite/images/header-logo.png"
,
"homepage_overlay_html"
:
"<h1>This is a Test Microsite Overlay HTML</h1>"
"homepage_overlay_html"
:
"<h1>This is a Test Microsite Overlay HTML</h1>"
,
"ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER"
:
False
,
"COURSE_CATALOG_VISIBILITY_PERMISSION"
:
"see_in_catalog"
,
"COURSE_ABOUT_VISIBILITY_PERMISSION"
:
"see_about_page"
,
"ENABLE_SHOPPING_CART"
:
True
,
"ENABLE_PAID_COURSE_REGISTRATION"
:
True
,
"SESSION_COOKIE_DOMAIN"
:
"test_microsite.localhost"
,
"urls"
:
{
'ABOUT'
:
'testmicrosite/about'
,
'PRIVACY'
:
'testmicrosite/privacy'
,
'TOS_AND_HONOR'
:
'testmicrosite/tos-and-honor'
,
},
},
"microsite_with_logistration"
:
{
"domain_prefix"
:
"logistration"
,
"university"
:
"logistration"
,
"platform_name"
:
"Test logistration"
,
"logo_image_url"
:
"test_microsite/images/header-logo.png"
,
"email_from_address"
:
"test_microsite@edx.org"
,
"payment_support_email"
:
"test_microsite@edx.org"
,
"ENABLE_MKTG_SITE"
:
False
,
"ENABLE_COMBINED_LOGIN_REGISTRATION"
:
True
,
"SITE_NAME"
:
"test_microsite.localhost"
,
"course_org_filter"
:
"LogistrationX"
,
"course_about_show_social_links"
:
False
,
"css_overrides_file"
:
"test_microsite/css/test_microsite.css"
,
"show_partners"
:
False
,
"show_homepage_promo_video"
:
False
,
"course_index_overlay_text"
:
"Logistration."
,
"course_index_overlay_logo_file"
:
"test_microsite/images/header-logo.png"
,
"homepage_overlay_html"
:
"<h1>This is a Logistration HTML</h1>"
,
"ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER"
:
False
,
"COURSE_CATALOG_VISIBILITY_PERMISSION"
:
"see_in_catalog"
,
"COURSE_ABOUT_VISIBILITY_PERMISSION"
:
"see_about_page"
,
"ENABLE_SHOPPING_CART"
:
True
,
"ENABLE_PAID_COURSE_REGISTRATION"
:
True
,
"SESSION_COOKIE_DOMAIN"
:
"test_logistration.localhost"
,
},
"default"
:
{
"university"
:
"default_university"
,
"domain_prefix"
:
"www"
,
}
}
MICROSITE_
ROOT_DIR
=
COMMON_ROOT
/
'test'
/
'test_microsites
'
FEATURES
[
'USE_MICROSITES'
]
=
True
MICROSITE_
TEST_HOSTNAME
=
'testmicrosite.testserver
'
MICROSITE_LOGISTRATION_HOSTNAME
=
'logistration.testserver'
# For consistency in user-experience, keep the value of this setting in sync with
# the one in lms/envs/test.py
...
...
common/djangoapps/edxmako/paths.py
View file @
9bcb1166
...
...
@@ -10,6 +10,7 @@ import pkg_resources
from
django.conf
import
settings
from
mako.lookup
import
TemplateLookup
from
microsite_configuration
import
microsite
from
.
import
LOOKUP
...
...
@@ -46,6 +47,18 @@ class DynamicTemplateLookup(TemplateLookup):
self
.
_collection
.
clear
()
self
.
_uri_cache
.
clear
()
def
get_template
(
self
,
uri
):
"""
Overridden method which will hand-off the template lookup to the microsite subsystem
"""
microsite_template
=
microsite
.
get_template
(
uri
)
return
(
microsite_template
if
microsite_template
else
super
(
DynamicTemplateLookup
,
self
)
.
get_template
(
uri
)
)
def
clear_lookups
(
namespace
):
"""
...
...
common/djangoapps/microsite_configuration/__init__.py
View file @
9bcb1166
"""
This file implements a class which is a handy utility to make any
call to the settings completely microsite aware by replacing the:
from django.conf import settings
with:
from microsite_configuration import settings
"""
from
django.conf
import
settings
as
base_settings
from
microsite_configuration
import
microsite
from
.templatetags.microsite
import
page_title_breadcrumbs
class
MicrositeAwareSettings
(
object
):
"""
This class is a proxy object of the settings object from django.
It will try to get a value from the microsite and default to the
django settings
"""
def
__getattr__
(
self
,
name
):
try
:
if
isinstance
(
microsite
.
get_value
(
name
),
dict
):
return
microsite
.
get_dict
(
name
,
getattr
(
base_settings
,
name
))
return
microsite
.
get_value
(
name
,
getattr
(
base_settings
,
name
))
except
KeyError
:
return
getattr
(
base_settings
,
name
)
settings
=
MicrositeAwareSettings
()
# pylint: disable=invalid-name
common/djangoapps/microsite_configuration/admin.py
0 → 100644
View file @
9bcb1166
"""
Django admin page for microsite models
"""
from
django.contrib
import
admin
from
django
import
forms
from
.models
import
(
Microsite
,
MicrositeHistory
,
MicrositeOrganizationMapping
,
MicrositeTemplate
)
from
util.organizations_helpers
import
get_organizations
class
MicrositeAdmin
(
admin
.
ModelAdmin
):
""" Admin interface for the Microsite object. """
list_display
=
(
'key'
,
'site'
)
search_fields
=
(
'site__domain'
,
'values'
)
class
Meta
(
object
):
# pylint: disable=missing-docstring
model
=
Microsite
class
MicrositeHistoryAdmin
(
admin
.
ModelAdmin
):
""" Admin interface for the MicrositeHistory object. """
list_display
=
(
'key'
,
'site'
,
'created'
)
search_fields
=
(
'site__domain'
,
'values'
)
ordering
=
[
'-created'
]
class
Meta
(
object
):
# pylint: disable=missing-docstring
model
=
MicrositeHistory
def
has_add_permission
(
self
,
request
):
"""Don't allow adds"""
return
False
def
has_delete_permission
(
self
,
request
,
obj
=
None
):
"""Don't allow deletes"""
return
False
class
MicrositeOrganizationMappingForm
(
forms
.
ModelForm
):
"""
Django admin form for MicrositeOrganizationMapping model
"""
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
MicrositeOrganizationMappingForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
organizations
=
get_organizations
()
org_choices
=
[(
org
[
"short_name"
],
org
[
"name"
])
for
org
in
organizations
]
org_choices
.
insert
(
0
,
(
''
,
'None'
))
self
.
fields
[
'organization'
]
=
forms
.
TypedChoiceField
(
choices
=
org_choices
,
required
=
False
,
empty_value
=
None
)
class
Meta
(
object
):
model
=
MicrositeOrganizationMapping
fields
=
'__all__'
class
MicrositeOrganizationMappingAdmin
(
admin
.
ModelAdmin
):
""" Admin interface for the MicrositeOrganizationMapping object. """
list_display
=
(
'organization'
,
'microsite'
)
search_fields
=
(
'organization'
,
'microsite'
)
form
=
MicrositeOrganizationMappingForm
class
Meta
(
object
):
# pylint: disable=missing-docstring
model
=
MicrositeOrganizationMapping
class
MicrositeTemplateAdmin
(
admin
.
ModelAdmin
):
""" Admin interface for the MicrositeTemplate object. """
list_display
=
(
'microsite'
,
'template_uri'
)
search_fields
=
(
'microsite'
,
'template_uri'
)
class
Meta
(
object
):
# pylint: disable=missing-docstring
model
=
MicrositeTemplate
admin
.
site
.
register
(
Microsite
,
MicrositeAdmin
)
admin
.
site
.
register
(
MicrositeHistory
,
MicrositeHistoryAdmin
)
admin
.
site
.
register
(
MicrositeOrganizationMapping
,
MicrositeOrganizationMappingAdmin
)
admin
.
site
.
register
(
MicrositeTemplate
,
MicrositeTemplateAdmin
)
common/djangoapps/microsite_configuration/backends/__init__.py
0 → 100644
View file @
9bcb1166
"""
Supported backends for microsites
1. filebased
This backend supports retrieval of microsite configurations/templates from filesystem.
2. database
This backend supports retrieval of microsite configurations/templates from database.
"""
common/djangoapps/microsite_configuration/backends/base.py
0 → 100644
View file @
9bcb1166
"""
Microsite configuration backend module.
Contains the base classes for microsite backends.
AbstractBaseMicrositeBackend is Abstract Base Class for the microsite configuration backend.
BaseMicrositeBackend is Base Class for microsite configuration backend.
BaseMicrositeTemplateBackend is Base Class for the microsite template backend.
"""
from
__future__
import
absolute_import
import
abc
import
edxmako
import
os.path
import
threading
from
django.conf
import
settings
from
util.url
import
strip_port_from_host
# pylint: disable=unused-argument
class
AbstractBaseMicrositeBackend
(
object
):
"""
Abstract Base Class for the microsite backends.
"""
__metaclass__
=
abc
.
ABCMeta
def
__init__
(
self
,
**
kwargs
):
pass
@abc.abstractmethod
def
set_config_by_domain
(
self
,
domain
):
"""
For a given request domain, find a match in our microsite configuration
and make it available to the complete django request process
"""
raise
NotImplementedError
()
@abc.abstractmethod
def
get_value
(
self
,
val_name
,
default
=
None
,
**
kwargs
):
"""
Returns a value associated with the request's microsite, if present
"""
raise
NotImplementedError
()
@abc.abstractmethod
def
get_dict
(
self
,
dict_name
,
default
=
None
,
**
kwargs
):
"""
Returns a dictionary product of merging the request's microsite and
the default value.
This can be used, for example, to return a merged dictonary from the
settings.FEATURES dict, including values defined at the microsite
"""
raise
NotImplementedError
()
@abc.abstractmethod
def
is_request_in_microsite
(
self
):
"""
This will return True/False if the current request is a request within a microsite
"""
raise
NotImplementedError
()
@abc.abstractmethod
def
has_override_value
(
self
,
val_name
):
"""
Returns True/False whether a Microsite has a definition for the
specified named value
"""
raise
NotImplementedError
()
@abc.abstractmethod
def
get_all_config
(
self
):
"""
This returns a set of orgs that are considered within all microsites.
This can be used, for example, to do filtering
"""
raise
NotImplementedError
()
@abc.abstractmethod
def
get_value_for_org
(
self
,
org
,
val_name
,
default
=
None
):
"""
This returns a configuration value for a microsite which has an org_filter that matches
what is passed in
"""
raise
NotImplementedError
()
@abc.abstractmethod
def
get_all_orgs
(
self
):
"""
This returns a set of orgs that are considered within a microsite. This can be used,
for example, to do filtering
"""
raise
NotImplementedError
()
@abc.abstractmethod
def
clear
(
self
):
"""
Clears out any microsite configuration from the current request/thread
"""
raise
NotImplementedError
()
class
BaseMicrositeBackend
(
AbstractBaseMicrositeBackend
):
"""
Base class for Microsite backends.
"""
def
__init__
(
self
,
**
kwargs
):
super
(
BaseMicrositeBackend
,
self
)
.
__init__
(
**
kwargs
)
self
.
current_request_configuration
=
threading
.
local
()
self
.
current_request_configuration
.
data
=
{}
self
.
current_request_configuration
.
cache
=
{}
def
has_configuration_set
(
self
):
"""
Returns whether there is any Microsite configuration settings
"""
return
getattr
(
settings
,
"MICROSITE_CONFIGURATION"
,
False
)
def
get_configuration
(
self
):
"""
Returns the current request's microsite configuration.
if request's microsite configuration is not present returns empty dict.
"""
if
not
hasattr
(
self
.
current_request_configuration
,
'data'
):
return
{}
return
self
.
current_request_configuration
.
data
def
get_key_from_cache
(
self
,
key
):
"""
Retrieves a key from a cache scoped to the thread
"""
if
hasattr
(
self
.
current_request_configuration
,
'cache'
):
return
self
.
current_request_configuration
.
cache
.
get
(
key
)
def
set_key_to_cache
(
self
,
key
,
value
):
"""
Stores a key value pair in a cache scoped to the thread
"""
if
hasattr
(
self
.
current_request_configuration
,
'cache'
):
self
.
current_request_configuration
.
cache
[
key
]
=
value
def
set_config_by_domain
(
self
,
domain
):
"""
For a given request domain, find a match in our microsite configuration
and then assign it to the thread local in order to make it available
to the complete Django request processing
"""
if
not
self
.
has_configuration_set
()
or
not
domain
:
return
for
key
,
value
in
settings
.
MICROSITE_CONFIGURATION
.
items
():
subdomain
=
value
.
get
(
'domain_prefix'
)
if
subdomain
and
domain
.
startswith
(
subdomain
):
self
.
_set_microsite_config
(
key
,
subdomain
,
domain
)
return
# if no match on subdomain then see if there is a 'default' microsite defined
# if so, then use that
if
'default'
in
settings
.
MICROSITE_CONFIGURATION
:
self
.
_set_microsite_config
(
'default'
,
subdomain
,
domain
)
return
def
get_value
(
self
,
val_name
,
default
=
None
,
**
kwargs
):
"""
Returns a value associated with the request's microsite, if present
"""
configuration
=
self
.
get_configuration
()
return
configuration
.
get
(
val_name
,
default
)
def
get_dict
(
self
,
dict_name
,
default
=
None
,
**
kwargs
):
"""
Returns a dictionary product of merging the request's microsite and
the default value.
Supports storing a cache of the merged value to improve performance
"""
cached_dict
=
self
.
get_key_from_cache
(
dict_name
)
if
cached_dict
:
return
cached_dict
default
=
default
or
{}
output
=
default
.
copy
()
output
.
update
(
self
.
get_value
(
dict_name
,
{}))
self
.
set_key_to_cache
(
dict_name
,
output
)
return
output
def
is_request_in_microsite
(
self
):
"""
This will return if current request is a request within a microsite
"""
return
bool
(
self
.
get_configuration
())
def
has_override_value
(
self
,
val_name
):
"""
Will return True/False whether a Microsite has a definition for the
specified val_name
"""
configuration
=
self
.
get_configuration
()
return
val_name
in
configuration
def
get_all_config
(
self
):
"""
This returns all configuration for all microsites
"""
config
=
{}
for
key
,
value
in
settings
.
MICROSITE_CONFIGURATION
.
iteritems
():
config
[
key
]
=
value
return
config
def
get_value_for_org
(
self
,
org
,
val_name
,
default
=
None
):
"""
This returns a configuration value for a microsite which has an org_filter that matches
what is passed in
"""
if
not
self
.
has_configuration_set
():
return
default
# Filter at the setting file
for
value
in
settings
.
MICROSITE_CONFIGURATION
.
itervalues
():
org_filter
=
value
.
get
(
'course_org_filter'
,
None
)
if
org_filter
==
org
:
return
value
.
get
(
val_name
,
default
)
return
default
def
get_all_orgs
(
self
):
"""
This returns a set of orgs that are considered within a microsite. This can be used,
for example, to do filtering
"""
org_filter_set
=
set
()
if
not
self
.
has_configuration_set
():
return
org_filter_set
# Get the orgs in the db
for
microsite
in
settings
.
MICROSITE_CONFIGURATION
.
itervalues
():
org_filter
=
microsite
.
get
(
'course_org_filter'
)
if
org_filter
:
org_filter_set
.
add
(
org_filter
)
return
org_filter_set
def
_set_microsite_config
(
self
,
microsite_config_key
,
subdomain
,
domain
):
"""
Helper internal method to actually find the microsite configuration
"""
config
=
settings
.
MICROSITE_CONFIGURATION
[
microsite_config_key
]
.
copy
()
config
[
'subdomain'
]
=
strip_port_from_host
(
subdomain
)
config
[
'microsite_config_key'
]
=
microsite_config_key
config
[
'site_domain'
]
=
strip_port_from_host
(
domain
)
template_dir
=
settings
.
MICROSITE_ROOT_DIR
/
microsite_config_key
/
'templates'
config
[
'template_dir'
]
=
template_dir
self
.
current_request_configuration
.
data
=
config
def
clear
(
self
):
"""
Clears out any microsite configuration from the current request/thread
"""
self
.
current_request_configuration
.
data
=
{}
self
.
current_request_configuration
.
cache
=
{}
def
enable_microsites
(
self
,
log
):
"""
Configure the paths for the microsites feature
"""
microsites_root
=
settings
.
MICROSITE_ROOT_DIR
if
os
.
path
.
isdir
(
microsites_root
):
edxmako
.
paths
.
add_lookup
(
'main'
,
microsites_root
)
settings
.
STATICFILES_DIRS
.
insert
(
0
,
microsites_root
)
log
.
info
(
'Loading microsite path at
%
s'
,
microsites_root
)
else
:
log
.
error
(
'Error loading
%
s. Directory does not exist'
,
microsites_root
)
def
enable_microsites_pre_startup
(
self
,
log
):
"""
The TEMPLATE_ENGINE directory to search for microsite templates
in non-mako templates must be loaded before the django startup
"""
microsites_root
=
settings
.
MICROSITE_ROOT_DIR
microsite_config_dict
=
settings
.
MICROSITE_CONFIGURATION
if
microsite_config_dict
:
settings
.
DEFAULT_TEMPLATE_ENGINE
[
'DIRS'
]
.
append
(
microsites_root
)
class
BaseMicrositeTemplateBackend
(
object
):
"""
Interface for microsite template providers. Base implementation is to use the filesystem.
When this backend is used templates are first searched in location set in `template_dir`
configuration of microsite on filesystem.
"""
def
get_template_path
(
self
,
relative_path
,
**
kwargs
):
"""
Returns a path (string) to a Mako template, which can either be in
an override or will just return what is passed in which is expected to be a string
"""
from
microsite_configuration.microsite
import
get_value
as
microsite_get_value
microsite_template_path
=
microsite_get_value
(
'template_dir'
,
None
)
if
not
microsite_template_path
:
microsite_template_path
=
'/'
.
join
([
settings
.
MICROSITE_ROOT_DIR
,
microsite_get_value
(
'microsite_config_key'
,
'default'
),
'templates'
,
])
search_path
=
os
.
path
.
join
(
microsite_template_path
,
relative_path
)
if
os
.
path
.
isfile
(
search_path
):
path
=
'/{0}/templates/{1}'
.
format
(
microsite_get_value
(
'microsite_config_key'
),
relative_path
)
return
path
else
:
return
relative_path
def
get_template
(
self
,
uri
):
"""
Returns the actual template for the microsite with the specified URI,
default implementation returns None, which means that the caller framework
should use default behavior
"""
return
common/djangoapps/microsite_configuration/backends/database.py
0 → 100644
View file @
9bcb1166
"""
Microsite backend that reads the configuration from the database
"""
from
mako.template
import
Template
from
util.cache
import
cache
from
django.conf
import
settings
from
django.dispatch
import
receiver
from
django.db.models.signals
import
post_save
from
util.memcache
import
fasthash
from
util.url
import
strip_port_from_host
from
microsite_configuration.backends.base
import
(
BaseMicrositeBackend
,
BaseMicrositeTemplateBackend
,
)
from
microsite_configuration.models
import
(
Microsite
,
MicrositeOrganizationMapping
,
MicrositeTemplate
)
from
microsite_configuration.microsite
import
get_value
as
microsite_get_value
class
DatabaseMicrositeBackend
(
BaseMicrositeBackend
):
"""
Microsite backend that reads the microsites definitions
from a table in the database according to the models.py file
This backend would allow us to save microsite configurations
into database and load them in local storage when HTTRequest
is originated from microsite.
E.g. we have setup a microsite with key `monster-university-academy` and
We would have a DB entry like this in table created by Microsite model.
key = monster-university-academy
subdomain = mua.edx.org
values = {
"platform_name": "Monster University Academy".
"course_org_filter: "MonsterX"
}
While using DatabaseMicrositeBackend any request coming from mua.edx.org
would get microsite configurations from `values` column.
"""
def
has_configuration_set
(
self
):
"""
Returns whether there is any Microsite configuration settings
"""
if
Microsite
.
objects
.
all
()[:
1
]
.
exists
():
return
True
else
:
return
False
def
set_config_by_domain
(
self
,
domain
):
"""
For a given request domain, find a match in our microsite configuration
and then assign it to the thread local in order to make it available
to the complete Django request processing
"""
if
not
self
.
has_configuration_set
()
or
not
domain
:
return
# look up based on the HTTP request domain name
# this will need to be a full domain name match,
# not a 'startswith' match
microsite
=
Microsite
.
get_microsite_for_domain
(
domain
)
if
not
microsite
:
# if no match, then try to find a 'default' key in Microsites
try
:
microsite
=
Microsite
.
objects
.
get
(
key
=
'default'
)
except
Microsite
.
DoesNotExist
:
pass
if
microsite
:
# if we have a match, then set up the microsite thread local
# data
self
.
_set_microsite_config_from_obj
(
microsite
.
site
.
domain
,
domain
,
microsite
)
def
get_all_config
(
self
):
"""
This returns all configuration for all microsites
"""
config
=
{}
candidates
=
Microsite
.
objects
.
all
()
for
microsite
in
candidates
:
values
=
microsite
.
values
config
[
microsite
.
key
]
=
values
return
config
def
get_value_for_org
(
self
,
org
,
val_name
,
default
=
None
):
"""
This returns a configuration value for a microsite which has an org_filter that matches
what is passed in
"""
microsite
=
MicrositeOrganizationMapping
.
get_microsite_for_organization
(
org
)
if
not
microsite
:
return
default
# cdodge: This approach will not leverage any caching, although I think only Studio calls
# this
config
=
microsite
.
values
return
config
.
get
(
val_name
,
default
)
def
get_all_orgs
(
self
):
"""
This returns a set of orgs that are considered within a microsite. This can be used,
for example, to do filtering
"""
# This should be cacheable (via memcache to keep consistent across a cluster)
# I believe this is called on the dashboard and catalog pages, so it'd be good to optimize
return
set
(
MicrositeOrganizationMapping
.
objects
.
all
()
.
values_list
(
'organization'
,
flat
=
True
))
def
_set_microsite_config_from_obj
(
self
,
subdomain
,
domain
,
microsite_object
):
"""
Helper internal method to actually find the microsite configuration
"""
config
=
microsite_object
.
values
config
[
'subdomain'
]
=
strip_port_from_host
(
subdomain
)
config
[
'site_domain'
]
=
strip_port_from_host
(
domain
)
config
[
'microsite_config_key'
]
=
microsite_object
.
key
# we take the list of ORGs associated with this microsite from the database mapping
# tables. NOTE, for now, we assume one ORG per microsite
organizations
=
microsite_object
.
get_organizations
()
# we must have at least one ORG defined
if
not
organizations
:
raise
Exception
(
'Configuration error. Microsite {key} does not have any ORGs mapped to it!'
.
format
(
key
=
microsite_object
.
key
)
)
# just take the first one for now, we'll have to change the upstream logic to allow
# for more than one ORG binding
config
[
'course_org_filter'
]
=
organizations
[
0
]
self
.
current_request_configuration
.
data
=
config
class
DatabaseMicrositeTemplateBackend
(
BaseMicrositeTemplateBackend
):
"""
Specialized class to pull templates from the database.
This Backend would allow us to save templates in DB and pull
them from there when required for a specific microsite.
This backend can be enabled by `MICROSITE_TEMPLATE_BACKEND` setting.
E.g. we have setup a microsite for subdomain `mua.edx.org` and
We have a DB entry like this in table created by MicrositeTemplate model.
microsite = Key for microsite(mua.edx.org)
template_uri = about.html
template = <html><body>Template from DB</body></html>
While using DatabaseMicrositeTemplateBackend any request coming from mua.edx.org/about.html
would get about.html template from DB and response would be the value of `template` column.
"""
def
get_template_path
(
self
,
relative_path
,
**
kwargs
):
return
relative_path
def
get_template
(
self
,
uri
):
"""
Override of the base class for us to look into the
database tables for a template definition, if we can't find
one we'll return None which means "use default means" (aka filesystem)
"""
cache_key
=
"template_cache."
+
fasthash
(
microsite_get_value
(
'site_domain'
)
+
'.'
+
uri
)
template_text
=
cache
.
get
(
cache_key
)
# pylint: disable=maybe-no-member
if
not
template_text
:
# cache is empty so pull template from DB and fill cache.
template_obj
=
MicrositeTemplate
.
get_template_for_microsite
(
microsite_get_value
(
'site_domain'
),
uri
)
if
not
template_obj
:
# We need to set something in the cache to improve performance
# of the templates stored in the filesystem as well
cache
.
set
(
# pylint: disable=maybe-no-member
cache_key
,
'##none'
,
settings
.
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
)
return
None
template_text
=
template_obj
.
template
cache
.
set
(
# pylint: disable=maybe-no-member
cache_key
,
template_text
,
settings
.
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
)
if
template_text
==
'##none'
:
return
None
return
Template
(
text
=
template_text
)
@staticmethod
@receiver
(
post_save
,
sender
=
MicrositeTemplate
)
def
clear_cache
(
sender
,
instance
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Clear the cached template when the model is saved
"""
cache_key
=
"template_cache."
+
fasthash
(
instance
.
microsite
.
site
.
domain
+
'.'
+
instance
.
template_uri
)
cache
.
delete
(
cache_key
)
# pylint: disable=maybe-no-member
common/djangoapps/microsite_configuration/backends/filebased.py
0 → 100644
View file @
9bcb1166
"""
Microsite backend that reads the configuration from a file
"""
from
microsite_configuration.backends.base
import
(
BaseMicrositeBackend
,
BaseMicrositeTemplateBackend
,
)
class
FilebasedMicrositeBackend
(
BaseMicrositeBackend
):
"""
Microsite backend that reads the microsites definitions
from a dictionary called MICROSITE_CONFIGURATION in the settings file.
"""
def
__init__
(
self
,
**
kwargs
):
super
(
FilebasedMicrositeBackend
,
self
)
.
__init__
(
**
kwargs
)
class
FilebasedMicrositeTemplateBackend
(
BaseMicrositeTemplateBackend
):
"""
Microsite backend that loads templates from filesystem.
"""
pass
common/djangoapps/microsite_configuration/microsite.py
View file @
9bcb1166
...
...
@@ -6,79 +6,61 @@ A microsite enables the following features:
2) Present a landing page with a listing of courses that are specific to the 'brand'
3) Ability to swap out some branding elements in the website
"""
import
threading
import
os.path
import
inspect
from
importlib
import
import_module
from
django.conf
import
settings
from
microsite_configuration.backends.base
import
BaseMicrositeBackend
,
BaseMicrositeTemplateBackend
CURRENT_REQUEST_CONFIGURATION
=
threading
.
local
()
CURRENT_REQUEST_CONFIGURATION
.
data
=
{}
__all__
=
[
'is_request_in_microsite'
,
'get_value'
,
'has_override_value'
,
'get_template_path'
,
'get_value_for_org'
,
'get_all_orgs'
,
'clear'
,
'set_by_domain'
,
'enable_microsites'
,
'get_all_config'
,
'is_feature_enabled'
,
'enable_microsites_pre_startup'
,
]
def
has_configuration_set
():
"""
Returns whether there is any Microsite configuration settings
"""
return
getattr
(
settings
,
"MICROSITE_CONFIGURATION"
,
False
)
BACKEND
=
None
TEMPLATES_BACKEND
=
None
def
get_configuration
():
def
is_feature_enabled
():
"""
Returns
the current request's microsite configuration
Returns
whether the feature flag to enable microsite has been set
"""
if
not
hasattr
(
CURRENT_REQUEST_CONFIGURATION
,
'data'
):
return
{}
return
CURRENT_REQUEST_CONFIGURATION
.
data
return
settings
.
FEATURES
.
get
(
'USE_MICROSITES'
,
False
)
def
is_request_in_microsite
():
"""
This will return if current request is a request within a microsite
"""
return
bool
(
get_configuration
()
)
return
BACKEND
.
is_request_in_microsite
(
)
def
get_value
(
val_name
,
default
=
None
):
def
get_value
(
val_name
,
default
=
None
,
**
kwargs
):
"""
Returns a value associated with the request's microsite, if present
"""
configuration
=
get_configuration
()
return
configuration
.
get
(
val_name
,
default
)
return
BACKEND
.
get_value
(
val_name
,
default
,
**
kwargs
)
def
has_override_value
(
val_name
):
def
get_dict
(
dict_name
,
default
=
None
,
**
kwargs
):
"""
Returns True/False whether a Microsite has a definition for the
specified named value
Returns a dictionary product of merging the request's microsite and
the default value.
This can be used, for example, to return a merged dictonary from the
settings.FEATURES dict, including values defined at the microsite
"""
configuration
=
get_configuration
()
return
val_name
in
configuration
return
BACKEND
.
get_dict
(
dict_name
,
default
,
**
kwargs
)
def
get_template_path
(
relative_path
):
def
has_override_value
(
val_name
):
"""
Returns a path (string) to a Mako template, which can either be in
a microsite directory (as an override) or will just return what is passed in which is
expected to be a string
Returns True/False whether a Microsite has a definition for the
specified named value
"""
if
not
is_request_in_microsite
():
return
relative_path
microsite_template_path
=
str
(
get_value
(
'template_dir'
))
if
microsite_template_path
:
search_path
=
os
.
path
.
join
(
microsite_template_path
,
relative_path
)
if
os
.
path
.
isfile
(
search_path
):
path
=
'/{0}/templates/{1}'
.
format
(
get_value
(
'microsite_name'
),
relative_path
)
return
path
return
relative_path
return
BACKEND
.
has_override_value
(
val_name
)
def
get_value_for_org
(
org
,
val_name
,
default
=
None
):
...
...
@@ -86,14 +68,7 @@ def get_value_for_org(org, val_name, default=None):
This returns a configuration value for a microsite which has an org_filter that matches
what is passed in
"""
if
not
has_configuration_set
():
return
default
for
value
in
settings
.
MICROSITE_CONFIGURATION
.
values
():
org_filter
=
value
.
get
(
'course_org_filter'
,
None
)
if
org_filter
==
org
:
return
value
.
get
(
val_name
,
default
)
return
default
return
BACKEND
.
get_value_for_org
(
org
,
val_name
,
default
)
def
get_all_orgs
():
...
...
@@ -101,52 +76,96 @@ def get_all_orgs():
This returns a set of orgs that are considered within a microsite. This can be used,
for example, to do filtering
"""
org_filter_set
=
set
()
if
not
has_configuration_set
():
return
org_filter_set
return
BACKEND
.
get_all_orgs
()
for
value
in
settings
.
MICROSITE_CONFIGURATION
.
values
():
org_filter
=
value
.
get
(
'course_org_filter'
)
if
org_filter
:
org_filter_set
.
add
(
org_filter
)
return
org_filter_set
def
get_all_config
():
"""
This returns a dict have all microsite configs. Each key in the dict represent a
microsite config.
"""
return
BACKEND
.
get_all_config
()
def
clear
():
"""
Clears out any microsite configuration from the current request/thread
"""
CURRENT_REQUEST_CONFIGURATION
.
data
=
{}
BACKEND
.
clear
()
def
_set_current_microsite
(
microsite_config_key
,
subdomain
,
domain
):
def
set_by_domain
(
domain
):
"""
Helper internal method to actually put a microsite on the threadlocal
For a given request domain, find a match in our microsite configuration
and make it available to the complete django request process
"""
config
=
settings
.
MICROSITE_CONFIGURATION
[
microsite_config_key
]
.
copy
()
config
[
'subdomain'
]
=
subdomain
config
[
'microsite_config_key'
]
=
microsite_config_key
config
[
'site_domain'
]
=
domain
CURRENT_REQUEST_CONFIGURATION
.
data
=
config
BACKEND
.
set_config_by_domain
(
domain
)
def
set_by_domain
(
domain
):
def
enable_microsites_pre_startup
(
log
):
"""
For a given request domain, find a match in our microsite configuration and then assign
it to the thread local so that it is available throughout the entire
Django request processing
Prepare the feature settings that must be enabled before django.setup() or
autostartup() during the startup script
"""
if
not
has_configuration_set
()
or
not
domain
:
return
if
is_feature_enabled
():
BACKEND
.
enable_microsites_pre_startup
(
log
)
for
key
,
value
in
settings
.
MICROSITE_CONFIGURATION
.
items
():
subdomain
=
value
.
get
(
'domain_prefix'
)
if
subdomain
and
domain
.
startswith
(
subdomain
):
_set_current_microsite
(
key
,
subdomain
,
domain
)
def
enable_microsites
(
log
):
"""
Enable the use of microsites during the startup script
"""
if
is_feature_enabled
():
BACKEND
.
enable_microsites
(
log
)
def
get_template
(
uri
):
"""
Returns a template for the specified URI, None if none exists or if caller should
use default templates/search paths
"""
if
not
is_request_in_microsite
():
return
# if no match on subdomain then see if there is a 'default' microsite defined
# if so, then use that
if
'default'
in
settings
.
MICROSITE_CONFIGURATION
:
_set_current_microsite
(
'default'
,
subdomain
,
domain
)
return
TEMPLATES_BACKEND
.
get_template
(
uri
)
def
get_template_path
(
relative_path
,
**
kwargs
):
"""
Returns a path (string) to a template
"""
if
not
is_request_in_microsite
():
return
relative_path
return
TEMPLATES_BACKEND
.
get_template_path
(
relative_path
,
**
kwargs
)
def
get_backend
(
name
,
expected_base_class
,
**
kwds
):
"""
Load a microsites backend and return an instance of it.
If backend is None (default) settings.MICROSITE_BACKEND is used.
Any additional args(kwds) will be used in the constructor of the backend.
"""
if
not
name
:
return
None
try
:
parts
=
name
.
split
(
'.'
)
module_name
=
'.'
.
join
(
parts
[:
-
1
])
class_name
=
parts
[
-
1
]
except
IndexError
:
raise
ValueError
(
'Invalid microsites backend
%
s'
%
name
)
try
:
module
=
import_module
(
module_name
)
cls
=
getattr
(
module
,
class_name
)
if
not
inspect
.
isclass
(
cls
)
or
not
issubclass
(
cls
,
expected_base_class
):
raise
TypeError
except
(
AttributeError
,
ValueError
):
raise
ValueError
(
'Cannot find microsites backend
%
s'
%
module_name
)
return
cls
(
**
kwds
)
BACKEND
=
get_backend
(
settings
.
MICROSITE_BACKEND
,
BaseMicrositeBackend
)
TEMPLATES_BACKEND
=
get_backend
(
settings
.
MICROSITE_TEMPLATE_BACKEND
,
BaseMicrositeTemplateBackend
)
common/djangoapps/microsite_configuration/migrations/0001_initial.py
0 → 100644
View file @
9bcb1166
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
import
jsonfield.fields
import
django.db.models.deletion
from
django.conf
import
settings
import
model_utils.fields
import
django.utils.timezone
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'sites'
,
'0001_initial'
),
migrations
.
swappable_dependency
(
settings
.
AUTH_USER_MODEL
),
]
operations
=
[
migrations
.
CreateModel
(
name
=
'HistoricalMicrositeOrganizationMapping'
,
fields
=
[
(
'id'
,
models
.
IntegerField
(
verbose_name
=
'ID'
,
db_index
=
True
,
auto_created
=
True
,
blank
=
True
)),
(
'organization'
,
models
.
CharField
(
max_length
=
63
,
db_index
=
True
)),
(
'history_id'
,
models
.
AutoField
(
serialize
=
False
,
primary_key
=
True
)),
(
'history_date'
,
models
.
DateTimeField
()),
(
'history_type'
,
models
.
CharField
(
max_length
=
1
,
choices
=
[(
'+'
,
'Created'
),
(
'~'
,
'Changed'
),
(
'-'
,
'Deleted'
)])),
(
'history_user'
,
models
.
ForeignKey
(
related_name
=
'+'
,
on_delete
=
django
.
db
.
models
.
deletion
.
SET_NULL
,
to
=
settings
.
AUTH_USER_MODEL
,
null
=
True
)),
],
options
=
{
'ordering'
:
(
'-history_date'
,
'-history_id'
),
'get_latest_by'
:
'history_date'
,
'verbose_name'
:
'historical microsite organization mapping'
,
},
),
migrations
.
CreateModel
(
name
=
'HistoricalMicrositeTemplate'
,
fields
=
[
(
'id'
,
models
.
IntegerField
(
verbose_name
=
'ID'
,
db_index
=
True
,
auto_created
=
True
,
blank
=
True
)),
(
'template_uri'
,
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)),
(
'template'
,
models
.
TextField
()),
(
'history_id'
,
models
.
AutoField
(
serialize
=
False
,
primary_key
=
True
)),
(
'history_date'
,
models
.
DateTimeField
()),
(
'history_type'
,
models
.
CharField
(
max_length
=
1
,
choices
=
[(
'+'
,
'Created'
),
(
'~'
,
'Changed'
),
(
'-'
,
'Deleted'
)])),
(
'history_user'
,
models
.
ForeignKey
(
related_name
=
'+'
,
on_delete
=
django
.
db
.
models
.
deletion
.
SET_NULL
,
to
=
settings
.
AUTH_USER_MODEL
,
null
=
True
)),
],
options
=
{
'ordering'
:
(
'-history_date'
,
'-history_id'
),
'get_latest_by'
:
'history_date'
,
'verbose_name'
:
'historical microsite template'
,
},
),
migrations
.
CreateModel
(
name
=
'Microsite'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
verbose_name
=
'ID'
,
serialize
=
False
,
auto_created
=
True
,
primary_key
=
True
)),
(
'key'
,
models
.
CharField
(
unique
=
True
,
max_length
=
63
,
db_index
=
True
)),
(
'values'
,
jsonfield
.
fields
.
JSONField
(
blank
=
True
)),
(
'site'
,
models
.
OneToOneField
(
related_name
=
'microsite'
,
to
=
'sites.Site'
)),
],
),
migrations
.
CreateModel
(
name
=
'MicrositeHistory'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
verbose_name
=
'ID'
,
serialize
=
False
,
auto_created
=
True
,
primary_key
=
True
)),
(
'created'
,
model_utils
.
fields
.
AutoCreatedField
(
default
=
django
.
utils
.
timezone
.
now
,
verbose_name
=
'created'
,
editable
=
False
)),
(
'modified'
,
model_utils
.
fields
.
AutoLastModifiedField
(
default
=
django
.
utils
.
timezone
.
now
,
verbose_name
=
'modified'
,
editable
=
False
)),
(
'key'
,
models
.
CharField
(
unique
=
True
,
max_length
=
63
,
db_index
=
True
)),
(
'values'
,
jsonfield
.
fields
.
JSONField
(
blank
=
True
)),
(
'site'
,
models
.
OneToOneField
(
related_name
=
'microsite_history'
,
to
=
'sites.Site'
)),
],
options
=
{
'verbose_name_plural'
:
'Microsite histories'
,
},
),
migrations
.
CreateModel
(
name
=
'MicrositeOrganizationMapping'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
verbose_name
=
'ID'
,
serialize
=
False
,
auto_created
=
True
,
primary_key
=
True
)),
(
'organization'
,
models
.
CharField
(
unique
=
True
,
max_length
=
63
,
db_index
=
True
)),
(
'microsite'
,
models
.
ForeignKey
(
to
=
'microsite_configuration.Microsite'
)),
],
),
migrations
.
CreateModel
(
name
=
'MicrositeTemplate'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
verbose_name
=
'ID'
,
serialize
=
False
,
auto_created
=
True
,
primary_key
=
True
)),
(
'template_uri'
,
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)),
(
'template'
,
models
.
TextField
()),
(
'microsite'
,
models
.
ForeignKey
(
to
=
'microsite_configuration.Microsite'
)),
],
),
migrations
.
AddField
(
model_name
=
'historicalmicrositetemplate'
,
name
=
'microsite'
,
field
=
models
.
ForeignKey
(
related_name
=
'+'
,
on_delete
=
django
.
db
.
models
.
deletion
.
DO_NOTHING
,
db_constraint
=
False
,
blank
=
True
,
to
=
'microsite_configuration.Microsite'
,
null
=
True
),
),
migrations
.
AddField
(
model_name
=
'historicalmicrositeorganizationmapping'
,
name
=
'microsite'
,
field
=
models
.
ForeignKey
(
related_name
=
'+'
,
on_delete
=
django
.
db
.
models
.
deletion
.
DO_NOTHING
,
db_constraint
=
False
,
blank
=
True
,
to
=
'microsite_configuration.Microsite'
,
null
=
True
),
),
migrations
.
AlterUniqueTogether
(
name
=
'micrositetemplate'
,
unique_together
=
set
([(
'microsite'
,
'template_uri'
)]),
),
]
common/djangoapps/microsite_configuration/migrations/__init__.py
0 → 100644
View file @
9bcb1166
common/djangoapps/microsite_configuration/models.py
0 → 100644
View file @
9bcb1166
"""
Model to store a microsite in the database.
The object is stored as a json representation of the python dict
that would have been used in the settings.
"""
import
collections
from
django.db
import
models
from
django.dispatch
import
receiver
from
django.db.models.signals
import
pre_save
,
pre_delete
from
django.db.models.base
import
ObjectDoesNotExist
from
django.contrib.sites.models
import
Site
from
jsonfield.fields
import
JSONField
from
model_utils.models
import
TimeStampedModel
from
simple_history.models
import
HistoricalRecords
class
Microsite
(
models
.
Model
):
"""
This is where the information about the microsite gets stored to the db.
To achieve the maximum flexibility, most of the fields are stored inside
a json field.
Notes:
- The key field was required for the dict definition at the settings, and it
is used in some of the microsite_configuration methods.
- The site field is django site.
- The values field must be validated on save to prevent the platform from crashing
badly in the case the string is not able to be loaded as json.
"""
site
=
models
.
OneToOneField
(
Site
,
related_name
=
'microsite'
)
key
=
models
.
CharField
(
max_length
=
63
,
db_index
=
True
,
unique
=
True
)
values
=
JSONField
(
null
=
False
,
blank
=
True
,
load_kwargs
=
{
'object_pairs_hook'
:
collections
.
OrderedDict
})
def
__unicode__
(
self
):
return
self
.
key
def
get_organizations
(
self
):
"""
Helper method to return a list of organizations associated with our particular Microsite
"""
return
MicrositeOrganizationMapping
.
get_organizations_for_microsite_by_pk
(
self
.
id
)
# pylint: disable=no-member
@classmethod
def
get_microsite_for_domain
(
cls
,
domain
):
"""
Returns the microsite associated with this domain. Note that we always convert to lowercase, or
None if no match
"""
# remove any port number from the hostname
domain
=
domain
.
split
(
':'
)[
0
]
microsites
=
cls
.
objects
.
filter
(
site__domain__iexact
=
domain
)
return
microsites
[
0
]
if
microsites
else
None
class
MicrositeHistory
(
TimeStampedModel
):
"""
This is an archive table for Microsites model, so that we can maintain a history of changes. Note that the
key field is no longer unique
"""
site
=
models
.
OneToOneField
(
Site
,
related_name
=
'microsite_history'
)
key
=
models
.
CharField
(
max_length
=
63
,
db_index
=
True
,
unique
=
True
)
values
=
JSONField
(
null
=
False
,
blank
=
True
,
load_kwargs
=
{
'object_pairs_hook'
:
collections
.
OrderedDict
})
def
__unicode__
(
self
):
return
self
.
key
class
Meta
(
object
):
""" Meta class for this Django model """
verbose_name_plural
=
"Microsite histories"
def
_make_archive_copy
(
instance
):
"""
Helper method to make a copy of a Microsite into the history table
"""
archive_object
=
MicrositeHistory
(
key
=
instance
.
key
,
site
=
instance
.
site
,
values
=
instance
.
values
,
)
archive_object
.
save
()
@receiver
(
pre_delete
,
sender
=
Microsite
)
def
on_microsite_deleted
(
sender
,
instance
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Archive the exam attempt when the item is about to be deleted
Make a clone and populate in the History table
"""
_make_archive_copy
(
instance
)
@receiver
(
pre_save
,
sender
=
Microsite
)
def
on_microsite_updated
(
sender
,
instance
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Archive the microsite on an update operation
"""
if
instance
.
id
:
# on an update case, get the original and archive it
original
=
Microsite
.
objects
.
get
(
id
=
instance
.
id
)
_make_archive_copy
(
original
)
class
MicrositeOrganizationMapping
(
models
.
Model
):
"""
Mapping of Organization to which Microsite it belongs
"""
organization
=
models
.
CharField
(
max_length
=
63
,
db_index
=
True
,
unique
=
True
)
microsite
=
models
.
ForeignKey
(
Microsite
,
db_index
=
True
)
# for archiving
history
=
HistoricalRecords
()
def
__unicode__
(
self
):
"""String conversion"""
return
u'{microsite_key}: {organization}'
.
format
(
microsite_key
=
self
.
microsite
.
key
,
organization
=
self
.
organization
)
@classmethod
def
get_organizations_for_microsite_by_pk
(
cls
,
microsite_pk
):
"""
Returns a list of organizations associated with the microsite key, returned as a set
"""
return
cls
.
objects
.
filter
(
microsite_id
=
microsite_pk
)
.
values_list
(
'organization'
,
flat
=
True
)
@classmethod
def
get_microsite_for_organization
(
cls
,
org
):
"""
Returns the microsite object for a given organization based on the table mapping, None if
no mapping exists
"""
try
:
item
=
cls
.
objects
.
select_related
(
'microsite'
)
.
get
(
organization
=
org
)
return
item
.
microsite
except
ObjectDoesNotExist
:
return
None
class
MicrositeTemplate
(
models
.
Model
):
"""
A HTML template that a microsite can use
"""
microsite
=
models
.
ForeignKey
(
Microsite
,
db_index
=
True
)
template_uri
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)
template
=
models
.
TextField
()
# for archiving
history
=
HistoricalRecords
()
def
__unicode__
(
self
):
"""String conversion"""
return
u'{microsite_key}: {template_uri}'
.
format
(
microsite_key
=
self
.
microsite
.
key
,
template_uri
=
self
.
template_uri
)
class
Meta
(
object
):
""" Meta class for this Django model """
unique_together
=
((
'microsite'
,
'template_uri'
),)
@classmethod
def
get_template_for_microsite
(
cls
,
domain
,
template_uri
):
"""
Returns the template object for the microsite, None if not found
"""
try
:
return
cls
.
objects
.
get
(
microsite__site__domain
=
domain
,
template_uri
=
template_uri
)
except
ObjectDoesNotExist
:
return
None
common/djangoapps/microsite_configuration/tests/backends/__init__.py
0 → 100644
View file @
9bcb1166
common/djangoapps/microsite_configuration/tests/backends/test_base.py
0 → 100644
View file @
9bcb1166
"""
Test Microsite base backends.
"""
from
django.test
import
TestCase
from
microsite_configuration.backends.base
import
(
AbstractBaseMicrositeBackend
,
)
class
NullBackend
(
AbstractBaseMicrositeBackend
):
"""
A class that does nothing but inherit from the base class.
We created this class to test methods of AbstractBaseMicrositeBackend class.
Since abstract class cannot be instantiated we created this wrapper class.
"""
def
set_config_by_domain
(
self
,
domain
):
"""
For a given request domain, find a match in our microsite configuration
and make it available to the complete django request process
"""
return
super
(
NullBackend
,
self
)
.
set_config_by_domain
(
domain
)
def
get_template_path
(
self
,
relative_path
,
**
kwargs
):
"""
Returns a path (string) to a Mako template, which can either be in
an override or will just return what is passed in which is expected to be a string
"""
return
super
(
NullBackend
,
self
)
.
get_template_path
(
relative_path
,
**
kwargs
)
def
get_value
(
self
,
val_name
,
default
=
None
,
**
kwargs
):
"""
Returns a value associated with the request's microsite, if present
"""
return
super
(
NullBackend
,
self
)
.
get_value
(
val_name
,
default
,
**
kwargs
)
def
get_dict
(
self
,
dict_name
,
default
=
None
,
**
kwargs
):
"""
Returns a dictionary product of merging the request's microsite and
the default value.
This can be used, for example, to return a merged dictonary from the
settings.FEATURES dict, including values defined at the microsite
"""
return
super
(
NullBackend
,
self
)
.
get_dict
(
dict_name
,
default
,
**
kwargs
)
def
is_request_in_microsite
(
self
):
"""
This will return True/False if the current request is a request within a microsite
"""
return
super
(
NullBackend
,
self
)
.
is_request_in_microsite
()
def
has_override_value
(
self
,
val_name
):
"""
Returns True/False whether a Microsite has a definition for the
specified named value
"""
return
super
(
NullBackend
,
self
)
.
has_override_value
(
val_name
)
def
get_all_config
(
self
):
"""
This returns a set of orgs that are considered within all microsites.
This can be used, for example, to do filtering
"""
return
super
(
NullBackend
,
self
)
.
get_all_config
()
def
get_value_for_org
(
self
,
org
,
val_name
,
default
=
None
):
"""
This returns a configuration value for a microsite which has an org_filter that matches
what is passed in
"""
return
super
(
NullBackend
,
self
)
.
get_value_for_org
(
org
,
val_name
,
default
)
def
get_all_orgs
(
self
):
"""
This returns a set of orgs that are considered within a microsite. This can be used,
for example, to do filtering
"""
return
super
(
NullBackend
,
self
)
.
get_all_orgs
()
def
clear
(
self
):
"""
Clears out any microsite configuration from the current request/thread
"""
return
super
(
NullBackend
,
self
)
.
clear
()
class
AbstractBaseMicrositeBackendTests
(
TestCase
):
"""
Go through and test the base abstract class
"""
def
test_cant_create_instance
(
self
):
"""
We shouldn't be able to create an instance of the base abstract class
"""
with
self
.
assertRaises
(
TypeError
):
AbstractBaseMicrositeBackend
()
# pylint: disable=abstract-class-instantiated
def
test_not_yet_implemented
(
self
):
"""
Make sure all base methods raise a NotImplementedError exception
"""
backend
=
NullBackend
()
with
self
.
assertRaises
(
NotImplementedError
):
backend
.
set_config_by_domain
(
None
)
with
self
.
assertRaises
(
NotImplementedError
):
backend
.
get_value
(
None
,
None
)
with
self
.
assertRaises
(
NotImplementedError
):
backend
.
get_dict
(
None
,
None
)
with
self
.
assertRaises
(
NotImplementedError
):
backend
.
is_request_in_microsite
()
with
self
.
assertRaises
(
NotImplementedError
):
backend
.
has_override_value
(
None
)
with
self
.
assertRaises
(
NotImplementedError
):
backend
.
get_all_config
()
with
self
.
assertRaises
(
NotImplementedError
):
backend
.
clear
()
with
self
.
assertRaises
(
NotImplementedError
):
backend
.
get_value_for_org
(
None
,
None
,
None
)
with
self
.
assertRaises
(
NotImplementedError
):
backend
.
get_all_orgs
()
common/djangoapps/microsite_configuration/tests/backends/test_database.py
0 → 100644
View file @
9bcb1166
"""
Test Microsite database backends.
"""
import
logging
from
mock
import
patch
from
django.conf
import
settings
from
microsite_configuration.backends.base
import
(
BaseMicrositeBackend
,
BaseMicrositeTemplateBackend
,
)
from
microsite_configuration
import
microsite
from
microsite_configuration.models
import
(
Microsite
,
MicrositeHistory
,
MicrositeTemplate
,
)
from
microsite_configuration.tests.tests
import
(
DatabaseMicrositeTestCase
,
)
from
microsite_configuration.tests.factories
import
(
SiteFactory
,
MicrositeFactory
,
MicrositeTemplateFactory
,
)
log
=
logging
.
getLogger
(
__name__
)
@patch
(
'microsite_configuration.microsite.BACKEND'
,
microsite
.
get_backend
(
'microsite_configuration.backends.database.DatabaseMicrositeBackend'
,
BaseMicrositeBackend
)
)
class
DatabaseMicrositeBackendTests
(
DatabaseMicrositeTestCase
):
"""
Go through and test the DatabaseMicrositeBackend class
"""
def
setUp
(
self
):
super
(
DatabaseMicrositeBackendTests
,
self
)
.
setUp
()
def
tearDown
(
self
):
super
(
DatabaseMicrositeBackendTests
,
self
)
.
tearDown
()
microsite
.
clear
()
def
test_get_value
(
self
):
"""
Tests microsite.get_value works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite
.
site
.
domain
)
self
.
assertEqual
(
microsite
.
get_value
(
'email_from_address'
),
self
.
microsite
.
values
[
'email_from_address'
])
def
test_is_request_in_microsite
(
self
):
"""
Tests microsite.is_request_in_microsite works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite
.
site
.
domain
)
self
.
assertTrue
(
microsite
.
is_request_in_microsite
())
def
test_get_dict
(
self
):
"""
Tests microsite.get_dict works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite
.
site
.
domain
)
self
.
assertEqual
(
microsite
.
get_dict
(
'nested_dict'
),
self
.
microsite
.
values
[
'nested_dict'
])
def
test_has_override_value
(
self
):
"""
Tests microsite.has_override_value works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite
.
site
.
domain
)
self
.
assertTrue
(
microsite
.
has_override_value
(
'platform_name'
))
def
test_get_value_for_org
(
self
):
"""
Tests microsite.get_value_for_org works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite
.
site
.
domain
)
self
.
assertEqual
(
microsite
.
get_value_for_org
(
self
.
microsite
.
get_organizations
()[
0
],
'platform_name'
),
self
.
microsite
.
values
[
'platform_name'
]
)
def
test_get_all_orgs
(
self
):
"""
Tests microsite.get_all_orgs works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite
.
site
.
domain
)
self
.
assertEqual
(
microsite
.
get_all_orgs
(),
set
(
self
.
microsite
.
get_organizations
())
)
def
test_clear
(
self
):
"""
Tests microsite.clear works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite
.
site
.
domain
)
self
.
assertEqual
(
microsite
.
get_value
(
'platform_name'
),
self
.
microsite
.
values
[
'platform_name'
]
)
microsite
.
clear
()
self
.
assertIsNone
(
microsite
.
get_value
(
'platform_name'
))
def
test_enable_microsites_pre_startup
(
self
):
"""
Tests microsite.test_enable_microsites_pre_startup works as expected.
"""
# remove microsite root directory paths first
settings
.
DEFAULT_TEMPLATE_ENGINE
[
'DIRS'
]
=
[
path
for
path
in
settings
.
DEFAULT_TEMPLATE_ENGINE
[
'DIRS'
]
if
path
!=
settings
.
MICROSITE_ROOT_DIR
]
with
patch
.
dict
(
'django.conf.settings.FEATURES'
,
{
'USE_MICROSITES'
:
False
}):
microsite
.
enable_microsites_pre_startup
(
log
)
self
.
assertNotIn
(
settings
.
MICROSITE_ROOT_DIR
,
settings
.
DEFAULT_TEMPLATE_ENGINE
[
'DIRS'
])
with
patch
.
dict
(
'django.conf.settings.FEATURES'
,
{
'USE_MICROSITES'
:
True
}):
microsite
.
enable_microsites_pre_startup
(
log
)
self
.
assertIn
(
settings
.
MICROSITE_ROOT_DIR
,
settings
.
DEFAULT_TEMPLATE_ENGINE
[
'DIRS'
])
@patch
(
'edxmako.paths.add_lookup'
)
def
test_enable_microsites
(
self
,
add_lookup
):
"""
Tests microsite.enable_microsites works as expected.
"""
# remove microsite root directory paths first
settings
.
STATICFILES_DIRS
=
[
path
for
path
in
settings
.
STATICFILES_DIRS
if
path
!=
settings
.
MICROSITE_ROOT_DIR
]
with
patch
.
dict
(
'django.conf.settings.FEATURES'
,
{
'USE_MICROSITES'
:
False
}):
microsite
.
enable_microsites
(
log
)
self
.
assertNotIn
(
settings
.
MICROSITE_ROOT_DIR
,
settings
.
STATICFILES_DIRS
)
add_lookup
.
assert_not_called
()
with
patch
.
dict
(
'django.conf.settings.FEATURES'
,
{
'USE_MICROSITES'
:
True
}):
microsite
.
enable_microsites
(
log
)
self
.
assertIn
(
settings
.
MICROSITE_ROOT_DIR
,
settings
.
STATICFILES_DIRS
)
add_lookup
.
assert_called_once_with
(
'main'
,
settings
.
MICROSITE_ROOT_DIR
)
def
test_get_all_configs
(
self
):
"""
Tests microsite.get_all_config works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite
.
site
.
domain
)
configs
=
microsite
.
get_all_config
()
self
.
assertEqual
(
len
(
configs
.
keys
()),
1
)
self
.
assertEqual
(
configs
[
self
.
microsite
.
key
],
self
.
microsite
.
values
)
def
test_set_config_by_domain
(
self
):
"""
Tests microsite.set_config_by_domain works as expected.
"""
microsite
.
clear
()
# if microsite config does not exist
microsite
.
set_by_domain
(
'unknown'
)
self
.
assertIsNone
(
microsite
.
get_value
(
'platform_name'
))
# if no microsite exists
Microsite
.
objects
.
all
()
.
delete
()
microsite
.
clear
()
microsite
.
set_by_domain
(
'unknown'
)
self
.
assertIsNone
(
microsite
.
get_value
(
'platform_name'
))
# if microsite site has no organization it should raise exception
new_microsite
=
MicrositeFactory
.
create
(
key
=
"test_microsite2"
)
new_microsite
.
site
=
SiteFactory
.
create
(
domain
=
'test.microsite2.com'
)
# This would update microsite so we test MicrositeHistory has old microsite
new_microsite
.
save
()
self
.
assertEqual
(
MicrositeHistory
.
objects
.
all
()
.
count
(),
2
)
with
self
.
assertRaises
(
Exception
):
microsite
.
set_by_domain
(
'test.microsite2.com'
)
@patch
(
'microsite_configuration.microsite.TEMPLATES_BACKEND'
,
microsite
.
get_backend
(
'microsite_configuration.backends.database.DatabaseMicrositeTemplateBackend'
,
BaseMicrositeTemplateBackend
)
)
class
DatabaseMicrositeTemplateBackendTests
(
DatabaseMicrositeTestCase
):
"""
Go through and test the DatabaseMicrositeTemplateBackend class
"""
def
setUp
(
self
):
super
(
DatabaseMicrositeTemplateBackendTests
,
self
)
.
setUp
()
MicrositeTemplateFactory
.
create
(
microsite
=
self
.
microsite
,
template_uri
=
'about.html'
,
template
=
"""
<html>
<body>
About this microsite.
</body>
</html>
"""
,
)
def
tearDown
(
self
):
super
(
DatabaseMicrositeTemplateBackendTests
,
self
)
.
tearDown
()
microsite
.
clear
()
def
test_microsite_get_template_when_no_template_exists
(
self
):
"""
Test microsite.get_template return None if there is not template in DB.
"""
MicrositeTemplate
.
objects
.
all
()
.
delete
()
microsite
.
set_by_domain
(
self
.
microsite
.
site
.
domain
)
template
=
microsite
.
get_template
(
'about.html'
)
self
.
assertIsNone
(
template
)
def
test_microsite_get_template
(
self
):
"""
Test microsite.get_template return appropriate template.
"""
microsite
.
set_by_domain
(
self
.
microsite
.
site
.
domain
)
template
=
microsite
.
get_template
(
'about.html'
)
self
.
assertIn
(
'About this microsite'
,
template
.
render
())
common/djangoapps/microsite_configuration/tests/backends/test_filebased.py
0 → 100644
View file @
9bcb1166
"""
Test Microsite filebased backends.
"""
from
mock
import
patch
from
django.test
import
TestCase
from
microsite_configuration.backends.base
import
(
BaseMicrositeBackend
,
)
from
microsite_configuration
import
microsite
@patch
(
'microsite_configuration.microsite.BACKEND'
,
microsite
.
get_backend
(
'microsite_configuration.backends.filebased.FilebasedMicrositeBackend'
,
BaseMicrositeBackend
)
)
class
FilebasedMicrositeBackendTests
(
TestCase
):
"""
Go through and test the FilebasedMicrositeBackend class
"""
def
setUp
(
self
):
super
(
FilebasedMicrositeBackendTests
,
self
)
.
setUp
()
self
.
microsite_subdomain
=
'testmicrosite'
def
tearDown
(
self
):
super
(
FilebasedMicrositeBackendTests
,
self
)
.
tearDown
()
microsite
.
clear
()
def
test_get_value
(
self
):
"""
Tests microsite.get_value works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite_subdomain
)
self
.
assertEqual
(
microsite
.
get_value
(
'platform_name'
),
'Test Microsite'
)
def
test_is_request_in_microsite
(
self
):
"""
Tests microsite.is_request_in_microsite works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite_subdomain
)
self
.
assertTrue
(
microsite
.
is_request_in_microsite
())
def
test_has_override_value
(
self
):
"""
Tests microsite.has_override_value works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite_subdomain
)
self
.
assertTrue
(
microsite
.
has_override_value
(
'platform_name'
))
def
test_get_value_for_org
(
self
):
"""
Tests microsite.get_value_for_org works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite_subdomain
)
self
.
assertEqual
(
microsite
.
get_value_for_org
(
'TestMicrositeX'
,
'platform_name'
),
'Test Microsite'
)
# if no config is set
microsite
.
clear
()
with
patch
(
'django.conf.settings.MICROSITE_CONFIGURATION'
,
False
):
self
.
assertEqual
(
microsite
.
get_value_for_org
(
'TestMicrositeX'
,
'platform_name'
,
'Default Value'
),
'Default Value'
)
def
test_get_all_orgs
(
self
):
"""
Tests microsite.get_all_orgs works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite_subdomain
)
self
.
assertEqual
(
microsite
.
get_all_orgs
(),
set
([
'TestMicrositeX'
,
'LogistrationX'
])
)
# if no config is set
microsite
.
clear
()
with
patch
(
'django.conf.settings.MICROSITE_CONFIGURATION'
,
False
):
self
.
assertEqual
(
microsite
.
get_all_orgs
(),
set
()
)
def
test_clear
(
self
):
"""
Tests microsite.clear works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite_subdomain
)
self
.
assertEqual
(
microsite
.
get_value
(
'platform_name'
),
'Test Microsite'
)
microsite
.
clear
()
self
.
assertIsNone
(
microsite
.
get_value
(
'platform_name'
))
def
test_get_all_configs
(
self
):
"""
Tests microsite.get_all_config works as expected.
"""
microsite
.
set_by_domain
(
self
.
microsite_subdomain
)
configs
=
microsite
.
get_all_config
()
self
.
assertEqual
(
len
(
configs
.
keys
()),
3
)
def
test_set_config_by_domain
(
self
):
"""
Tests microsite.set_config_by_domain works as expected.
"""
microsite
.
clear
()
# if microsite config does not exist default config should be used
microsite
.
set_by_domain
(
'unknown'
)
self
.
assertEqual
(
microsite
.
get_value
(
'university'
),
'default_university'
)
common/djangoapps/microsite_configuration/tests/factories.py
0 → 100644
View file @
9bcb1166
"""
Factories module to hold microsite factories
"""
import
factory
from
factory.django
import
DjangoModelFactory
from
django.contrib.sites.models
import
Site
from
microsite_configuration.models
import
(
Microsite
,
MicrositeOrganizationMapping
,
MicrositeTemplate
,
)
class
SiteFactory
(
DjangoModelFactory
):
"""
Factory for django.contrib.sites.models.Site
"""
class
Meta
(
object
):
model
=
Site
name
=
"test microsite"
domain
=
"testmicrosite.testserver"
class
MicrositeFactory
(
DjangoModelFactory
):
"""
Factory for Microsite
"""
class
Meta
(
object
):
model
=
Microsite
key
=
"test_microsite"
site
=
factory
.
SubFactory
(
SiteFactory
)
values
=
{
"domain_prefix"
:
"testmicrosite"
,
"university"
:
"test_microsite"
,
"platform_name"
:
"Test Microsite DB"
,
"logo_image_url"
:
"test_microsite/images/header-logo.png"
,
"email_from_address"
:
"test_microsite_db@edx.org"
,
"payment_support_email"
:
"test_microsit_dbe@edx.org"
,
"ENABLE_MKTG_SITE"
:
False
,
"SITE_NAME"
:
"test_microsite.localhost"
,
"course_org_filter"
:
"TestMicrositeX"
,
"course_about_show_social_links"
:
False
,
"css_overrides_file"
:
"test_microsite/css/test_microsite.css"
,
"show_partners"
:
False
,
"show_homepage_promo_video"
:
False
,
"course_index_overlay_text"
:
"This is a Test Microsite Overlay Text."
,
"course_index_overlay_logo_file"
:
"test_microsite/images/header-logo.png"
,
"homepage_overlay_html"
:
"<h1>This is a Test Microsite Overlay HTML</h1>"
,
"ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER"
:
False
,
"COURSE_CATALOG_VISIBILITY_PERMISSION"
:
"see_in_catalog"
,
"COURSE_ABOUT_VISIBILITY_PERMISSION"
:
"see_about_page"
,
"ENABLE_SHOPPING_CART"
:
True
,
"ENABLE_PAID_COURSE_REGISTRATION"
:
True
,
"SESSION_COOKIE_DOMAIN"
:
"test_microsite.localhost"
,
"nested_dict"
:
{
"key 1"
:
"value 1"
,
"key 2"
:
"value 2"
,
}
}
class
MicrositeOrganizationMappingFactory
(
DjangoModelFactory
):
"""
Factory for MicrositeOrganizationMapping
"""
class
Meta
(
object
):
model
=
MicrositeOrganizationMapping
class
MicrositeTemplateFactory
(
DjangoModelFactory
):
"""
Factory for MicrositeTemplate
"""
class
Meta
(
object
):
model
=
MicrositeTemplate
common/djangoapps/microsite_configuration/tests/test_logic.py
View file @
9bcb1166
...
...
@@ -2,25 +2,45 @@
Some additional unit tests for Microsite logic. The LMS covers some of the Microsite testing, this adds
some additional coverage
"""
import
django.test
import
ddt
from
mock
import
patch
from
microsite_configuration.microsite
import
get_value_for_org
from
microsite_configuration.microsite
import
(
get_value_for_org
,
get_backend
,
)
from
microsite_configuration.backends.base
import
BaseMicrositeBackend
from
microsite_configuration.tests.tests
import
(
DatabaseMicrositeTestCase
,
MICROSITE_BACKENDS
,
)
class
TestMicrosites
(
django
.
test
.
TestCase
):
@ddt.ddt
class
TestMicrosites
(
DatabaseMicrositeTestCase
):
"""
Run through some Microsite logic
"""
def
test_get_value_for_org
(
self
):
def
setUp
(
self
):
super
(
TestMicrosites
,
self
)
.
setUp
()
@ddt.data
(
*
MICROSITE_BACKENDS
)
def
test_get_value_for_org_when_microsite_has_no_org
(
self
,
site_backend
):
"""
Make sure
we can do lookups on Microsite configuration based on ORG fields
Make sure
default value is returned if there's no Microsite ORG match
"""
# first make sure default value is returned if there's no Microsite ORG match
with
patch
(
'microsite_configuration.microsite.BACKEND'
,
get_backend
(
site_backend
,
BaseMicrositeBackend
)):
value
=
get_value_for_org
(
"BogusX"
,
"university"
,
"default_value"
)
self
.
assertEquals
(
value
,
"default_value"
)
# now test when we call in a value Microsite ORG, note this is defined in test.py configuration
@ddt.data
(
*
MICROSITE_BACKENDS
)
def
test_get_value_for_org
(
self
,
site_backend
):
"""
Make sure get_value_for_org return value of org if it present.
"""
with
patch
(
'microsite_configuration.microsite.BACKEND'
,
get_backend
(
site_backend
,
BaseMicrositeBackend
)):
value
=
get_value_for_org
(
"TestMicrositeX"
,
"university"
,
"default_value"
)
self
.
assertEquals
(
value
,
"test_microsite"
)
common/djangoapps/microsite_configuration/tests/test_microsites.py
View file @
9bcb1166
...
...
@@ -4,38 +4,73 @@ Tests microsite_configuration templatetags and helper functions.
"""
from
django.test
import
TestCase
from
django.conf
import
settings
from
microsite_configuration.templatetags
import
microsite
from
microsite_configuration.templatetags
import
microsite
as
microsite_tags
from
microsite_configuration
import
microsite
from
microsite_configuration.backends.base
import
BaseMicrositeBackend
from
microsite_configuration.backends.database
import
DatabaseMicrositeBackend
class
Micro
S
iteTests
(
TestCase
):
class
Micro
s
iteTests
(
TestCase
):
"""
Make sure some of the helper functions work
"""
def
test_breadcrumbs
(
self
):
crumbs
=
[
'my'
,
'less specific'
,
'Page'
]
expected
=
u'my | less specific | Page | edX'
title
=
microsite
.
page_title_breadcrumbs
(
*
crumbs
)
title
=
microsite
_tags
.
page_title_breadcrumbs
(
*
crumbs
)
self
.
assertEqual
(
expected
,
title
)
def
test_unicode_title
(
self
):
crumbs
=
[
u'øo'
,
u'π tastes gréât'
,
u'驴'
]
expected
=
u'øo | π tastes gréât | 驴 | edX'
title
=
microsite
.
page_title_breadcrumbs
(
*
crumbs
)
title
=
microsite
_tags
.
page_title_breadcrumbs
(
*
crumbs
)
self
.
assertEqual
(
expected
,
title
)
def
test_platform_name
(
self
):
pname
=
microsite
.
platform_name
()
pname
=
microsite
_tags
.
platform_name
()
self
.
assertEqual
(
pname
,
settings
.
PLATFORM_NAME
)
def
test_breadcrumb_tag
(
self
):
crumbs
=
[
'my'
,
'less specific'
,
'Page'
]
expected
=
u'my | less specific | Page | edX'
title
=
microsite
.
page_title_breadcrumbs_tag
(
None
,
*
crumbs
)
title
=
microsite
_tags
.
page_title_breadcrumbs_tag
(
None
,
*
crumbs
)
self
.
assertEqual
(
expected
,
title
)
def
test_microsite_template_path
(
self
):
"""
When an unexistent path is passed to the filter, it should return the same path
"""
path
=
microsite
.
microsite_template_path
(
'footer.html'
)
path
=
microsite
_tags
.
microsite_template_path
(
'footer.html'
)
self
.
assertEqual
(
"footer.html"
,
path
)
def
test_get_backend_raise_error_for_invalid_class
(
self
):
"""
Test get_backend returns None for invalid paths
and raises TypeError when invalid class or class name is a method.
"""
# invalid backend path
self
.
assertEqual
(
microsite
.
get_backend
(
None
,
BaseMicrositeBackend
),
None
)
# invalid class or class name is a method
with
self
.
assertRaises
(
TypeError
):
microsite
.
get_backend
(
'microsite_configuration.microsite.get_backend'
,
BaseMicrositeBackend
)
def
test_get_backend_raise_error_when_module_has_no_class
(
self
):
"""
Test get_backend raises ValueError when module does not have a class.
"""
# module does not have a class
with
self
.
assertRaises
(
ValueError
):
microsite
.
get_backend
(
'microsite_configuration.microsite.invalid_method'
,
BaseMicrositeBackend
)
def
test_get_backend_for_valid_class
(
self
):
"""
Test get_backend loads class if class exists.
"""
# load a valid class
self
.
assertIsInstance
(
microsite
.
get_backend
(
'microsite_configuration.backends.database.DatabaseMicrositeBackend'
,
BaseMicrositeBackend
),
DatabaseMicrositeBackend
)
common/djangoapps/microsite_configuration/tests/test_middleware.py
View file @
9bcb1166
...
...
@@ -2,28 +2,39 @@
"""
Test Microsite middleware.
"""
import
ddt
import
unittest
from
mock
import
patch
from
django.test
import
TestCase
from
django.conf
import
settings
from
django.test.client
import
Client
from
django.test.utils
import
override_settings
import
unittest
from
student.tests.factories
import
UserFactory
from
microsite_configuration.microsite
import
(
get_backend
,
)
from
microsite_configuration.backends.base
import
BaseMicrositeBackend
from
microsite_configuration.tests.tests
import
(
DatabaseMicrositeTestCase
,
side_effect_for_get_value
,
MICROSITE_BACKENDS
,
)
# NOTE: We set SESSION_SAVE_EVERY_REQUEST to True in order to make sure
# Sessions are always started on every request
# pylint: disable=no-member, protected-access
@ddt.ddt
@override_settings
(
SESSION_SAVE_EVERY_REQUEST
=
True
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
class
Micro
SiteSessionCookieTests
(
TestCase
):
class
Micro
siteSessionCookieTests
(
DatabaseMicrosite
TestCase
):
"""
Tests regarding the session cookie management in the middlware for Micro
S
ites
Tests regarding the session cookie management in the middlware for Micro
s
ites
"""
def
setUp
(
self
):
super
(
Micro
S
iteSessionCookieTests
,
self
)
.
setUp
()
super
(
Micro
s
iteSessionCookieTests
,
self
)
.
setUp
()
# Create a test client, and log it in so that it will save some session
# data.
self
.
user
=
UserFactory
.
create
()
...
...
@@ -32,29 +43,39 @@ class MicroSiteSessionCookieTests(TestCase):
self
.
client
=
Client
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
"password"
)
def
test_session_cookie_domain_no_microsite
(
self
):
@ddt.data
(
*
MICROSITE_BACKENDS
)
def
test_session_cookie_domain_no_microsite
(
self
,
site_backend
):
"""
Tests that non-microsite behaves according to default behavior
"""
with
patch
(
'microsite_configuration.microsite.BACKEND'
,
get_backend
(
site_backend
,
BaseMicrositeBackend
)):
response
=
self
.
client
.
get
(
'/'
)
self
.
assertNotIn
(
'test_microsite.localhost'
,
str
(
response
.
cookies
[
'sessionid'
]))
# pylint: disable=no-member
self
.
assertNotIn
(
'Domain'
,
str
(
response
.
cookies
[
'sessionid'
]))
# pylint: disable=no-member
self
.
assertNotIn
(
'test_microsite.localhost'
,
str
(
response
.
cookies
[
'sessionid'
]))
self
.
assertNotIn
(
'Domain'
,
str
(
response
.
cookies
[
'sessionid'
]))
def
test_session_cookie_domain
(
self
):
@ddt.data
(
*
MICROSITE_BACKENDS
)
def
test_session_cookie_domain
(
self
,
site_backend
):
"""
Makes sure that the cookie being set in a Microsite
is the one specially overridden in configuration,
in this case in test.py
is the one specially overridden in configuration
"""
with
patch
(
'microsite_configuration.microsite.BACKEND'
,
get_backend
(
site_backend
,
BaseMicrositeBackend
)):
response
=
self
.
client
.
get
(
'/'
,
HTTP_HOST
=
settings
.
MICROSITE_TEST_HOSTNAME
)
self
.
assertIn
(
'test_microsite.localhost'
,
str
(
response
.
cookies
[
'sessionid'
]))
# pylint: disable=no-member
self
.
assertIn
(
'test_microsite.localhost'
,
str
(
response
.
cookies
[
'sessionid'
]))
@
patch.dict
(
"django.conf.settings.MICROSITE_CONFIGURATION"
,
{
'test_microsite'
:
{
'SESSION_COOKIE_DOMAIN'
:
None
}}
)
def
test_microsite_none_cookie_domain
(
self
):
@
ddt.data
(
*
MICROSITE_BACKENDS
)
def
test_microsite_none_cookie_domain
(
self
,
site_backend
):
"""
Tests to make sure that a Microsite that specifies None for 'SESSION_COOKIE_DOMAIN' does not
set a domain on the session cookie
"""
with
patch
(
'microsite_configuration.microsite.get_value'
)
as
mock_get_value
:
mock_get_value
.
side_effect
=
side_effect_for_get_value
(
'SESSION_COOKIE_DOMAIN'
,
None
)
with
patch
(
'microsite_configuration.microsite.BACKEND'
,
get_backend
(
site_backend
,
BaseMicrositeBackend
)):
response
=
self
.
client
.
get
(
'/'
,
HTTP_HOST
=
settings
.
MICROSITE_TEST_HOSTNAME
)
self
.
assertNotIn
(
'test_microsite.localhost'
,
str
(
response
.
cookies
[
'sessionid'
]))
# pylint: disable=no-member
self
.
assertNotIn
(
'Domain'
,
str
(
response
.
cookies
[
'sessionid'
]))
# pylint: disable=no-member
self
.
assertNotIn
(
'test_microsite.localhost'
,
str
(
response
.
cookies
[
'sessionid'
]))
self
.
assertNotIn
(
'Domain'
,
str
(
response
.
cookies
[
'sessionid'
]))
common/djangoapps/microsite_configuration/tests/tests.py
0 → 100644
View file @
9bcb1166
"""
Holds base classes for microsite tests
"""
from
mock
import
DEFAULT
from
django.test
import
TestCase
from
microsite_configuration.tests.factories
import
(
MicrositeFactory
,
MicrositeOrganizationMappingFactory
,
)
MICROSITE_BACKENDS
=
(
'microsite_configuration.backends.filebased.FilebasedMicrositeBackend'
,
'microsite_configuration.backends.database.DatabaseMicrositeBackend'
,
)
class
DatabaseMicrositeTestCase
(
TestCase
):
"""
Base class for microsite related tests.
"""
def
setUp
(
self
):
super
(
DatabaseMicrositeTestCase
,
self
)
.
setUp
()
self
.
microsite
=
MicrositeFactory
.
create
()
MicrositeOrganizationMappingFactory
.
create
(
microsite
=
self
.
microsite
,
organization
=
'TestMicrositeX'
)
def
side_effect_for_get_value
(
value
,
return_value
):
"""
returns a side_effect with given return value for a given value
"""
def
side_effect
(
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
"""
A side effect for tests which returns a value based
on a given argument otherwise return actual function.
"""
if
args
[
0
]
==
value
:
return
return_value
else
:
return
DEFAULT
return
side_effect
common/djangoapps/util/url.py
View file @
9bcb1166
...
...
@@ -20,3 +20,10 @@ def reload_django_url_config():
reloaded
=
import_module
(
urlconf
)
reloaded_urls
=
reloaded
.
urlpatterns
set_urlconf
(
tuple
(
reloaded_urls
))
def
strip_port_from_host
(
host
):
"""
Strips port number from host
"""
return
host
.
split
(
':'
)[
0
]
lms/envs/aws.py
View file @
9bcb1166
...
...
@@ -537,9 +537,6 @@ ORA2_FILE_PREFIX = ENV_TOKENS.get("ORA2_FILE_PREFIX", ORA2_FILE_PREFIX)
MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED
=
ENV_TOKENS
.
get
(
"MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED"
,
5
)
MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS
=
ENV_TOKENS
.
get
(
"MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS"
,
15
*
60
)
MICROSITE_CONFIGURATION
=
ENV_TOKENS
.
get
(
'MICROSITE_CONFIGURATION'
,
{})
MICROSITE_ROOT_DIR
=
path
(
ENV_TOKENS
.
get
(
'MICROSITE_ROOT_DIR'
,
''
))
#### PASSWORD POLICY SETTINGS #####
PASSWORD_MIN_LENGTH
=
ENV_TOKENS
.
get
(
"PASSWORD_MIN_LENGTH"
)
PASSWORD_MAX_LENGTH
=
ENV_TOKENS
.
get
(
"PASSWORD_MAX_LENGTH"
)
...
...
@@ -735,5 +732,17 @@ JWT_EXPIRATION = ENV_TOKENS.get('JWT_EXPIRATION', JWT_EXPIRATION)
PROCTORING_BACKEND_PROVIDER
=
AUTH_TOKENS
.
get
(
"PROCTORING_BACKEND_PROVIDER"
,
PROCTORING_BACKEND_PROVIDER
)
PROCTORING_SETTINGS
=
ENV_TOKENS
.
get
(
"PROCTORING_SETTINGS"
,
PROCTORING_SETTINGS
)
################# MICROSITE ####################
MICROSITE_CONFIGURATION
=
ENV_TOKENS
.
get
(
'MICROSITE_CONFIGURATION'
,
{})
MICROSITE_ROOT_DIR
=
path
(
ENV_TOKENS
.
get
(
'MICROSITE_ROOT_DIR'
,
''
))
# this setting specify which backend to be used when pulling microsite specific configuration
MICROSITE_BACKEND
=
ENV_TOKENS
.
get
(
"MICROSITE_BACKEND"
,
MICROSITE_BACKEND
)
# this setting specify which backend to be used when loading microsite specific templates
MICROSITE_TEMPLATE_BACKEND
=
ENV_TOKENS
.
get
(
"MICROSITE_TEMPLATE_BACKEND"
,
MICROSITE_TEMPLATE_BACKEND
)
# TTL for microsite database template cache
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
=
ENV_TOKENS
.
get
(
"MICROSITE_DATABASE_TEMPLATE_CACHE_TTL"
,
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
)
# Course Content Bookmarks Settings
MAX_BOOKMARKS_PER_COURSE
=
ENV_TOKENS
.
get
(
'MAX_BOOKMARKS_PER_COURSE'
,
MAX_BOOKMARKS_PER_COURSE
)
lms/envs/common.py
View file @
9bcb1166
...
...
@@ -2641,6 +2641,22 @@ JWT_ISSUER = None
NOTIFICATION_EMAIL_CSS
=
"templates/credit_notifications/credit_notification.css"
NOTIFICATION_EMAIL_EDX_LOGO
=
"templates/credit_notifications/edx-logo-header.png"
################################ Settings for Microsites ################################
### Select an implementation for the microsite backend
# for MICROSITE_BACKEND possible choices are
# 1. microsite_configuration.backends.filebased.FilebasedMicrositeBackend
# 2. microsite_configuration.backends.database.DatabaseMicrositeBackend
MICROSITE_BACKEND
=
'microsite_configuration.backends.filebased.FilebasedMicrositeBackend'
# for MICROSITE_TEMPLATE_BACKEND possible choices are
# 1. microsite_configuration.backends.filebased.FilebasedMicrositeTemplateBackend
# 2. microsite_configuration.backends.database.DatabaseMicrositeTemplateBackend
MICROSITE_TEMPLATE_BACKEND
=
'microsite_configuration.backends.filebased.FilebasedMicrositeTemplateBackend'
# TTL for microsite database template cache
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
=
5
*
60
#### PROCTORING CONFIGURATION DEFAULTS
PROCTORING_BACKEND_PROVIDER
=
{
...
...
lms/envs/test.py
View file @
9bcb1166
...
...
@@ -423,6 +423,8 @@ PLATFORM_NAME = "edX"
SITE_NAME
=
"edx.org"
# set up some testing for microsites
FEATURES
[
'USE_MICROSITES'
]
=
True
MICROSITE_ROOT_DIR
=
COMMON_ROOT
/
'test'
/
'test_microsites'
MICROSITE_CONFIGURATION
=
{
"test_microsite"
:
{
"domain_prefix"
:
"testmicrosite"
,
...
...
@@ -483,15 +485,14 @@ MICROSITE_CONFIGURATION = {
"domain_prefix"
:
"www"
,
}
}
MICROSITE_ROOT_DIR
=
COMMON_ROOT
/
'test'
/
'test_microsites'
MICROSITE_TEST_HOSTNAME
=
'testmicrosite.testserver'
MICROSITE_LOGISTRATION_HOSTNAME
=
'logistration.testserver'
FEATURES
[
'USE_MICROSITES'
]
=
True
# add extra template directory for test-only templates
MAKO_TEMPLATES
[
'main'
]
.
extend
([
COMMON_ROOT
/
'test'
/
'templates'
COMMON_ROOT
/
'test'
/
'templates'
,
COMMON_ROOT
/
'test'
/
'test_microsites'
])
...
...
lms/startup.py
View file @
9bcb1166
...
...
@@ -18,6 +18,8 @@ from monkey_patch import third_party_auth
import
xmodule.x_module
import
lms_xblock.runtime
from
microsite_configuration
import
microsite
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -31,8 +33,10 @@ def run():
if
settings
.
FEATURES
.
get
(
'ENABLE_THIRD_PARTY_AUTH'
,
False
):
enable_third_party_auth
()
if
settings
.
FEATURES
.
get
(
'USE_MICROSITES'
,
False
):
enable_microsites_pre_startup
()
# We currently use 2 template rendering engines, mako and django_templates,
# and one of them (django templates), requires the directories be added
# before the django.setup().
microsite
.
enable_microsites_pre_startup
(
log
)
django
.
setup
()
...
...
@@ -40,12 +44,12 @@ def run():
add_mimetypes
()
# Mako requires the directories to be added after the django setup.
microsite
.
enable_microsites
(
log
)
if
settings
.
FEATURES
.
get
(
'USE_CUSTOM_THEME'
,
False
):
enable_stanford_theme
()
if
settings
.
FEATURES
.
get
(
'USE_MICROSITES'
,
False
):
enable_microsites
()
# Initialize Segment analytics module by setting the write_key.
if
settings
.
LMS_SEGMENT_KEY
:
analytics
.
write_key
=
settings
.
LMS_SEGMENT_KEY
...
...
@@ -119,56 +123,12 @@ def enable_stanford_theme():
settings
.
LOCALE_PATHS
=
(
theme_root
/
'conf/locale'
,)
+
settings
.
LOCALE_PATHS
def
enable_microsites_pre_startup
():
"""
The TEMPLATE_ENGINE directory to search for microsite templates
in non-mako templates must be loaded before the django startup
"""
microsites_root
=
settings
.
MICROSITE_ROOT_DIR
microsite_config_dict
=
settings
.
MICROSITE_CONFIGURATION
if
microsite_config_dict
:
settings
.
DEFAULT_TEMPLATE_ENGINE
[
'DIRS'
]
.
append
(
microsites_root
)
def
enable_microsites
():
"""
Enable the use of microsites, which are websites that allow
for subdomains for the edX platform, e.g. foo.edx.org
Calls the enable_microsites function in the microsite backend.
Here for backwards compatibility
"""
microsites_root
=
settings
.
MICROSITE_ROOT_DIR
microsite_config_dict
=
settings
.
MICROSITE_CONFIGURATION
for
ms_name
,
ms_config
in
microsite_config_dict
.
items
():
# Calculate the location of the microsite's files
ms_root
=
microsites_root
/
ms_name
ms_config
=
microsite_config_dict
[
ms_name
]
# pull in configuration information from each
# microsite root
if
ms_root
.
isdir
():
# store the path on disk for later use
ms_config
[
'microsite_root'
]
=
ms_root
template_dir
=
ms_root
/
'templates'
ms_config
[
'template_dir'
]
=
template_dir
ms_config
[
'microsite_name'
]
=
ms_name
log
.
info
(
'Loading microsite
%
s'
,
ms_root
)
else
:
# not sure if we have application logging at this stage of
# startup
log
.
error
(
'Error loading microsite
%
s. Directory does not exist'
,
ms_root
)
# remove from our configuration as it is not valid
del
microsite_config_dict
[
ms_name
]
# if we have any valid microsites defined, let's wire in the Mako and STATIC_FILES search paths
if
microsite_config_dict
:
edxmako
.
paths
.
add_lookup
(
'main'
,
microsites_root
)
settings
.
STATICFILES_DIRS
.
insert
(
0
,
microsites_root
)
microsite
.
enable_microsites
(
log
)
def
enable_third_party_auth
():
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment