Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-notes-api
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-notes-api
Commits
d769550d
Commit
d769550d
authored
Nov 04, 2016
by
Mushtaq Ali
Committed by
GitHub
Nov 04, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #50 from edx/mushtaq/edxnotes-multiple-usageids
Support bulk search in /search endpoint
parents
eae09382
3d9affca
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
79 additions
and
23 deletions
+79
-23
AUTHORS
+1
-0
notesapi/v1/tests/test_views.py
+26
-0
notesapi/v1/views.py
+43
-17
notesserver/test_views.py
+4
-3
notesserver/views.py
+5
-3
No files found.
AUTHORS
View file @
d769550d
...
@@ -2,3 +2,4 @@ Oleg Marshev <oleg@edx.org>
...
@@ -2,3 +2,4 @@ Oleg Marshev <oleg@edx.org>
Tim Babych <tim.babych@gmail.com>
Tim Babych <tim.babych@gmail.com>
Christina Roberts <christina@edx.org>
Christina Roberts <christina@edx.org>
Ben McMorran <ben.mcmorran@gmail.com>
Ben McMorran <ben.mcmorran@gmail.com>
Mushtaq Ali <mushtaak@gmail.com>
notesapi/v1/tests/test_views.py
View file @
d769550d
...
@@ -680,6 +680,32 @@ class AnnotationSearchViewTests(BaseAnnotationViewTests):
...
@@ -680,6 +680,32 @@ class AnnotationSearchViewTests(BaseAnnotationViewTests):
search_and_verify
(
"First"
,
"First one"
,
[])
search_and_verify
(
"First"
,
"First one"
,
[])
search_and_verify
(
"Second"
,
"Second note"
,
[
"tag1"
,
"tag2"
])
search_and_verify
(
"Second"
,
"Second note"
,
[
"tag1"
,
"tag2"
])
@ddt.data
(
True
,
False
)
def
test_usage_id_search
(
self
,
is_es_disabled
):
"""
Verifies the search with usage id.
"""
self
.
_create_annotation
(
text
=
u'First one'
,
usage_id
=
'test-1'
)
self
.
_create_annotation
(
text
=
u'Second note'
,
usage_id
=
'test-2'
)
self
.
_create_annotation
(
text
=
u'Third note'
,
usage_id
=
'test-3'
)
@patch
(
'django.conf.settings.ES_DISABLED'
,
is_es_disabled
)
def
verify_usage_id_search
(
usage_ids
):
"""
Verify search results based on usage id operation.
Arguments:
usage_ids: List. The identifier string of the annotations XBlock(s).
"""
results
=
self
.
_get_search_results
(
usage_id
=
usage_ids
)
self
.
assertEqual
(
len
(
results
),
len
(
usage_ids
))
# Here we are reverse-traversing usage_ids because response has descending ordered rows.
for
index
,
u_id
in
enumerate
(
usage_ids
[::
-
1
]):
self
.
assertEqual
(
results
[
index
][
'usage_id'
],
u_id
)
verify_usage_id_search
(
usage_ids
=
[
'test-1'
])
verify_usage_id_search
(
usage_ids
=
[
'test-1'
,
'test-2'
,
'test-3'
])
def
test_search_deleted
(
self
):
def
test_search_deleted
(
self
):
"""
"""
Tests for search method to not return deleted notes.
Tests for search method to not return deleted notes.
...
...
notesapi/v1/views.py
View file @
d769550d
...
@@ -35,10 +35,12 @@ class AnnotationSearchView(GenericAPIView):
...
@@ -35,10 +35,12 @@ class AnnotationSearchView(GenericAPIView):
"""
"""
**Use Case**
**Use Case**
* Search and return a
paginated
list of annotations for a user.
* Search and return a list of annotations for a user.
The annotations are always sorted in descending order by updated date.
The annotations are always sorted in descending order by updated date.
Response is paginated by default except usage_id based search.
Each page in the list contains 25 annotations by default. The page
Each page in the list contains 25 annotations by default. The page
size can be altered by passing parameter "page_size=<page_size>".
size can be altered by passing parameter "page_size=<page_size>".
...
@@ -58,6 +60,8 @@ class AnnotationSearchView(GenericAPIView):
...
@@ -58,6 +60,8 @@ class AnnotationSearchView(GenericAPIView):
GET /api/v1/search/
GET /api/v1/search/
GET /api/v1/search/?course_id={course_id}&user={user_id}
GET /api/v1/search/?course_id={course_id}&user={user_id}
GET /api/v1/search/?course_id={course_id}&user={user_id}&usage_id={usage_id}
GET /api/v1/search/?course_id={course_id}&user={user_id}&usage_id={usage_id}&usage_id={usage_id} ...
**Query Parameters for GET**
**Query Parameters for GET**
...
@@ -67,6 +71,8 @@ class AnnotationSearchView(GenericAPIView):
...
@@ -67,6 +71,8 @@ class AnnotationSearchView(GenericAPIView):
* user: Anonymized user id.
* user: Anonymized user id.
* usage_id: The identifier string of the annotations XBlock.
* text: Student's thoughts on the quote
* text: Student's thoughts on the quote
* highlight: dict. Only used when search from ElasticSearch. It contains two keys:
* highlight: dict. Only used when search from ElasticSearch. It contains two keys:
...
@@ -109,31 +115,49 @@ class AnnotationSearchView(GenericAPIView):
...
@@ -109,31 +115,49 @@ class AnnotationSearchView(GenericAPIView):
* updated: DateTime. When was the last time annotation was updated.
* updated: DateTime. When was the last time annotation was updated.
"""
"""
params
=
{}
query_params
=
{}
search_with_usage_id
=
False
def
get
(
self
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
def
get
(
self
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
"""
"""
Search annotations in most appropriate storage
Search annotations in most appropriate storage
"""
"""
self
.
query_params
=
{}
self
.
search_with_usage_id
=
False
self
.
params
=
self
.
request
.
query_params
.
dict
()
usage_ids
=
self
.
request
.
query_params
.
getlist
(
'usage_id'
)
if
len
(
usage_ids
)
>
0
:
self
.
search_with_usage_id
=
True
self
.
query_params
[
'usage_id__in'
]
=
usage_ids
if
'course_id'
in
self
.
params
:
self
.
query_params
[
'course_id'
]
=
self
.
params
[
'course_id'
]
# search in DB when ES is not available or there is no need to bother it
# search in DB when ES is not available or there is no need to bother it
if
settings
.
ES_DISABLED
or
'text'
not
in
self
.
request
.
query_params
.
dict
():
if
settings
.
ES_DISABLED
or
'text'
not
in
self
.
params
:
if
'user'
in
self
.
params
:
self
.
query_params
[
'user_id'
]
=
self
.
params
[
'user'
]
return
self
.
get_from_db
(
*
args
,
**
kwargs
)
return
self
.
get_from_db
(
*
args
,
**
kwargs
)
else
:
else
:
if
'user'
in
self
.
params
:
self
.
query_params
[
'user'
]
=
self
.
params
[
'user'
]
return
self
.
get_from_es
(
*
args
,
**
kwargs
)
return
self
.
get_from_es
(
*
args
,
**
kwargs
)
def
get_from_db
(
self
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
def
get_from_db
(
self
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
"""
"""
Search annotations in database.
Search annotations in database.
"""
"""
params
=
self
.
request
.
query_params
.
dict
()
query
=
Note
.
objects
.
filter
(
**
self
.
query_params
)
.
order_by
(
'-updated'
)
query
=
Note
.
objects
.
filter
(
**
{
f
:
v
for
(
f
,
v
)
in
params
.
items
()
if
f
in
(
'course_id'
,
'usage_id'
)}
)
.
order_by
(
'-updated'
)
if
'
user'
in
params
:
if
'
text'
in
self
.
params
:
query
=
query
.
filter
(
user_id
=
params
[
'user'
]
)
query
=
query
.
filter
(
Q
(
text__icontains
=
self
.
params
[
'text'
])
|
Q
(
tags__icontains
=
self
.
params
[
'text'
])
)
if
'text'
in
params
:
# Do not send paginated result if usage id based search.
query
=
query
.
filter
(
Q
(
text__icontains
=
params
[
'text'
])
|
Q
(
tags__icontains
=
params
[
'text'
]))
if
self
.
search_with_usage_id
:
serializer
=
NoteSerializer
(
query
,
many
=
True
)
return
Response
(
serializer
.
data
,
status
=
status
.
HTTP_200_OK
)
page
=
self
.
paginate_queryset
(
query
)
page
=
self
.
paginate_queryset
(
query
)
serializer
=
NoteSerializer
(
page
,
many
=
True
)
serializer
=
NoteSerializer
(
page
,
many
=
True
)
...
@@ -144,16 +168,13 @@ class AnnotationSearchView(GenericAPIView):
...
@@ -144,16 +168,13 @@ class AnnotationSearchView(GenericAPIView):
"""
"""
Search annotations in ElasticSearch.
Search annotations in ElasticSearch.
"""
"""
params
=
self
.
request
.
query_params
.
dict
()
query
=
SearchQuerySet
()
.
models
(
Note
)
.
filter
(
**
self
.
query_params
)
query
=
SearchQuerySet
()
.
models
(
Note
)
.
filter
(
**
{
f
:
v
for
(
f
,
v
)
in
params
.
items
()
if
f
in
(
'user'
,
'course_id'
,
'usage_id'
)}
)
if
'text'
in
params
:
if
'text'
in
self
.
params
:
clean_text
=
query
.
query
.
clean
(
params
[
'text'
])
clean_text
=
query
.
query
.
clean
(
self
.
params
[
'text'
])
query
=
query
.
filter
(
SQ
(
data
=
clean_text
))
query
=
query
.
filter
(
SQ
(
data
=
clean_text
))
if
params
.
get
(
'highlight'
):
if
self
.
params
.
get
(
'highlight'
):
opts
=
{
opts
=
{
'pre_tags'
:
[
'{elasticsearch_highlight_start}'
],
'pre_tags'
:
[
'{elasticsearch_highlight_start}'
],
'post_tags'
:
[
'{elasticsearch_highlight_end}'
],
'post_tags'
:
[
'{elasticsearch_highlight_end}'
],
...
@@ -161,6 +182,11 @@ class AnnotationSearchView(GenericAPIView):
...
@@ -161,6 +182,11 @@ class AnnotationSearchView(GenericAPIView):
}
}
query
=
query
.
highlight
(
**
opts
)
query
=
query
.
highlight
(
**
opts
)
# Do not send paginated result if usage id based search.
if
self
.
search_with_usage_id
:
serializer
=
NotesElasticSearchSerializer
(
query
,
many
=
True
)
return
Response
(
serializer
.
data
,
status
=
status
.
HTTP_200_OK
)
page
=
self
.
paginate_queryset
(
query
)
page
=
self
.
paginate_queryset
(
query
)
serializer
=
NotesElasticSearchSerializer
(
page
,
many
=
True
)
serializer
=
NotesElasticSearchSerializer
(
page
,
many
=
True
)
response
=
self
.
get_paginated_response
(
serializer
.
data
)
response
=
self
.
get_paginated_response
(
serializer
.
data
)
...
...
notesserver/test_views.py
View file @
d769550d
import
json
import
datetime
import
datetime
from
unittest
import
skipIf
from
unittest
import
skipIf
from
mock
import
patch
,
Mock
from
mock
import
patch
,
Mock
...
@@ -17,7 +18,7 @@ class OperationalEndpointsTest(APITestCase):
...
@@ -17,7 +18,7 @@ class OperationalEndpointsTest(APITestCase):
"""
"""
response
=
self
.
client
.
get
(
reverse
(
'heartbeat'
))
response
=
self
.
client
.
get
(
reverse
(
'heartbeat'
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
data
,
{
"OK"
:
True
})
self
.
assertEquals
(
json
.
loads
(
response
.
content
)
,
{
"OK"
:
True
})
@skipIf
(
settings
.
ES_DISABLED
,
"Do not test if Elasticsearch service is disabled."
)
@skipIf
(
settings
.
ES_DISABLED
,
"Do not test if Elasticsearch service is disabled."
)
@patch
(
'notesserver.views.get_es'
)
@patch
(
'notesserver.views.get_es'
)
...
@@ -28,7 +29,7 @@ class OperationalEndpointsTest(APITestCase):
...
@@ -28,7 +29,7 @@ class OperationalEndpointsTest(APITestCase):
mocked_get_es
.
return_value
.
ping
.
return_value
=
False
mocked_get_es
.
return_value
.
ping
.
return_value
=
False
response
=
self
.
client
.
get
(
reverse
(
'heartbeat'
))
response
=
self
.
client
.
get
(
reverse
(
'heartbeat'
))
self
.
assertEquals
(
response
.
status_code
,
500
)
self
.
assertEquals
(
response
.
status_code
,
500
)
self
.
assertEquals
(
response
.
data
,
{
"OK"
:
False
,
"check"
:
"es"
})
self
.
assertEquals
(
json
.
loads
(
response
.
content
)
,
{
"OK"
:
False
,
"check"
:
"es"
})
@patch
(
"django.db.backends.utils.CursorWrapper"
)
@patch
(
"django.db.backends.utils.CursorWrapper"
)
def
test_heartbeat_failure_db
(
self
,
mocked_cursor_wrapper
):
def
test_heartbeat_failure_db
(
self
,
mocked_cursor_wrapper
):
...
@@ -38,7 +39,7 @@ class OperationalEndpointsTest(APITestCase):
...
@@ -38,7 +39,7 @@ class OperationalEndpointsTest(APITestCase):
mocked_cursor_wrapper
.
side_effect
=
Exception
mocked_cursor_wrapper
.
side_effect
=
Exception
response
=
self
.
client
.
get
(
reverse
(
'heartbeat'
))
response
=
self
.
client
.
get
(
reverse
(
'heartbeat'
))
self
.
assertEquals
(
response
.
status_code
,
500
)
self
.
assertEquals
(
response
.
status_code
,
500
)
self
.
assertEquals
(
response
.
data
,
{
"OK"
:
False
,
"check"
:
"db"
})
self
.
assertEquals
(
json
.
loads
(
response
.
content
)
,
{
"OK"
:
False
,
"check"
:
"db"
})
def
test_root
(
self
):
def
test_root
(
self
):
"""
"""
...
...
notesserver/views.py
View file @
d769550d
...
@@ -3,6 +3,8 @@ import datetime
...
@@ -3,6 +3,8 @@ import datetime
from
django.db
import
connection
from
django.db
import
connection
from
django.conf
import
settings
from
django.conf
import
settings
from
django.http
import
JsonResponse
from
rest_framework
import
status
from
rest_framework
import
status
from
rest_framework.permissions
import
AllowAny
from
rest_framework.permissions
import
AllowAny
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
...
@@ -38,12 +40,12 @@ def heartbeat(request): # pylint: disable=unused-argument
...
@@ -38,12 +40,12 @@ def heartbeat(request): # pylint: disable=unused-argument
try
:
try
:
db_status
()
db_status
()
except
Exception
:
except
Exception
:
return
Response
({
"OK"
:
False
,
"check"
:
"db"
},
status
=
status
.
HTTP_500_INTERNAL_SERVER_ERROR
)
return
Json
Response
({
"OK"
:
False
,
"check"
:
"db"
},
status
=
status
.
HTTP_500_INTERNAL_SERVER_ERROR
)
if
not
settings
.
ES_DISABLED
and
not
get_es
()
.
ping
():
if
not
settings
.
ES_DISABLED
and
not
get_es
()
.
ping
():
return
Response
({
"OK"
:
False
,
"check"
:
"es"
},
status
=
status
.
HTTP_500_INTERNAL_SERVER_ERROR
)
return
Json
Response
({
"OK"
:
False
,
"check"
:
"es"
},
status
=
status
.
HTTP_500_INTERNAL_SERVER_ERROR
)
return
Response
({
"OK"
:
True
}
)
return
JsonResponse
({
"OK"
:
True
},
status
=
status
.
HTTP_200_OK
)
@api_view
([
'GET'
])
@api_view
([
'GET'
])
...
...
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