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
4fb3da23
Commit
4fb3da23
authored
Oct 07, 2016
by
John Eskew
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Remove config_models & use external config_models repo instead.
parent
5cb129e2
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
1 additions
and
766 deletions
+1
-766
common/djangoapps/config_models/README.rst
+0
-0
common/djangoapps/config_models/__init__.py
+0
-62
common/djangoapps/config_models/admin.py
+0
-207
common/djangoapps/config_models/decorators.py
+0
-26
common/djangoapps/config_models/management/__init__.py
+0
-0
common/djangoapps/config_models/management/commands/__init__.py
+0
-0
common/djangoapps/config_models/management/commands/populate_model.py
+0
-72
common/djangoapps/config_models/models.py
+0
-0
common/djangoapps/config_models/templatetags.py
+0
-29
common/djangoapps/config_models/tests/__init__.py
+0
-0
common/djangoapps/config_models/tests/data/data.json
+0
-14
common/djangoapps/config_models/tests/test_model_deserialization.py
+0
-219
common/djangoapps/config_models/tests/tests.py
+0
-0
common/djangoapps/config_models/utils.py
+0
-69
common/djangoapps/config_models/views.py
+0
-68
requirements/edx/github.txt
+1
-0
No files found.
common/djangoapps/config_models/README.rst
deleted
100644 → 0
View file @
5cb129e2
common/djangoapps/config_models/__init__.py
deleted
100644 → 0
View file @
5cb129e2
"""
Model-Based Configuration
=========================
This app allows other apps to easily define a configuration model
that can be hooked into the admin site to allow configuration management
with auditing.
Installation
------------
Add ``config_models`` to your ``INSTALLED_APPS`` list.
Usage
-----
Create a subclass of ``ConfigurationModel``, with fields for each
value that needs to be configured::
class MyConfiguration(ConfigurationModel):
frobble_timeout = IntField(default=10)
frazzle_target = TextField(defalut="debug")
This is a normal django model, so it must be synced and migrated as usual.
The default values for the fields in the ``ConfigurationModel`` will be
used if no configuration has yet been created.
Register that class with the Admin site, using the ``ConfigurationAdminModel``::
from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin
admin.site.register(MyConfiguration, ConfigurationModelAdmin)
Use the configuration in your code::
def my_view(self, request):
config = MyConfiguration.current()
fire_the_missiles(config.frazzle_target, timeout=config.frobble_timeout)
Use the admin site to add new configuration entries. The most recently created
entry is considered to be ``current``.
Configuration
-------------
The current ``ConfigurationModel`` will be cached in the ``configuration`` django cache,
or in the ``default`` cache if ``configuration`` doesn't exist. You can specify the cache
timeout in each ``ConfigurationModel`` by setting the ``cache_timeout`` property.
You can change the name of the cache key used by the ``ConfigurationModel`` by overriding
the ``cache_key_name`` function.
Extension
---------
``ConfigurationModels`` are just django models, so they can be extended with new fields
and migrated as usual. Newly added fields must have default values and should be nullable,
so that rollbacks to old versions of configuration work correctly.
"""
common/djangoapps/config_models/admin.py
deleted
100644 → 0
View file @
5cb129e2
"""
Admin site models for managing :class:`.ConfigurationModel` subclasses
"""
from
django.forms
import
models
from
django.contrib
import
admin
from
django.contrib.admin
import
ListFilter
from
django.core.cache
import
caches
,
InvalidCacheBackendError
from
django.core.files.base
import
File
from
django.core.urlresolvers
import
reverse
from
django.http
import
HttpResponseRedirect
from
django.shortcuts
import
get_object_or_404
from
django.utils.translation
import
ugettext_lazy
as
_
try
:
cache
=
caches
[
'configuration'
]
# pylint: disable=invalid-name
except
InvalidCacheBackendError
:
from
django.core.cache
import
cache
# pylint: disable=protected-access
class
ConfigurationModelAdmin
(
admin
.
ModelAdmin
):
"""
:class:`~django.contrib.admin.ModelAdmin` for :class:`.ConfigurationModel` subclasses
"""
date_hierarchy
=
'change_date'
def
get_actions
(
self
,
request
):
return
{
'revert'
:
(
ConfigurationModelAdmin
.
revert
,
'revert'
,
_
(
'Revert to the selected configuration'
))
}
def
get_list_display
(
self
,
request
):
return
self
.
get_displayable_field_names
()
def
get_displayable_field_names
(
self
):
"""
Return all field names, excluding reverse foreign key relationships.
"""
return
[
f
.
name
for
f
in
self
.
model
.
_meta
.
get_fields
()
if
not
f
.
one_to_many
]
# Don't allow deletion of configuration
def
has_delete_permission
(
self
,
request
,
obj
=
None
):
return
False
# Make all fields read-only when editing an object
def
get_readonly_fields
(
self
,
request
,
obj
=
None
):
if
obj
:
# editing an existing object
return
self
.
get_displayable_field_names
()
return
self
.
readonly_fields
def
add_view
(
self
,
request
,
form_url
=
''
,
extra_context
=
None
):
# Prepopulate new configuration entries with the value of the current config
get
=
request
.
GET
.
copy
()
get
.
update
(
models
.
model_to_dict
(
self
.
model
.
current
()))
request
.
GET
=
get
return
super
(
ConfigurationModelAdmin
,
self
)
.
add_view
(
request
,
form_url
,
extra_context
)
# Hide the save buttons in the change view
def
change_view
(
self
,
request
,
object_id
,
form_url
=
''
,
extra_context
=
None
):
extra_context
=
extra_context
or
{}
extra_context
[
'readonly'
]
=
True
return
super
(
ConfigurationModelAdmin
,
self
)
.
change_view
(
request
,
object_id
,
form_url
,
extra_context
=
extra_context
)
def
save_model
(
self
,
request
,
obj
,
form
,
change
):
obj
.
changed_by
=
request
.
user
super
(
ConfigurationModelAdmin
,
self
)
.
save_model
(
request
,
obj
,
form
,
change
)
cache
.
delete
(
obj
.
cache_key_name
(
*
(
getattr
(
obj
,
key_name
)
for
key_name
in
obj
.
KEY_FIELDS
)))
cache
.
delete
(
obj
.
key_values_cache_key_name
())
def
revert
(
self
,
request
,
queryset
):
"""
Admin action to revert a configuration back to the selected value
"""
if
queryset
.
count
()
!=
1
:
self
.
message_user
(
request
,
_
(
"Please select a single configuration to revert to."
))
return
target
=
queryset
[
0
]
target
.
id
=
None
self
.
save_model
(
request
,
target
,
None
,
False
)
self
.
message_user
(
request
,
_
(
"Reverted configuration."
))
return
HttpResponseRedirect
(
reverse
(
'admin:{}_{}_change'
.
format
(
self
.
model
.
_meta
.
app_label
,
self
.
model
.
_meta
.
model_name
,
),
args
=
(
target
.
id
,),
)
)
class
ShowHistoryFilter
(
ListFilter
):
"""
Admin change view filter to show only the most recent (i.e. the "current") row for each
unique key value.
"""
title
=
_
(
'Status'
)
parameter_name
=
'show_history'
def
__init__
(
self
,
request
,
params
,
model
,
model_admin
):
super
(
ShowHistoryFilter
,
self
)
.
__init__
(
request
,
params
,
model
,
model_admin
)
if
self
.
parameter_name
in
params
:
value
=
params
.
pop
(
self
.
parameter_name
)
self
.
used_parameters
[
self
.
parameter_name
]
=
value
def
has_output
(
self
):
""" Should this filter be shown? """
return
True
def
choices
(
self
,
cl
):
""" Returns choices ready to be output in the template. """
show_all
=
self
.
used_parameters
.
get
(
self
.
parameter_name
)
==
"1"
return
(
{
'display'
:
_
(
'Current Configuration'
),
'selected'
:
not
show_all
,
'query_string'
:
cl
.
get_query_string
({},
[
self
.
parameter_name
]),
},
{
'display'
:
_
(
'All (Show History)'
),
'selected'
:
show_all
,
'query_string'
:
cl
.
get_query_string
({
self
.
parameter_name
:
"1"
},
[]),
}
)
def
queryset
(
self
,
request
,
queryset
):
""" Filter the queryset. No-op since it's done by KeyedConfigurationModelAdmin """
return
queryset
def
expected_parameters
(
self
):
""" List the query string params used by this filter """
return
[
self
.
parameter_name
]
class
KeyedConfigurationModelAdmin
(
ConfigurationModelAdmin
):
"""
:class:`~django.contrib.admin.ModelAdmin` for :class:`.ConfigurationModel` subclasses that
use extra keys (i.e. they have KEY_FIELDS set).
"""
date_hierarchy
=
None
list_filter
=
(
ShowHistoryFilter
,
)
def
get_queryset
(
self
,
request
):
"""
Annote the queryset with an 'is_active' property that's true iff that row is the most
recently added row for that particular set of KEY_FIELDS values.
Filter the queryset to show only is_active rows by default.
"""
if
request
.
GET
.
get
(
ShowHistoryFilter
.
parameter_name
)
==
'1'
:
queryset
=
self
.
model
.
objects
.
with_active_flag
()
else
:
# Show only the most recent row for each key.
queryset
=
self
.
model
.
objects
.
current_set
()
ordering
=
self
.
get_ordering
(
request
)
if
ordering
:
return
queryset
.
order_by
(
*
ordering
)
return
queryset
def
get_list_display
(
self
,
request
):
""" Add a link to each row for creating a new row using the chosen row as a template """
return
self
.
get_displayable_field_names
()
+
[
'edit_link'
]
def
add_view
(
self
,
request
,
form_url
=
''
,
extra_context
=
None
):
# Prepopulate new configuration entries with the value of the current config, if given:
if
'source'
in
request
.
GET
:
get
=
request
.
GET
.
copy
()
source_id
=
int
(
get
.
pop
(
'source'
)[
0
])
source
=
get_object_or_404
(
self
.
model
,
pk
=
source_id
)
source_dict
=
models
.
model_to_dict
(
source
)
for
field_name
,
field_value
in
source_dict
.
items
():
# read files into request.FILES, if:
# * user hasn't ticked the "clear" checkbox
# * user hasn't uploaded a new file
if
field_value
and
isinstance
(
field_value
,
File
):
clear_checkbox_name
=
'{0}-clear'
.
format
(
field_name
)
if
request
.
POST
.
get
(
clear_checkbox_name
)
!=
'on'
:
request
.
FILES
.
setdefault
(
field_name
,
field_value
)
get
[
field_name
]
=
field_value
request
.
GET
=
get
# Call our grandparent's add_view, skipping the parent code
# because the parent code has a different way to prepopulate new configuration entries
# with the value of the latest config, which doesn't make sense for keyed models.
# pylint: disable=bad-super-call
return
super
(
ConfigurationModelAdmin
,
self
)
.
add_view
(
request
,
form_url
,
extra_context
)
def
edit_link
(
self
,
inst
):
""" Edit link for the change view """
if
not
inst
.
is_active
:
return
u'--'
update_url
=
reverse
(
'admin:{}_{}_add'
.
format
(
self
.
model
.
_meta
.
app_label
,
self
.
model
.
_meta
.
model_name
))
update_url
+=
"?source={}"
.
format
(
inst
.
pk
)
return
u'<a href="{}">{}</a>'
.
format
(
update_url
,
_
(
'Update'
))
edit_link
.
allow_tags
=
True
edit_link
.
short_description
=
_
(
'Update'
)
common/djangoapps/config_models/decorators.py
deleted
100644 → 0
View file @
5cb129e2
"""Decorators for model-based configuration. """
from
functools
import
wraps
from
django.http
import
HttpResponseNotFound
def
require_config
(
config_model
):
"""View decorator that enables/disables a view based on configuration.
Arguments:
config_model (ConfigurationModel subclass): The class of the configuration
model to check.
Returns:
HttpResponse: 404 if the configuration model is disabled,
otherwise returns the response from the decorated view.
"""
def
_decorator
(
func
):
@wraps
(
func
)
def
_inner
(
*
args
,
**
kwargs
):
if
not
config_model
.
current
()
.
enabled
:
return
HttpResponseNotFound
()
else
:
return
func
(
*
args
,
**
kwargs
)
return
_inner
return
_decorator
common/djangoapps/config_models/management/__init__.py
deleted
100644 → 0
View file @
5cb129e2
common/djangoapps/config_models/management/commands/__init__.py
deleted
100644 → 0
View file @
5cb129e2
common/djangoapps/config_models/management/commands/populate_model.py
deleted
100644 → 0
View file @
5cb129e2
"""
Populates a ConfigurationModel by deserializing JSON data contained in a file.
"""
import
os
from
optparse
import
make_option
from
django.core.management.base
import
BaseCommand
,
CommandError
from
django.utils.translation
import
ugettext_lazy
as
_
from
config_models.utils
import
deserialize_json
class
Command
(
BaseCommand
):
"""
This command will deserialize the JSON data in the supplied file to populate
a ConfigurationModel. Note that this will add new entries to the model, but it
will not delete any entries (ConfigurationModel entries are read-only).
"""
help
=
"""
Populates a ConfigurationModel by deserializing the supplied JSON.
JSON should be in a file, with the following format:
{ "model": "config_models.ExampleConfigurationModel",
"data":
[
{ "enabled": True,
"color": "black"
...
},
{ "enabled": False,
"color": "yellow"
...
},
...
]
}
A username corresponding to an existing user must be specified to indicate who
is executing the command.
$ ... populate_model -f path/to/file.json -u username
"""
option_list
=
BaseCommand
.
option_list
+
(
make_option
(
'-f'
,
'--file'
,
metavar
=
'JSON_FILE'
,
dest
=
'file'
,
default
=
False
,
help
=
'JSON file to import ConfigurationModel data'
),
make_option
(
'-u'
,
'--username'
,
metavar
=
'USERNAME'
,
dest
=
'username'
,
default
=
False
,
help
=
'username to specify who is executing the command'
),
)
def
handle
(
self
,
*
args
,
**
options
):
if
'file'
not
in
options
or
not
options
[
'file'
]:
raise
CommandError
(
_
(
"A file containing JSON must be specified."
))
if
'username'
not
in
options
or
not
options
[
'username'
]:
raise
CommandError
(
_
(
"A valid username must be specified."
))
json_file
=
options
[
'file'
]
if
not
os
.
path
.
exists
(
json_file
):
raise
CommandError
(
_
(
"File {0} does not exist"
)
.
format
(
json_file
))
self
.
stdout
.
write
(
_
(
"Importing JSON data from file {0}"
)
.
format
(
json_file
))
with
open
(
json_file
)
as
data
:
created_entries
=
deserialize_json
(
data
,
options
[
'username'
])
self
.
stdout
.
write
(
_
(
"Import complete, {0} new entries created"
)
.
format
(
created_entries
))
common/djangoapps/config_models/models.py
deleted
100644 → 0
View file @
5cb129e2
This diff is collapsed.
Click to expand it.
common/djangoapps/config_models/templatetags.py
deleted
100644 → 0
View file @
5cb129e2
"""
Override the submit_row template tag to remove all save buttons from the
admin dashboard change view if the context has readonly marked in it.
"""
from
django.contrib.admin.templatetags.admin_modify
import
register
from
django.contrib.admin.templatetags.admin_modify
import
submit_row
as
original_submit_row
@register.inclusion_tag
(
'admin/submit_line.html'
,
takes_context
=
True
)
def
submit_row
(
context
):
"""
Overrides 'django.contrib.admin.templatetags.admin_modify.submit_row'.
Manipulates the context going into that function by hiding all of the buttons
in the submit row if the key `readonly` is set in the context.
"""
ctx
=
original_submit_row
(
context
)
if
context
.
get
(
'readonly'
,
False
):
ctx
.
update
({
'show_delete_link'
:
False
,
'show_save_as_new'
:
False
,
'show_save_and_add_another'
:
False
,
'show_save_and_continue'
:
False
,
'show_save'
:
False
,
})
else
:
return
ctx
common/djangoapps/config_models/tests/__init__.py
deleted
100644 → 0
View file @
5cb129e2
common/djangoapps/config_models/tests/data/data.json
deleted
100644 → 0
View file @
5cb129e2
{
"model"
:
"config_models.ExampleDeserializeConfig"
,
"data"
:
[
{
"name"
:
"betty"
,
"enabled"
:
true
,
"int_field"
:
5
},
{
"name"
:
"fred"
,
"enabled"
:
false
}
]
}
common/djangoapps/config_models/tests/test_model_deserialization.py
deleted
100644 → 0
View file @
5cb129e2
"""
Tests of the populate_model management command and its helper utils.deserialize_json method.
"""
import
textwrap
import
os.path
from
django.utils
import
timezone
from
django.utils.six
import
BytesIO
from
django.contrib.auth.models
import
User
from
django.core.management.base
import
CommandError
from
django.db
import
models
from
config_models.management.commands
import
populate_model
from
config_models.models
import
ConfigurationModel
from
config_models.utils
import
deserialize_json
from
openedx.core.djangolib.testing.utils
import
CacheIsolationTestCase
class
ExampleDeserializeConfig
(
ConfigurationModel
):
"""
Test model for testing deserialization of ``ConfigurationModels`` with keyed configuration.
"""
KEY_FIELDS
=
(
'name'
,)
name
=
models
.
TextField
()
int_field
=
models
.
IntegerField
(
default
=
10
)
def
__unicode__
(
self
):
return
"ExampleDeserializeConfig(enabled={}, name={}, int_field={})"
.
format
(
self
.
enabled
,
self
.
name
,
self
.
int_field
)
class
DeserializeJSONTests
(
CacheIsolationTestCase
):
"""
Tests of deserializing the JSON representation of ConfigurationModels.
"""
def
setUp
(
self
):
super
(
DeserializeJSONTests
,
self
)
.
setUp
()
self
.
test_username
=
'test_worker'
User
.
objects
.
create_user
(
username
=
self
.
test_username
)
self
.
fixture_path
=
os
.
path
.
join
(
os
.
path
.
dirname
(
__file__
),
'data'
,
'data.json'
)
def
test_deserialize_models
(
self
):
"""
Tests the "happy path", where 2 instances of the test model should be created.
A valid username is supplied for the operation.
"""
start_date
=
timezone
.
now
()
with
open
(
self
.
fixture_path
)
as
data
:
entries_created
=
deserialize_json
(
data
,
self
.
test_username
)
self
.
assertEquals
(
2
,
entries_created
)
self
.
assertEquals
(
2
,
ExampleDeserializeConfig
.
objects
.
count
())
betty
=
ExampleDeserializeConfig
.
current
(
'betty'
)
self
.
assertTrue
(
betty
.
enabled
)
self
.
assertEquals
(
5
,
betty
.
int_field
)
self
.
assertGreater
(
betty
.
change_date
,
start_date
)
self
.
assertEquals
(
self
.
test_username
,
betty
.
changed_by
.
username
)
fred
=
ExampleDeserializeConfig
.
current
(
'fred'
)
self
.
assertFalse
(
fred
.
enabled
)
self
.
assertEquals
(
10
,
fred
.
int_field
)
self
.
assertGreater
(
fred
.
change_date
,
start_date
)
self
.
assertEquals
(
self
.
test_username
,
fred
.
changed_by
.
username
)
def
test_existing_entries_not_removed
(
self
):
"""
Any existing configuration model entries are retained
(though they may be come history)-- deserialize_json is purely additive.
"""
ExampleDeserializeConfig
(
name
=
"fred"
,
enabled
=
True
)
.
save
()
ExampleDeserializeConfig
(
name
=
"barney"
,
int_field
=
200
)
.
save
()
with
open
(
self
.
fixture_path
)
as
data
:
entries_created
=
deserialize_json
(
data
,
self
.
test_username
)
self
.
assertEquals
(
2
,
entries_created
)
self
.
assertEquals
(
4
,
ExampleDeserializeConfig
.
objects
.
count
())
self
.
assertEquals
(
3
,
len
(
ExampleDeserializeConfig
.
objects
.
current_set
()))
self
.
assertEquals
(
5
,
ExampleDeserializeConfig
.
current
(
'betty'
)
.
int_field
)
self
.
assertEquals
(
200
,
ExampleDeserializeConfig
.
current
(
'barney'
)
.
int_field
)
# The JSON file changes "enabled" to False for Fred.
fred
=
ExampleDeserializeConfig
.
current
(
'fred'
)
self
.
assertFalse
(
fred
.
enabled
)
def
test_duplicate_entries_not_made
(
self
):
"""
If there is no change in an entry (besides changed_by and change_date),
a new entry is not made.
"""
with
open
(
self
.
fixture_path
)
as
data
:
entries_created
=
deserialize_json
(
data
,
self
.
test_username
)
self
.
assertEquals
(
2
,
entries_created
)
with
open
(
self
.
fixture_path
)
as
data
:
entries_created
=
deserialize_json
(
data
,
self
.
test_username
)
self
.
assertEquals
(
0
,
entries_created
)
# Importing twice will still only result in 2 records (second import a no-op).
self
.
assertEquals
(
2
,
ExampleDeserializeConfig
.
objects
.
count
())
# Change Betty.
betty
=
ExampleDeserializeConfig
.
current
(
'betty'
)
betty
.
int_field
=
-
8
betty
.
save
()
self
.
assertEquals
(
3
,
ExampleDeserializeConfig
.
objects
.
count
())
self
.
assertEquals
(
-
8
,
ExampleDeserializeConfig
.
current
(
'betty'
)
.
int_field
)
# Now importing will add a new entry for Betty.
with
open
(
self
.
fixture_path
)
as
data
:
entries_created
=
deserialize_json
(
data
,
self
.
test_username
)
self
.
assertEquals
(
1
,
entries_created
)
self
.
assertEquals
(
4
,
ExampleDeserializeConfig
.
objects
.
count
())
self
.
assertEquals
(
5
,
ExampleDeserializeConfig
.
current
(
'betty'
)
.
int_field
)
def
test_bad_username
(
self
):
"""
Tests the error handling when the specified user does not exist.
"""
test_json
=
textwrap
.
dedent
(
"""
{
"model": "config_models.ExampleDeserializeConfig",
"data": [{"name": "dino"}]
}
"""
)
with
self
.
assertRaisesRegexp
(
Exception
,
"User matching query does not exist"
):
deserialize_json
(
BytesIO
(
test_json
),
"unknown_username"
)
def
test_invalid_json
(
self
):
"""
Tests the error handling when there is invalid JSON.
"""
test_json
=
textwrap
.
dedent
(
"""
{
"model": "config_models.ExampleDeserializeConfig",
"data": [{"name": "dino"
"""
)
with
self
.
assertRaisesRegexp
(
Exception
,
"JSON parse error"
):
deserialize_json
(
BytesIO
(
test_json
),
self
.
test_username
)
def
test_invalid_model
(
self
):
"""
Tests the error handling when the configuration model specified does not exist.
"""
test_json
=
textwrap
.
dedent
(
"""
{
"model": "xxx.yyy",
"data":[{"name": "dino"}]
}
"""
)
with
self
.
assertRaisesRegexp
(
Exception
,
"No installed app"
):
deserialize_json
(
BytesIO
(
test_json
),
self
.
test_username
)
class
PopulateModelTestCase
(
CacheIsolationTestCase
):
"""
Tests of populate model management command.
"""
def
setUp
(
self
):
super
(
PopulateModelTestCase
,
self
)
.
setUp
()
self
.
file_path
=
os
.
path
.
join
(
os
.
path
.
dirname
(
__file__
),
'data'
,
'data.json'
)
self
.
test_username
=
'test_management_worker'
User
.
objects
.
create_user
(
username
=
self
.
test_username
)
def
test_run_command
(
self
):
"""
Tests the "happy path", where 2 instances of the test model should be created.
A valid username is supplied for the operation.
"""
_run_command
(
file
=
self
.
file_path
,
username
=
self
.
test_username
)
self
.
assertEquals
(
2
,
ExampleDeserializeConfig
.
objects
.
count
())
betty
=
ExampleDeserializeConfig
.
current
(
'betty'
)
self
.
assertEquals
(
self
.
test_username
,
betty
.
changed_by
.
username
)
fred
=
ExampleDeserializeConfig
.
current
(
'fred'
)
self
.
assertEquals
(
self
.
test_username
,
fred
.
changed_by
.
username
)
def
test_no_user_specified
(
self
):
"""
Tests that a username must be specified.
"""
with
self
.
assertRaisesRegexp
(
CommandError
,
"A valid username must be specified"
):
_run_command
(
file
=
self
.
file_path
)
def
test_bad_user_specified
(
self
):
"""
Tests that a username must be specified.
"""
with
self
.
assertRaisesRegexp
(
Exception
,
"User matching query does not exist"
):
_run_command
(
file
=
self
.
file_path
,
username
=
"does_not_exist"
)
def
test_no_file_specified
(
self
):
"""
Tests the error handling when no JSON file is supplied.
"""
with
self
.
assertRaisesRegexp
(
CommandError
,
"A file containing JSON must be specified"
):
_run_command
(
username
=
self
.
test_username
)
def
test_bad_file_specified
(
self
):
"""
Tests the error handling when the path to the JSON file is incorrect.
"""
with
self
.
assertRaisesRegexp
(
CommandError
,
"File does/not/exist.json does not exist"
):
_run_command
(
file
=
"does/not/exist.json"
,
username
=
self
.
test_username
)
def
_run_command
(
*
args
,
**
kwargs
):
"""Run the management command to deserializer JSON ConfigurationModel data. """
command
=
populate_model
.
Command
()
return
command
.
handle
(
*
args
,
**
kwargs
)
common/djangoapps/config_models/tests/tests.py
deleted
100644 → 0
View file @
5cb129e2
This diff is collapsed.
Click to expand it.
common/djangoapps/config_models/utils.py
deleted
100644 → 0
View file @
5cb129e2
"""
Utilities for working with ConfigurationModels.
"""
from
django.apps
import
apps
from
rest_framework.parsers
import
JSONParser
from
rest_framework.serializers
import
ModelSerializer
from
django.contrib.auth.models
import
User
def
get_serializer_class
(
configuration_model
):
""" Returns a ConfigurationModel serializer class for the supplied configuration_model. """
class
AutoConfigModelSerializer
(
ModelSerializer
):
"""Serializer class for configuration models."""
class
Meta
(
object
):
"""Meta information for AutoConfigModelSerializer."""
model
=
configuration_model
def
create
(
self
,
validated_data
):
if
"changed_by_username"
in
self
.
context
:
validated_data
[
'changed_by'
]
=
User
.
objects
.
get
(
username
=
self
.
context
[
"changed_by_username"
])
return
super
(
AutoConfigModelSerializer
,
self
)
.
create
(
validated_data
)
return
AutoConfigModelSerializer
def
deserialize_json
(
stream
,
username
):
"""
Given a stream containing JSON, deserializers the JSON into ConfigurationModel instances.
The stream is expected to be in the following format:
{ "model": "config_models.ExampleConfigurationModel",
"data":
[
{ "enabled": True,
"color": "black"
...
},
{ "enabled": False,
"color": "yellow"
...
},
...
]
}
If the provided stream does not contain valid JSON for the ConfigurationModel specified,
an Exception will be raised.
Arguments:
stream: The stream of JSON, as described above.
username: The username of the user making the change. This must match an existing user.
Returns: the number of created entries
"""
parsed_json
=
JSONParser
()
.
parse
(
stream
)
serializer_class
=
get_serializer_class
(
apps
.
get_model
(
parsed_json
[
"model"
]))
list_serializer
=
serializer_class
(
data
=
parsed_json
[
"data"
],
context
=
{
"changed_by_username"
:
username
},
many
=
True
)
if
list_serializer
.
is_valid
():
model_class
=
serializer_class
.
Meta
.
model
for
data
in
reversed
(
list_serializer
.
validated_data
):
if
model_class
.
equal_to_current
(
data
):
list_serializer
.
validated_data
.
remove
(
data
)
entries_created
=
len
(
list_serializer
.
validated_data
)
list_serializer
.
save
()
return
entries_created
else
:
raise
Exception
(
list_serializer
.
error_messages
)
common/djangoapps/config_models/views.py
deleted
100644 → 0
View file @
5cb129e2
"""
API view to allow manipulation of configuration models.
"""
from
rest_framework.generics
import
CreateAPIView
,
RetrieveAPIView
from
rest_framework.permissions
import
DjangoModelPermissions
from
rest_framework.authentication
import
SessionAuthentication
from
django.db
import
transaction
from
config_models.utils
import
get_serializer_class
class
ReadableOnlyByAuthors
(
DjangoModelPermissions
):
"""Only allow access by users with `add` permissions on the model."""
perms_map
=
DjangoModelPermissions
.
perms_map
.
copy
()
perms_map
[
'GET'
]
=
perms_map
[
'OPTIONS'
]
=
perms_map
[
'HEAD'
]
=
perms_map
[
'POST'
]
class
AtomicMixin
(
object
):
"""Mixin to provide atomic transaction for as_view."""
@classmethod
def
create_atomic_wrapper
(
cls
,
wrapped_func
):
"""Returns a wrapped function."""
def
_create_atomic_wrapper
(
*
args
,
**
kwargs
):
"""Actual wrapper."""
# When a view call fails due to a permissions error, it raises an exception.
# An uncaught exception breaks the DB transaction for any following DB operations
# unless it's wrapped in a atomic() decorator or context manager.
with
transaction
.
atomic
():
return
wrapped_func
(
*
args
,
**
kwargs
)
return
_create_atomic_wrapper
@classmethod
def
as_view
(
cls
,
**
initkwargs
):
"""Overrides as_view to add atomic transaction."""
view
=
super
(
AtomicMixin
,
cls
)
.
as_view
(
**
initkwargs
)
return
cls
.
create_atomic_wrapper
(
view
)
class
ConfigurationModelCurrentAPIView
(
AtomicMixin
,
CreateAPIView
,
RetrieveAPIView
):
"""
This view allows an authenticated user with the appropriate model permissions
to read and write the current configuration for the specified `model`.
Like other APIViews, you can use this by using a url pattern similar to the following::
url(r'config/example_config$', ConfigurationModelCurrentAPIView.as_view(model=ExampleConfig))
"""
authentication_classes
=
(
SessionAuthentication
,)
permission_classes
=
(
ReadableOnlyByAuthors
,)
model
=
None
def
get_queryset
(
self
):
return
self
.
model
.
objects
.
all
()
def
get_object
(
self
):
# Return the currently active configuration
return
self
.
model
.
current
()
def
get_serializer_class
(
self
):
if
self
.
serializer_class
is
None
:
self
.
serializer_class
=
get_serializer_class
(
self
.
model
)
return
self
.
serializer_class
def
perform_create
(
self
,
serializer
):
# Set the requesting user as the one who is updating the configuration
serializer
.
save
(
changed_by
=
self
.
request
.
user
)
requirements/edx/github.txt
View file @
4fb3da23
...
...
@@ -92,6 +92,7 @@ git+https://github.com/edx/xblock-utils.git@v1.0.3#egg=xblock-utils==1.0.3
git+https://github.com/edx/edx-user-state-client.git@1.0.1#egg=edx-user-state-client==1.0.1
git+https://github.com/edx/xblock-lti-consumer.git@v1.0.9#egg=xblock-lti-consumer==1.0.9
git+https://github.com/edx/edx-proctoring.git@0.14.0#egg=edx-proctoring==0.14.0
git+https://github.com/edx/django-config-models.git@0.1.0#egg=config_models==0.1.0
# Third Party XBlocks
-e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga
...
...
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