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
beba48a5
Commit
beba48a5
authored
Jun 27, 2017
by
Clinton Blackburn
Committed by
Clinton Blackburn
Jun 28, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added model and API endpoints for experiment-scoped data
parent
be51c719
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
194 additions
and
15 deletions
+194
-15
lms/djangoapps/experiments/factories.py
+10
-1
lms/djangoapps/experiments/filters.py
+7
-1
lms/djangoapps/experiments/migrations/0002_auto_20170627_1402.py
+34
-0
lms/djangoapps/experiments/models.py
+15
-0
lms/djangoapps/experiments/permissions.py
+7
-0
lms/djangoapps/experiments/serializers.py
+7
-1
lms/djangoapps/experiments/tests/test_views.py
+83
-2
lms/djangoapps/experiments/urls.py
+3
-3
lms/djangoapps/experiments/views.py
+28
-7
No files found.
lms/djangoapps/experiments/factories.py
View file @
beba48a5
import
factory
from
experiments.models
import
ExperimentData
from
experiments.models
import
ExperimentData
,
ExperimentKeyValue
from
student.tests.factories
import
UserFactory
...
...
@@ -12,3 +12,12 @@ class ExperimentDataFactory(factory.DjangoModelFactory):
experiment_id
=
factory
.
fuzzy
.
FuzzyInteger
(
0
)
key
=
factory
.
Sequence
(
lambda
n
:
n
)
value
=
factory
.
Faker
(
'word'
)
class
ExperimentKeyValueFactory
(
factory
.
DjangoModelFactory
):
class
Meta
(
object
):
model
=
ExperimentKeyValue
experiment_id
=
factory
.
fuzzy
.
FuzzyInteger
(
0
)
key
=
factory
.
Sequence
(
lambda
n
:
n
)
value
=
factory
.
Faker
(
'word'
)
lms/djangoapps/experiments/filters.py
View file @
beba48a5
import
django_filters
from
experiments.models
import
ExperimentData
from
experiments.models
import
ExperimentData
,
ExperimentKeyValue
class
ExperimentDataFilter
(
django_filters
.
FilterSet
):
class
Meta
(
object
):
model
=
ExperimentData
fields
=
[
'experiment_id'
,
'key'
,
]
class
ExperimentKeyValueFilter
(
django_filters
.
FilterSet
):
class
Meta
(
object
):
model
=
ExperimentKeyValue
fields
=
[
'experiment_id'
,
'key'
,
]
lms/djangoapps/experiments/migrations/0002_auto_20170627_1402.py
0 → 100644
View file @
beba48a5
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
import
django_extensions.db.fields
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'experiments'
,
'0001_initial'
),
]
operations
=
[
migrations
.
CreateModel
(
name
=
'ExperimentKeyValue'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
verbose_name
=
'ID'
,
serialize
=
False
,
auto_created
=
True
,
primary_key
=
True
)),
(
'created'
,
django_extensions
.
db
.
fields
.
CreationDateTimeField
(
auto_now_add
=
True
,
verbose_name
=
'created'
)),
(
'modified'
,
django_extensions
.
db
.
fields
.
ModificationDateTimeField
(
auto_now
=
True
,
verbose_name
=
'modified'
)),
(
'experiment_id'
,
models
.
PositiveSmallIntegerField
(
verbose_name
=
b
'Experiment ID'
,
db_index
=
True
)),
(
'key'
,
models
.
CharField
(
max_length
=
255
)),
(
'value'
,
models
.
TextField
()),
],
options
=
{
'verbose_name'
:
'Experiment Data'
,
'verbose_name_plural'
:
'Experiment Data'
,
},
),
migrations
.
AlterUniqueTogether
(
name
=
'experimentkeyvalue'
,
unique_together
=
set
([(
'experiment_id'
,
'key'
)]),
),
]
lms/djangoapps/experiments/models.py
View file @
beba48a5
...
...
@@ -20,3 +20,18 @@ class ExperimentData(TimeStampedModel):
unique_together
=
(
(
'user'
,
'experiment_id'
,
'key'
),
)
class
ExperimentKeyValue
(
TimeStampedModel
):
experiment_id
=
models
.
PositiveSmallIntegerField
(
null
=
False
,
blank
=
False
,
db_index
=
True
,
verbose_name
=
'Experiment ID'
)
key
=
models
.
CharField
(
null
=
False
,
blank
=
False
,
max_length
=
255
)
value
=
models
.
TextField
()
class
Meta
(
object
):
verbose_name
=
'Experiment Data'
verbose_name_plural
=
'Experiment Data'
unique_together
=
(
(
'experiment_id'
,
'key'
),
)
lms/djangoapps/experiments/permissions.py
View file @
beba48a5
from
rest_framework.permissions
import
SAFE_METHODS
,
BasePermission
from
openedx.core.lib.api
import
permissions
...
...
@@ -17,3 +19,8 @@ class IsStaffOrOwner(permissions.IsStaffOrOwner):
# The view will handle filtering for the current user
return
True
class
IsStaffOrReadOnly
(
BasePermission
):
def
has_permission
(
self
,
request
,
view
):
return
request
.
user
.
is_staff
or
request
.
method
in
SAFE_METHODS
lms/djangoapps/experiments/serializers.py
View file @
beba48a5
from
django.contrib.auth
import
get_user_model
from
rest_framework
import
serializers
from
.models
import
ExperimentData
from
.models
import
ExperimentData
,
ExperimentKeyValue
User
=
get_user_model
()
# pylint:disable=invalid-name
...
...
@@ -20,3 +20,9 @@ class ExperimentDataSerializer(serializers.ModelSerializer):
class
Meta
(
ExperimentDataCreateSerializer
.
Meta
):
read_only_fields
=
(
'user'
,)
class
ExperimentKeyValueSerializer
(
serializers
.
ModelSerializer
):
class
Meta
(
object
):
model
=
ExperimentKeyValue
fields
=
(
'id'
,
'experiment_id'
,
'key'
,
'value'
,
'created'
,
'modified'
,)
lms/djangoapps/experiments/tests/test_views.py
View file @
beba48a5
...
...
@@ -3,8 +3,8 @@ import urllib
from
django.core.urlresolvers
import
reverse
from
rest_framework.test
import
APITestCase
from
experiments.factories
import
ExperimentDataFactory
from
experiments.models
import
ExperimentData
from
experiments.factories
import
ExperimentDataFactory
,
ExperimentKeyValueFactory
from
experiments.models
import
ExperimentData
,
ExperimentKeyValue
from
experiments.serializers
import
ExperimentDataSerializer
from
student.tests.factories
import
UserFactory
...
...
@@ -208,3 +208,84 @@ class ExperimentDataViewSetTests(APITestCase):
self
.
assertEqual
(
response
.
status_code
,
200
)
ExperimentData
.
objects
.
get
(
user
=
user
,
**
kwargs
)
ExperimentData
.
objects
.
get
(
user
=
other_user
,
**
kwargs
)
class
ExperimentKeyValueViewSetTests
(
APITestCase
):
def
test_permissions
(
self
):
""" Staff access is required for write operations. """
url
=
reverse
(
'api_experiments:v0:key_value-list'
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response
=
self
.
client
.
post
(
url
,
{})
self
.
assertEqual
(
response
.
status_code
,
401
)
instance
=
ExperimentKeyValueFactory
()
url
=
reverse
(
'api_experiments:v0:key_value-detail'
,
kwargs
=
{
'pk'
:
instance
.
id
})
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
user
=
UserFactory
(
is_staff
=
False
)
self
.
client
.
login
(
username
=
user
.
username
,
password
=
UserFactory
.
_DEFAULT_PASSWORD
)
response
=
self
.
client
.
put
(
url
,
{})
self
.
assertEqual
(
response
.
status_code
,
403
)
response
=
self
.
client
.
patch
(
url
,
{})
self
.
assertEqual
(
response
.
status_code
,
403
)
response
=
self
.
client
.
delete
(
url
)
self
.
assertEqual
(
response
.
status_code
,
403
)
def
test_bulk_upsert_permissions
(
self
):
""" Non-staff users should not be allowed to access the endpoint. """
data
=
[]
url
=
reverse
(
'api_experiments:v0:key_value-bulk-upsert'
)
user
=
UserFactory
(
is_staff
=
False
)
# Authentication required
response
=
self
.
client
.
put
(
url
,
data
,
format
=
'json'
)
self
.
assertEqual
(
response
.
status_code
,
401
)
# Staff permission required
self
.
client
.
login
(
username
=
user
.
username
,
password
=
UserFactory
.
_DEFAULT_PASSWORD
)
response
=
self
.
client
.
put
(
url
,
data
,
format
=
'json'
)
self
.
assertEqual
(
response
.
status_code
,
403
)
def
test_bulk_upsert
(
self
):
""" The endpoint should support creating/updating multiple ExperimentData objects with a single call. """
url
=
reverse
(
'api_experiments:v0:key_value-bulk-upsert'
)
experiment_id
=
1
user
=
UserFactory
(
is_staff
=
True
)
data
=
[
{
'experiment_id'
:
experiment_id
,
'key'
:
'foo'
,
'value'
:
'bar'
,
},
{
'experiment_id'
:
experiment_id
,
'key'
:
'foo1'
,
'value'
:
'bar'
,
},
]
self
.
client
.
login
(
username
=
user
.
username
,
password
=
UserFactory
.
_DEFAULT_PASSWORD
)
# New data should be created
response
=
self
.
client
.
put
(
url
,
data
,
format
=
'json'
)
self
.
assertEqual
(
response
.
status_code
,
200
)
kwargs
=
{
'experiment_id'
:
experiment_id
,
'value'
:
'bar'
,
}
ExperimentKeyValue
.
objects
.
get
(
key
=
'foo'
,
**
kwargs
)
ExperimentKeyValue
.
objects
.
get
(
key
=
'foo1'
,
**
kwargs
)
# Subsequent calls should update the existing data rather than create more
response
=
self
.
client
.
put
(
url
,
data
,
format
=
'json'
)
self
.
assertEqual
(
response
.
status_code
,
200
)
ExperimentKeyValue
.
objects
.
get
(
key
=
'foo'
,
**
kwargs
)
ExperimentKeyValue
.
objects
.
get
(
key
=
'foo1'
,
**
kwargs
)
lms/djangoapps/experiments/urls.py
View file @
beba48a5
from
django.conf.urls
import
include
,
url
from
experiments
import
routers
from
experiments.views
import
ExperimentDataViewSet
from
experiments
import
routers
,
views
router
=
routers
.
DefaultRouter
()
router
.
register
(
r'data'
,
ExperimentDataViewSet
,
base_name
=
'data'
)
router
.
register
(
r'data'
,
views
.
ExperimentDataViewSet
,
base_name
=
'data'
)
router
.
register
(
r'key-value'
,
views
.
ExperimentKeyValueViewSet
,
base_name
=
'key_value'
)
urlpatterns
=
[
url
(
r'^v0/'
,
include
(
router
.
urls
,
namespace
=
'v0'
)),
...
...
lms/djangoapps/experiments/views.py
View file @
beba48a5
...
...
@@ -6,10 +6,9 @@ from rest_framework.decorators import list_route
from
rest_framework.filters
import
DjangoFilterBackend
from
rest_framework.response
import
Response
from
experiments
import
filters
from
experiments.models
import
ExperimentData
from
experiments.permissions
import
IsStaffOrOwner
from
experiments.serializers
import
ExperimentDataCreateSerializer
,
ExperimentDataSerializer
from
experiments
import
filters
,
serializers
from
experiments.models
import
ExperimentData
,
ExperimentKeyValue
from
experiments.permissions
import
IsStaffOrOwner
,
IsStaffOrReadOnly
from
openedx.core.lib.api.authentication
import
SessionAuthenticationAllowInactiveUser
User
=
get_user_model
()
# pylint: disable=invalid-name
...
...
@@ -21,7 +20,7 @@ class ExperimentDataViewSet(viewsets.ModelViewSet):
filter_class
=
filters
.
ExperimentDataFilter
permission_classes
=
(
permissions
.
IsAuthenticated
,
IsStaffOrOwner
,)
queryset
=
ExperimentData
.
objects
.
all
()
serializer_class
=
ExperimentDataSerializer
serializer_class
=
serializers
.
ExperimentDataSerializer
_cached_users
=
{}
def
filter_queryset
(
self
,
queryset
):
...
...
@@ -30,8 +29,8 @@ class ExperimentDataViewSet(viewsets.ModelViewSet):
def
get_serializer_class
(
self
):
if
self
.
action
==
'create'
:
return
ExperimentDataCreateSerializer
return
ExperimentDataSerializer
return
serializers
.
ExperimentDataCreateSerializer
return
serializers
.
ExperimentDataSerializer
def
create_or_update
(
self
,
request
,
*
args
,
**
kwargs
):
# If we have a primary key, treat this as a regular update request
...
...
@@ -82,3 +81,25 @@ class ExperimentDataViewSet(viewsets.ModelViewSet):
serializer
=
self
.
get_serializer
(
upserted
,
many
=
True
)
return
Response
(
serializer
.
data
)
class
ExperimentKeyValueViewSet
(
viewsets
.
ModelViewSet
):
authentication_classes
=
(
JwtAuthentication
,
SessionAuthenticationAllowInactiveUser
,)
filter_backends
=
(
DjangoFilterBackend
,)
filter_class
=
filters
.
ExperimentKeyValueFilter
permission_classes
=
(
IsStaffOrReadOnly
,)
queryset
=
ExperimentKeyValue
.
objects
.
all
()
serializer_class
=
serializers
.
ExperimentKeyValueSerializer
@list_route
(
methods
=
[
'put'
],
permission_classes
=
[
permissions
.
IsAdminUser
])
def
bulk_upsert
(
self
,
request
):
upserted
=
[]
with
transaction
.
atomic
():
for
item
in
request
.
data
:
datum
,
__
=
ExperimentKeyValue
.
objects
.
update_or_create
(
experiment_id
=
item
[
'experiment_id'
],
key
=
item
[
'key'
],
defaults
=
{
'value'
:
item
[
'value'
]})
upserted
.
append
(
datum
)
serializer
=
self
.
get_serializer
(
upserted
,
many
=
True
)
return
Response
(
serializer
.
data
)
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