Commit e2333214 by muhammad-ammar Committed by muzaffaryousaf

paginate edxnotes views

TNL-3840
parent c88697ca
""" """
Helper methods related to EdxNotes. Helper methods related to EdxNotes.
""" """
import json import json
import logging import logging
from json import JSONEncoder from json import JSONEncoder
from uuid import uuid4 from uuid import uuid4
import urlparse
from urllib import urlencode
import requests import requests
from datetime import datetime from datetime import datetime
...@@ -34,6 +35,8 @@ HIGHLIGHT_TAG = "span" ...@@ -34,6 +35,8 @@ HIGHLIGHT_TAG = "span"
HIGHLIGHT_CLASS = "note-highlight" HIGHLIGHT_CLASS = "note-highlight"
# OAuth2 Client name for edxnotes # OAuth2 Client name for edxnotes
CLIENT_NAME = "edx-notes" CLIENT_NAME = "edx-notes"
DEFAULT_PAGE = 1
DEFAULT_PAGE_SIZE = 10
class NoteJSONEncoder(JSONEncoder): class NoteJSONEncoder(JSONEncoder):
...@@ -63,19 +66,32 @@ def get_token_url(course_id): ...@@ -63,19 +66,32 @@ def get_token_url(course_id):
}) })
def send_request(user, course_id, path="", query_string=None): def send_request(user, course_id, page, page_size, path="", text=None):
""" """
Sends a request with appropriate parameters and headers. Sends a request to notes api with appropriate parameters and headers.
Arguments:
user: Current logged in user
course_id: Course id
page: requested or default page number
page_size: requested or default page size
path: `search` or `annotations`. This is used to calculate notes api endpoint.
text: text to search.
Returns:
Response received from notes api
""" """
url = get_internal_endpoint(path) url = get_internal_endpoint(path)
params = { params = {
"user": anonymous_id_for_user(user, None), "user": anonymous_id_for_user(user, None),
"course_id": unicode(course_id).encode("utf-8"), "course_id": unicode(course_id).encode("utf-8"),
"page": page,
"page_size": page_size,
} }
if query_string: if text:
params.update({ params.update({
"text": query_string, "text": text,
"highlight": True, "highlight": True,
"highlight_tag": HIGHLIGHT_TAG, "highlight_tag": HIGHLIGHT_TAG,
"highlight_class": HIGHLIGHT_CLASS, "highlight_class": HIGHLIGHT_CLASS,
...@@ -239,39 +255,89 @@ def get_index(usage_key, children): ...@@ -239,39 +255,89 @@ def get_index(usage_key, children):
return children.index(usage_key) return children.index(usage_key)
def search(user, course, query_string): def construct_pagination_urls(request, course_id, api_next_url, api_previous_url):
""" """
Returns search results for the `query_string(str)`. Construct next and previous urls for LMS. `api_next_url` and `api_previous_url`
are returned from notes api but we need to transform them according to LMS notes
views by removing and replacing extra information.
Arguments:
request: HTTP request object
course_id: course id
api_next_url: notes api next url
api_previous_url: notes api previous url
Returns:
next_url: lms notes next url
previous_url: lms notes previous url
""" """
response = send_request(user, course.id, "search", query_string) def lms_url(url):
try: """
content = json.loads(response.content) Create lms url from api url.
collection = content["rows"] """
except (ValueError, KeyError): if url is None:
log.warning("invalid JSON: %s", response.content) return None
raise EdxNotesParseError(_("Server error. Please try again in a few minutes."))
content.update({
"rows": preprocess_collection(user, course, collection)
})
return json.dumps(content, cls=NoteJSONEncoder) keys = ('page', 'page_size', 'text')
parsed = urlparse.urlparse(url)
query_params = urlparse.parse_qs(parsed.query)
encoded_query_params = urlencode({key: query_params.get(key)[0] for key in keys if key in query_params})
return "{}?{}".format(request.build_absolute_uri(base_url), encoded_query_params)
def get_notes(user, course): base_url = reverse("notes", kwargs={"course_id": course_id})
next_url = lms_url(api_next_url)
previous_url = lms_url(api_previous_url)
return next_url, previous_url
def get_notes(request, course, page=DEFAULT_PAGE, page_size=DEFAULT_PAGE_SIZE, text=None):
""" """
Returns all notes for the user. Returns paginated list of notes for the user.
Arguments:
request: HTTP request object
course: Course descriptor
page: requested or default page number
page_size: requested or default page size
text: text to search. If None then return all results for the current logged in user.
Returns:
Paginated dictionary with these key:
start: start of the current page
current_page: current page number
next: url for next page
previous: url for previous page
count: total number of notes available for the sent query
num_pages: number of pages available
results: list with notes info dictionary. each item in this list will be a dict
""" """
response = send_request(user, course.id, "annotations") path = 'search' if text else 'annotations'
response = send_request(request.user, course.id, page, page_size, path, text)
try: try:
collection = json.loads(response.content) collection = json.loads(response.content)
except ValueError: except ValueError:
return None raise EdxNotesParseError(_("Invalid response received from notes api."))
if not collection: # Verify response dict structure
return None expected_keys = ['count', 'results', 'num_pages', 'start', 'next', 'previous', 'current_page']
keys = collection.keys()
if not keys or not all(key in expected_keys for key in keys):
raise EdxNotesParseError(_("Invalid response received from notes api."))
filtered_results = preprocess_collection(request.user, course, collection['results'])
collection['results'] = filtered_results
collection['next'], collection['previous'] = construct_pagination_urls(
request,
course.id,
collection['next'],
collection['previous']
)
return json.dumps(preprocess_collection(user, course, collection), cls=NoteJSONEncoder) return json.dumps(collection, cls=NoteJSONEncoder)
def get_endpoint(api_url, path=""): def get_endpoint(api_url, path=""):
......
...@@ -7,7 +7,7 @@ from django.conf.urls import patterns, url ...@@ -7,7 +7,7 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( urlpatterns = patterns(
"edxnotes.views", "edxnotes.views",
url(r"^/$", "edxnotes", name="edxnotes"), url(r"^/$", "edxnotes", name="edxnotes"),
url(r"^/search/$", "search_notes", name="search_notes"), url(r"^/notes/$", "notes", name="notes"),
url(r"^/token/$", "get_token", name="get_token"), url(r"^/token/$", "get_token", name="get_token"),
url(r"^/visibility/$", "edxnotes_visibility", name="edxnotes_visibility"), url(r"^/visibility/$", "edxnotes_visibility", name="edxnotes_visibility"),
) )
...@@ -7,6 +7,7 @@ from django.contrib.auth.decorators import login_required ...@@ -7,6 +7,7 @@ from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, HttpResponseBadRequest, Http404 from django.http import HttpResponse, HttpResponseBadRequest, Http404
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.views.decorators.http import require_GET
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
...@@ -18,8 +19,9 @@ from edxnotes.helpers import ( ...@@ -18,8 +19,9 @@ from edxnotes.helpers import (
get_edxnotes_id_token, get_edxnotes_id_token,
get_notes, get_notes,
is_feature_enabled, is_feature_enabled,
search,
get_course_position, get_course_position,
DEFAULT_PAGE,
DEFAULT_PAGE_SIZE,
) )
...@@ -30,6 +32,13 @@ log = logging.getLogger(__name__) ...@@ -30,6 +32,13 @@ log = logging.getLogger(__name__)
def edxnotes(request, course_id): def edxnotes(request, course_id):
""" """
Displays the EdxNotes page. Displays the EdxNotes page.
Arguments:
request: HTTP request object
course_id: course id
Returns:
Rendered HTTP response.
""" """
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, "load", course_key) course = get_course_with_access(request.user, "load", course_key)
...@@ -38,19 +47,19 @@ def edxnotes(request, course_id): ...@@ -38,19 +47,19 @@ def edxnotes(request, course_id):
raise Http404 raise Http404
try: try:
notes = get_notes(request.user, course) notes_info = get_notes(request, course)
except EdxNotesServiceUnavailable: except (EdxNotesParseError, EdxNotesServiceUnavailable) as err:
raise Http404 return JsonResponseBadRequest({"error": err.message}, status=500)
context = { context = {
"course": course, "course": course,
"search_endpoint": reverse("search_notes", kwargs={"course_id": course_id}), "notes_endpoint": reverse("notes", kwargs={"course_id": course_id}),
"notes": notes, "notes": notes_info,
"debug": json.dumps(settings.DEBUG), "debug": json.dumps(settings.DEBUG),
'position': None, 'position': None,
} }
if not notes: if len(json.loads(notes_info)['results']) == 0:
field_data_cache = FieldDataCache.cache_for_descriptor_descendents( field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course.id, request.user, course, depth=2 course.id, request.user, course, depth=2
) )
...@@ -66,27 +75,97 @@ def edxnotes(request, course_id): ...@@ -66,27 +75,97 @@ def edxnotes(request, course_id):
return render_to_response("edxnotes/edxnotes.html", context) return render_to_response("edxnotes/edxnotes.html", context)
@require_GET
@login_required @login_required
def search_notes(request, course_id): def notes(request, course_id):
""" """
Handles search requests. Notes view to handle list and search requests.
Query parameters:
page: page number to get
page_size: number of items in the page
text: text string to search. If `text` param is missing then get all the
notes for the current user for this course else get only those notes
which contain the `text` value.
Arguments:
request: HTTP request object
course_id: course id
Returns:
Paginated response as JSON. A sample response is below.
{
"count": 101,
"num_pages": 11,
"current_page": 1,
"results": [
{
"chapter": {
"index": 4,
"display_name": "About Exams and Certificates",
"location": "i4x://org/course/category/name@revision",
"children": [
"i4x://org/course/category/name@revision"
]
},
"updated": "Dec 09, 2015 at 09:31 UTC",
"tags": ["shadow","oil"],
"quote": "foo bar baz",
"section": {
"display_name": "edX Exams",
"location": "i4x://org/course/category/name@revision",
"children": [
"i4x://org/course/category/name@revision",
"i4x://org/course/category/name@revision",
]
},
"created": "2015-12-09T09:31:17.338305Z",
"ranges": [
{
"start": "/div[1]/p[1]",
"end": "/div[1]/p[1]",
"startOffset": 0,
"endOffset": 6
}
],
"user": "50cf92f9a3d8489df95e583549b919df",
"text": "first angry height hungry structure",
"course_id": "edx/DemoX/Demo",
"id": "1231",
"unit": {
"url": "/courses/edx%2FDemoX%2FDemo/courseware/1414ffd5143b4b508f739b563ab468b7/workflow/1",
"display_name": "EdX Exams",
"location": "i4x://org/course/category/name@revision"
},
"usage_id": "i4x://org/course/category/name@revision"
} ],
"next": "http://0.0.0.0:8000/courses/edx%2FDemoX%2FDemo/edxnotes/notes/?page=2&page_size=10",
"start": 0,
"previous": null
}
""" """
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, "load", course_key) course = get_course_with_access(request.user, 'load', course_key)
if not is_feature_enabled(course): if not is_feature_enabled(course):
raise Http404 raise Http404
if "text" not in request.GET: page = request.GET.get('page') or DEFAULT_PAGE
return HttpResponseBadRequest() page_size = request.GET.get('page_size') or DEFAULT_PAGE_SIZE
text = request.GET.get('text')
query_string = request.GET["text"]
try: try:
search_results = search(request.user, course, query_string) notes_info = get_notes(
request,
course,
page=page,
page_size=page_size,
text=text
)
except (EdxNotesParseError, EdxNotesServiceUnavailable) as err: except (EdxNotesParseError, EdxNotesServiceUnavailable) as err:
return JsonResponseBadRequest({"error": err.message}, status=500) return JsonResponseBadRequest({"error": err.message}, status=500)
return HttpResponse(search_results) return HttpResponse(notes_info, content_type="application/json")
# pylint: disable=unused-argument # pylint: disable=unused-argument
......
...@@ -26,7 +26,7 @@ import json ...@@ -26,7 +26,7 @@ import json
% if notes: % if notes:
<div class="wrapper-notes-search"> <div class="wrapper-notes-search">
<form role="search" action="${search_endpoint}" method="GET" id="search-notes-form" class="is-hidden"> <form role="search" action="${notes_endpoint}" method="GET" id="search-notes-form" class="is-hidden">
<label for="search-notes-input" class="sr">${_('Search notes for:')}</label> <label for="search-notes-input" class="sr">${_('Search notes for:')}</label>
<input type="search" class="search-notes-input" id="search-notes-input" name="note" placeholder="${_('Search notes for...')}" required> <input type="search" class="search-notes-input" id="search-notes-input" name="note" placeholder="${_('Search notes for...')}" required>
<button type="submit" class="search-notes-submit"> <button type="submit" class="search-notes-submit">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment