Co-Authored-By: Jean-Michel Claus <jmc@edx.org> Co-Authored-By: Brian Talbot <btalbot@edx.org> Co-Authored-By: Tim Babych <tim@edx.org> Co-Authored-By: Oleg Marshev <oleg@edx.org> Co-Authored-By: Chris Rodriguez <crodriguez@edx.org>
331 lines
9.9 KiB
Python
331 lines
9.9 KiB
Python
"""
|
|
Stub implementation of EdxNotes for acceptance tests
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from uuid import uuid4
|
|
from datetime import datetime
|
|
from copy import deepcopy
|
|
|
|
from .http import StubHttpRequestHandler, StubHttpService
|
|
|
|
|
|
# pylint: disable=invalid-name
|
|
class StubEdxNotesServiceHandler(StubHttpRequestHandler):
|
|
"""
|
|
Handler for EdxNotes requests.
|
|
"""
|
|
URL_HANDLERS = {
|
|
"GET": {
|
|
"/api/v1/annotations$": "_collection",
|
|
"/api/v1/annotations/(?P<note_id>[0-9A-Fa-f]+)$": "_read",
|
|
"/api/v1/search$": "_search",
|
|
},
|
|
"POST": {
|
|
"/api/v1/annotations$": "_create",
|
|
"/create_notes": "_create_notes",
|
|
},
|
|
"PUT": {
|
|
"/api/v1/annotations/(?P<note_id>[0-9A-Fa-f]+)$": "_update",
|
|
"/cleanup$": "_cleanup",
|
|
},
|
|
"DELETE": {
|
|
"/api/v1/annotations/(?P<note_id>[0-9A-Fa-f]+)$": "_delete",
|
|
},
|
|
}
|
|
|
|
def _match_pattern(self, pattern_handlers):
|
|
"""
|
|
Finds handler by the provided handler patterns and delegate response to
|
|
the matched handler.
|
|
"""
|
|
for pattern in pattern_handlers:
|
|
match = re.match(pattern, self.path_only)
|
|
if match:
|
|
handler = getattr(self, pattern_handlers[pattern], None)
|
|
if handler:
|
|
handler(**match.groupdict())
|
|
return True
|
|
return None
|
|
|
|
def _send_handler_response(self, method):
|
|
"""
|
|
Delegate response to handler methods.
|
|
If no handler defined, send a 404 response.
|
|
"""
|
|
# Choose the list of handlers based on the HTTP method
|
|
if method in self.URL_HANDLERS:
|
|
handlers_list = self.URL_HANDLERS[method]
|
|
else:
|
|
self.log_error("Unrecognized method '{method}'".format(method=method))
|
|
return
|
|
|
|
# Check the path (without querystring params) against our list of handlers
|
|
if self._match_pattern(handlers_list):
|
|
return
|
|
# If we don't have a handler for this URL and/or HTTP method,
|
|
# respond with a 404.
|
|
else:
|
|
self.send_response(404, content="404 Not Found")
|
|
|
|
def do_GET(self):
|
|
"""
|
|
Handle GET methods to the EdxNotes API stub.
|
|
"""
|
|
self._send_handler_response("GET")
|
|
|
|
def do_POST(self):
|
|
"""
|
|
Handle POST methods to the EdxNotes API stub.
|
|
"""
|
|
self._send_handler_response("POST")
|
|
|
|
def do_PUT(self):
|
|
"""
|
|
Handle PUT methods to the EdxNotes API stub.
|
|
"""
|
|
if self.path.startswith("/set_config"):
|
|
return StubHttpRequestHandler.do_PUT(self)
|
|
|
|
self._send_handler_response("PUT")
|
|
|
|
def do_DELETE(self):
|
|
"""
|
|
Handle DELETE methods to the EdxNotes API stub.
|
|
"""
|
|
self._send_handler_response("DELETE")
|
|
|
|
def do_OPTIONS(self):
|
|
"""
|
|
Handle OPTIONS methods to the EdxNotes API stub.
|
|
"""
|
|
self.send_response(200, headers={
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
"Access-Control-Allow-Headers": "Content-Length, Content-Type, X-Annotator-Auth-Token, X-Requested-With, X-Annotator-Auth-Token, X-Requested-With, X-CSRFToken",
|
|
})
|
|
|
|
def respond(self, status_code=200, content=None):
|
|
"""
|
|
Send a response back to the client with the HTTP `status_code` (int),
|
|
the given content serialized as JSON (str), and the headers set appropriately.
|
|
"""
|
|
headers = {
|
|
"Access-Control-Allow-Origin": "*",
|
|
}
|
|
if status_code < 400 and content:
|
|
headers["Content-Type"] = "application/json"
|
|
content = json.dumps(content)
|
|
else:
|
|
headers["Content-Type"] = "text/html"
|
|
|
|
self.send_response(status_code, content, headers)
|
|
|
|
def _create(self):
|
|
"""
|
|
Create a note, assign id, annotator_schema_version, created and updated dates.
|
|
"""
|
|
note = json.loads(self.request_content)
|
|
note.update({
|
|
"id": uuid4().hex,
|
|
"annotator_schema_version": "v1.0",
|
|
"created": datetime.utcnow().isoformat(),
|
|
"updated": datetime.utcnow().isoformat(),
|
|
})
|
|
self.server.add_notes(note)
|
|
self.respond(content=note)
|
|
|
|
def _create_notes(self):
|
|
"""
|
|
The same as self._create, but it works a list of notes.
|
|
"""
|
|
try:
|
|
notes = json.loads(self.request_content)
|
|
except ValueError:
|
|
self.respond(400, "Bad Request")
|
|
return
|
|
|
|
if not isinstance(notes, list):
|
|
self.respond(400, "Bad Request")
|
|
return
|
|
|
|
for note in notes:
|
|
note.update({
|
|
"id": uuid4().hex,
|
|
"annotator_schema_version": "v1.0",
|
|
"created": note["created"] if note.get("created") else datetime.utcnow().isoformat(),
|
|
"updated": note["updated"] if note.get("updated") else datetime.utcnow().isoformat(),
|
|
})
|
|
self.server.add_notes(note)
|
|
|
|
self.respond(content=notes)
|
|
|
|
def _read(self, note_id):
|
|
"""
|
|
Return the note by note id.
|
|
"""
|
|
notes = self.server.get_notes()
|
|
result = self.server.filter_by_id(notes, note_id)
|
|
if result:
|
|
self.respond(content=result[0])
|
|
else:
|
|
self.respond(404, "404 Not Found")
|
|
|
|
def _update(self, note_id):
|
|
"""
|
|
Update the note by note id.
|
|
"""
|
|
note = self.server.update_note(note_id, json.loads(self.request_content))
|
|
if note:
|
|
self.respond(content=note)
|
|
else:
|
|
self.respond(404, "404 Not Found")
|
|
|
|
def _delete(self, note_id):
|
|
"""
|
|
Delete the note by note id.
|
|
"""
|
|
if self.server.delete_note(note_id):
|
|
self.respond(204, "No Content")
|
|
else:
|
|
self.respond(404, "404 Not Found")
|
|
|
|
def _search(self):
|
|
"""
|
|
Search for a notes by user id, course_id and usage_id.
|
|
"""
|
|
user = self.get_params.get("user", None)
|
|
usage_id = self.get_params.get("usage_id", None)
|
|
course_id = self.get_params.get("course_id", None)
|
|
text = self.get_params.get("text", None)
|
|
|
|
if user is None:
|
|
self.respond(400, "Bad Request")
|
|
return
|
|
|
|
notes = self.server.get_notes()
|
|
if course_id is not None:
|
|
notes = self.server.filter_by_course_id(notes, course_id)
|
|
if usage_id is not None:
|
|
notes = self.server.filter_by_usage_id(notes, usage_id)
|
|
if text:
|
|
notes = self.server.search(notes, text)
|
|
self.respond(content={
|
|
"total": len(notes),
|
|
"rows": notes,
|
|
})
|
|
|
|
def _collection(self):
|
|
"""
|
|
Return all notes for the user.
|
|
"""
|
|
user = self.get_params.get("user", None)
|
|
if user is None:
|
|
self.send_response(400, content="Bad Request")
|
|
return
|
|
notes = self.server.get_notes()
|
|
self.respond(content=notes)
|
|
|
|
def _cleanup(self):
|
|
"""
|
|
Helper method that removes all notes to the stub EdxNotes service.
|
|
"""
|
|
self.server.cleanup()
|
|
self.respond()
|
|
|
|
|
|
class StubEdxNotesService(StubHttpService):
|
|
"""
|
|
Stub EdxNotes service.
|
|
"""
|
|
HANDLER_CLASS = StubEdxNotesServiceHandler
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(StubEdxNotesService, self).__init__(*args, **kwargs)
|
|
self.notes = list()
|
|
|
|
def get_notes(self):
|
|
"""
|
|
Returns a list of all notes.
|
|
"""
|
|
notes = deepcopy(self.notes)
|
|
notes.reverse()
|
|
return notes
|
|
|
|
def add_notes(self, notes):
|
|
"""
|
|
Adds `notes(list)` to the stub EdxNotes service.
|
|
"""
|
|
if not isinstance(notes, list):
|
|
notes = [notes]
|
|
|
|
for note in notes:
|
|
self.notes.append(note)
|
|
|
|
def update_note(self, note_id, note_info):
|
|
"""
|
|
Updates the note with `note_id(str)` by the `note_info(dict)` to the
|
|
stub EdxNotes service.
|
|
"""
|
|
note = self.filter_by_id(self.notes, note_id)
|
|
if note:
|
|
note[0].update(note_info)
|
|
return note
|
|
else:
|
|
return None
|
|
|
|
def delete_note(self, note_id):
|
|
"""
|
|
Removes the note with `note_id(str)` to the stub EdxNotes service.
|
|
"""
|
|
note = self.filter_by_id(self.notes, note_id)
|
|
if note:
|
|
index = self.notes.index(note[0])
|
|
self.notes.pop(index)
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def cleanup(self):
|
|
"""
|
|
Removes all notes to the stub EdxNotes service.
|
|
"""
|
|
self.notes = list()
|
|
|
|
def filter_by_id(self, data, note_id):
|
|
"""
|
|
Filters provided `data(list)` by the `note_id(str)`.
|
|
"""
|
|
return self.filter_by(data, "id", note_id)
|
|
|
|
def filter_by_user(self, data, user):
|
|
"""
|
|
Filters provided `data(list)` by the `user(str)`.
|
|
"""
|
|
return self.filter_by(data, "user", user)
|
|
|
|
def filter_by_usage_id(self, data, usage_id):
|
|
"""
|
|
Filters provided `data(list)` by the `usage_id(str)`.
|
|
"""
|
|
return self.filter_by(data, "usage_id", usage_id)
|
|
|
|
def filter_by_course_id(self, data, course_id):
|
|
"""
|
|
Filters provided `data(list)` by the `course_id(str)`.
|
|
"""
|
|
return self.filter_by(data, "course_id", course_id)
|
|
|
|
def filter_by(self, data, field_name, value):
|
|
"""
|
|
Filters provided `data(list)` by the `field_name(str)` with `value`.
|
|
"""
|
|
return [note for note in data if note.get(field_name) == value]
|
|
|
|
def search(self, data, query):
|
|
"""
|
|
Search the `query(str)` text in the provided `data(list)`.
|
|
"""
|
|
return [note for note in data if unicode(query).strip() in note.get("text", "").split()]
|