diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2519ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +xblock_google_drive.egg-info +*.log +*.pyc +*~ diff --git a/.pep8 b/.pep8 new file mode 100644 index 0000000..f32cb5a --- /dev/null +++ b/.pep8 @@ -0,0 +1,4 @@ +[pep8] +ignore=E501 +max_line_length=119 +exclude=settings diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..822285f --- /dev/null +++ b/.pylintrc @@ -0,0 +1,84 @@ +[MASTER] + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=migrations + + +[MESSAGES CONTROL] + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# I0011 locally-disabled (module-level pylint overrides) +# disabling ```duplicate-code``` due to 5 lines that are the same near the top of +# both xblock files - of course these are similar because it is a core pattern to +# use - if we separated out into a separate file, we'd have the 5 lines in a separate +# file and have 2 import statements within each file to get back the stuff - pretty +# useless warning in this specific case +disable=I0011,W0232,duplicate-code + + +[REPORTS] + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_|dummy + + +[TYPECHECK] + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + + +[BASIC] + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{2,50}$ + + +[SIMILARITIES] + +# Ignore imports when computing similarities. +ignore-imports=no + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=120 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,string,TERMIOS,Bastion,rexec diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..57058d1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: python + +python: + - "2.7" + +before_install: + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" + +install: + - "pip install -r requirements.txt" + - "pip uninstall -y xblock-google-drive && python setup.py sdist && pip install dist/xblock-google-drive-0.1.tar.gz" + +script: + - DJANGO_SETTINGS_MODULE="settings" nosetests --with-coverage --cover-package="google_drive" --with-django + - pep8 --config=.pep8 google_drive + - pylint --rcfile=.pylintrc google_drive --report=no + +after_success: coveralls diff --git a/README.md b/README.md deleted file mode 100644 index fb0ca8d..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -xblock-google-drive -===================== diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e8f7efe --- /dev/null +++ b/README.rst @@ -0,0 +1,155 @@ +Google Drive & Calendar XBlock |Build Status| +--------------------------------------------- + +This XBlock allows embedding of Google documents and calendar, within an +edX course. + +Installation +------------ + +Install the requirements into the python virtual environment of your +``edx-platform`` installation by running the following command from the +root folder: + +.. code:: bash + + $ pip install -r requirements.txt + +Enabling in Studio +------------------ + +You can enable the Google Drive & Calendar XBlock in studio through the +advanced settings. + +1. From the main page of a specific course, navigate to + ``Settings -> Advanced Settings`` from the top menu. +2. Check for the ``advanced_modules`` policy key, and add + ``"google-document"`` and ``"google-calendar"`` to the policy value + list. +3. Click the "Save changes" button. + +Workbench installation and settings +----------------------------------- + +Install to the workbench's virtualenv by running the following command +form the google-drive repo root: + +.. code:: bash + + pip install -r requirements.txt + +Running the workbench +--------------------- + +.. code:: bash + + $ ./manage.py runserver 8000 + +Access it at `http://localhost:8000/ `__. + +Running tests +------------- + +From google-drive directory, run the tests with the following command: + +.. code:: bash + + $ DJANGO_SETTINGS_MODULE="settings" nosetests --with-django tests/* + +If you want to run only the integration or the unit tests, append the +directory to the command. You can also run separate modules in this +manner. + +.. code:: bash + + $ DJANGO_SETTINGS_MODULE="settings" nosetests --with-django tests/unit + +To see the coverage, run the tests using the following command: + +.. code:: bash + + $ DJANGO_SETTINGS_MODULE="settings" nosetests --with-coverage --cover-package="google_drive" --with-django + +If you have not installed the xblock-sdk in the active virtualenv, you +might also have to prepend ``PYTHONPATH=".:/path/to/xblock"`` to the +command above. (``/path/to/xblock`` is the path to the xblock-sdk, where +the workbench resides). + +Changes to be documented +------------------------ + +1. Calendar width is set to 100% of parent element's width; this + optimizes the display of google document content within the LMS user + interface +2. Max width of Google images is set to 100% to prevent images from + overflowing outside the parent element's boundaries +3. Since Google WordProcessing documents and Spreadsheets don't allow + users to explicitly define width and height, their width is set to + 100%. Also, min height is set to 450px, so that documents and/or + spreadsheets with larger number of rows are displayed in their + natural size. Overflow scroll is automatically turned on when the + height of the document becomes larger than the height of the parent. + +Validation +---------- + +Each time a character is added to or removed from Google Calendar ID, +validation takes place. Analogically, validation takes place for +embedded code of Google Drive File. + +1. Google calendar IDs are being validated against a regular expression. + IDs must contain at least one '@' character, with at least one + character on each side of it, ie. 'a@a'. + +2. Embedded code of Google Drive file is being validated on the server + side, by checking the status code of the HTTP response. Since error + status codes start with 400, it's assumed that each status code + that's larger than or equal to 400 states that file is invalid. If + for any reason exception occurs while getting an HTTP response, error + code is returned, thus overriding default signalization that is + invoked by edx platform when the 500 status code is reported. + +Accessibility (a11y) +-------------------- + +For users with a visual impairment: + +1. Iframes in which Google calendars and Google Drive files (except + images) are shown now have title attribute with alternative text + content which describes what the iframe contains. +2. Images have alt attribute which contains alternative text that has + the same purpose as the title attribute of an iframe has + +Analytics +--------- + +For analytics purposes, each time an image or iframe containing a +calendar or Google Drive file is loaded, an event will be triggered. + +There are two types of events: + +1. edx.googlecomponent.calendar.displayed (if an iframe containing a + Google calendar is loaded) +2. edx.googlecomponent.document.displayed (if an image or an iframe + containing a Google Drive File is loaded) + +License +------- + +The Google Drive & Calendar XBlocks are available under the GNU Affero +General Public License (AGPLv3). + +Installation Troubleshooting +---------------------------- + +On a Mac, some people have received errors when installing lxml, trying +to find a specific header file for the compiler + +Try the following if you encounter a problem: + +:: + + CPATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/usr/include/libxml2 CFLAGS=-Qunused-arguments CPPFLAGS=-Qunused-arguments pip install lxml + +.. |Build Status| image:: https://travis-ci.org/edx-solutions/xblock-google-drive.svg?branch=master + :target: https://travis-ci.org/edx-solutions/xblock-google-drive diff --git a/google_drive/__init__.py b/google_drive/__init__.py index b2456b7..6fa04c4 100644 --- a/google_drive/__init__.py +++ b/google_drive/__init__.py @@ -1,2 +1,5 @@ +""" +Google drive XBlocks +""" from .google_docs import GoogleDocumentBlock -from .google_calendar import GoogleCalendarBlock \ No newline at end of file +from .google_calendar import GoogleCalendarBlock diff --git a/google_drive/google_calendar.py b/google_drive/google_calendar.py index 6257d04..1c83b56 100644 --- a/google_drive/google_calendar.py +++ b/google_drive/google_calendar.py @@ -1,33 +1,30 @@ +""" Google Calendar XBlock implementation """ # -*- coding: utf-8 -*- # # Imports ########################################################### - -import pkg_resources -#import logging -import textwrap -#import json -#import webob -#from lxml import etree -#from xml.etree import ElementTree as ET +import logging from xblock.core import XBlock -from xblock.fields import Scope, String +from xblock.fields import Scope, String, Integer from xblock.fragment import Fragment -# from StringIO import StringIO - -from .utils import render_template, AttrDict, load_resource - +from xblockutils.publish_event import PublishEventMixin +from xblockutils.resources import ResourceLoader -# Globals ########################################################### +LOG = logging.getLogger(__name__) +RESOURCE_LOADER = ResourceLoader(__name__) -#log = logging.getLogger(__name__) +# Constants ########################################################### +DEFAULT_CALENDAR_ID = "edx.org_lom804qe3ttspplj1bgeu1l3ak@group.calendar.google.com" +DEFAULT_CALENDAR_URL = ( + 'https://www.google.com/calendar/embed?mode=Month&src={}&showCalendars=0'.format(DEFAULT_CALENDAR_ID)) +CALENDAR_TEMPLATE = "/templates/html/google_calendar.html" +CALENDAR_EDIT_TEMPLATE = "/templates/html/google_calendar_edit.html" # Classes ########################################################### - -class GoogleCalendarBlock(XBlock): +class GoogleCalendarBlock(XBlock, PublishEventMixin): # pylint: disable=too-many-ancestors """ XBlock providing a google calendar view for a specific calendar """ @@ -35,62 +32,89 @@ class GoogleCalendarBlock(XBlock): display_name="Display Name", help="This name appears in the horizontal navigation at the top of the page.", scope=Scope.settings, - default="Example Google Calendar" + default="Google Calendar" ) - - embed_code = String( - display_name="Embed Code", - help="Google provides an embed code for Drive calendars with a variety of settings. From inside a Google Drive calendar, select Publish to the Web from within the File menu to get to your Embed Code with your desired settings.", + calendar_id = String( + display_name="Public Calendar ID", + help=( + "Google provides an ID for publicly available calendars. In the Google Calendar, " + "open Settings and copy the ID from the Calendar Address section into this field." + ), scope=Scope.settings, - default=textwrap.dedent(""" - - """)) - - def student_view(self, context={}): + default=DEFAULT_CALENDAR_ID + ) + default_view = Integer( + display_name="Default View", + help="The calendar view that students see by default. A student can change this view.", + scope=Scope.settings, + default=1 + ) + views = [(0, 'Week'), (1, 'Month'), (2, 'Agenda')] + + # Context argument is specified for xblocks, but we are not using herein + def student_view(self, context): # pylint: disable=unused-argument """ Player view, displayed to the student """ - fragment = Fragment() - context.update({ - "self": self, - }) - fragment.add_content(render_template('/templates/html/google_docs.html', context)) - fragment.add_css(load_resource('public/css/google_docs.css')) - fragment.add_javascript(load_resource('public/js/google_docs.js')) - fragment.initialize_js('GoogleDocumentBlock') + fragment.add_content(RESOURCE_LOADER.render_template(CALENDAR_TEMPLATE, { + "mode": self.views[self.default_view][1], + "src": self.calendar_id, + "title": self.display_name, + })) + fragment.add_css(RESOURCE_LOADER.load_unicode('public/css/google_calendar.css')) + fragment.add_javascript(RESOURCE_LOADER.load_unicode('public/js/google_calendar.js')) + + fragment.initialize_js('GoogleCalendarBlock') return fragment - def studio_view(self, context): + # Context argument is specified for xblocks, but we are not using herein + def studio_view(self, context): # pylint: disable=unused-argument """ Editing view in Studio """ fragment = Fragment() - fragment.add_content(render_template('/templates/html/google_docs_edit.html', { + # Need to access protected members of fields to get their default value + fragment.add_content(RESOURCE_LOADER.render_template(CALENDAR_EDIT_TEMPLATE, { 'self': self, + 'defaultName': self.fields['display_name']._default, # pylint: disable=protected-access + 'defaultID': self.fields['calendar_id']._default # pylint: disable=protected-access })) - fragment.add_javascript(load_resource('public/js/google_docs_edit.js')) + fragment.add_javascript(RESOURCE_LOADER.load_unicode('public/js/google_calendar_edit.js')) + fragment.add_css(RESOURCE_LOADER.load_unicode('public/css/google_edit.css')) - fragment.initialize_js('GoogleDocumentEditBlock') + fragment.initialize_js('GoogleCalendarEditBlock') return fragment + # suffix argument is specified for xblocks, but we are not using herein @XBlock.json_handler - def studio_submit(self, submissions, suffix=''): - - self.display_name = submissions['display_name'] - self.embed_code = submissions['embed_code'] + def studio_submit(self, submissions, suffix=''): # pylint: disable=unused-argument + """ + Change the settings for this XBlock given by the Studio user + """ + if not isinstance(submissions, dict): + LOG.error("submissions object from Studio is not a dict - %r", submissions) + return { + 'result': 'error' + } + + if 'display_name' in submissions: + self.display_name = submissions['display_name'] + if 'calendar_id' in submissions: + self.calendar_id = submissions['calendar_id'] + if 'default_view' in submissions: + self.default_view = submissions['default_view'] return { 'result': 'success', } + + @staticmethod + def workbench_scenarios(): + """ + A canned scenario for display in the workbench. + """ + return [("Google Calendar scenario", "")] diff --git a/google_drive/google_docs.py b/google_drive/google_docs.py index 71948f1..92d03c9 100644 --- a/google_drive/google_docs.py +++ b/google_drive/google_docs.py @@ -1,33 +1,44 @@ +""" Google Document XBlock implementation """ # -*- coding: utf-8 -*- # # Imports ########################################################### - -import pkg_resources -#import logging +import logging import textwrap -#import json -#import webob -#from lxml import etree -#from xml.etree import ElementTree as ET +import requests from xblock.core import XBlock from xblock.fields import Scope, String from xblock.fragment import Fragment -# from StringIO import StringIO - -from .utils import render_template, AttrDict, load_resource - - -# Globals ########################################################### - -#log = logging.getLogger(__name__) +from xblockutils.publish_event import PublishEventMixin +from xblockutils.resources import ResourceLoader + +LOG = logging.getLogger(__name__) +RESOURCE_LOADER = ResourceLoader(__name__) + +# Constants ########################################################### +DEFAULT_DOCUMENT_URL = ( + 'https://docs.google.com/presentation/d/1x2ZuzqHsMoh1epK8VsGAlanSo7r9z55ualwQlj-ofBQ/embed?' + 'start=true&loop=true&delayms=10000' +) +DEFAULT_EMBED_CODE = textwrap.dedent(""" + +""") .format(DEFAULT_DOCUMENT_URL) +DOCUMENT_TEMPLATE = "/templates/html/google_docs.html" +DOCUMENT_EDIT_TEMPLATE = "/templates/html/google_docs_edit.html" # Classes ########################################################### - -class GoogleDocumentBlock(XBlock): +class GoogleDocumentBlock(XBlock, PublishEventMixin): # pylint: disable=too-many-ancestors """ XBlock providing a google document embed link """ @@ -37,60 +48,109 @@ class GoogleDocumentBlock(XBlock): scope=Scope.settings, default="Google Document" ) - embed_code = String( display_name="Embed Code", - help="Google provides an embed code for Drive documents with a variety of settings. From inside a Google Drive document, select Publish to the Web from within the File menuto get to your Embed Code with your desired settings.", + help=( + "Google provides an embed code for Drive documents. In the Google Drive document, " + "from the File menu, select Publish to the Web. Modify settings as needed, click " + "Publish, and copy the embed code into this field." + ), scope=Scope.settings, - default=textwrap.dedent(""" - - """)) - - def student_view(self, context={}): + default=DEFAULT_EMBED_CODE + ) + alt_text = String( + display_name="Alternative Text", + help="Alternative text describes an image and appears if the image is unavailable.", + scope=Scope.settings, + default="" + ) + + # Context argument is specified for xblocks, but we are not using herein + def student_view(self, context): # pylint: disable=unused-argument """ Player view, displayed to the student """ - fragment = Fragment() - context.update({ - "self": self, - }) - fragment.add_content(render_template('/templates/html/google_docs.html', context)) - fragment.add_css(load_resource('public/css/google_docs.css')) - fragment.add_javascript(load_resource('public/js/google_docs.js')) + + fragment.add_content(RESOURCE_LOADER.render_template(DOCUMENT_TEMPLATE, {"self": self})) + fragment.add_css(RESOURCE_LOADER.load_unicode('public/css/google_docs.css')) + fragment.add_javascript(RESOURCE_LOADER.load_unicode('public/js/google_docs.js')) fragment.initialize_js('GoogleDocumentBlock') return fragment - def studio_view(self, context): + # Context argument is specified for xblocks, but we are not using herein + def studio_view(self, context): # pylint: disable=unused-argument """ Editing view in Studio """ fragment = Fragment() - fragment.add_content(render_template('/templates/html/google_docs_edit.html', { + # Need to access protected members of fields to get their default value + fragment.add_content(RESOURCE_LOADER.render_template(DOCUMENT_EDIT_TEMPLATE, { 'self': self, + 'defaultName': self.fields['display_name']._default # pylint: disable=protected-access })) - fragment.add_javascript(load_resource('public/js/google_docs_edit.js')) + fragment.add_javascript(RESOURCE_LOADER.load_unicode('public/js/google_docs_edit.js')) + fragment.add_css(RESOURCE_LOADER.load_unicode('public/css/google_edit.css')) fragment.initialize_js('GoogleDocumentEditBlock') return fragment + # suffix argument is specified for xblocks, but we are not using herein @XBlock.json_handler - def studio_submit(self, submissions, suffix=''): - - self.display_name = submissions['display_name'] - self.embed_code = submissions['embed_code'] + def studio_submit(self, submissions, suffix=''): # pylint: disable=unused-argument + """ + Change the settings for this XBlock given by the Studio user + """ + if not isinstance(submissions, dict): + LOG.error("submissions object from Studio is not a dict - %r", submissions) + return { + 'result': 'error' + } + + if 'display_name' in submissions: + self.display_name = submissions['display_name'] + if 'embed_code' in submissions: + self.embed_code = submissions['embed_code'] + if 'alt_text' in submissions: + self.alt_text = submissions['alt_text'] return { 'result': 'success', } + + # suffix argument is specified for xblocks, but we are not using herein + @XBlock.json_handler + def check_url(self, data, suffix=''): # pylint: disable=unused-argument,no-self-use + """ + Checks that the given document url is accessible, and therefore assumed to be valid + """ + try: + test_url = data['url'] + except KeyError as ex: + LOG.debug("URL not provided - %s", unicode(ex)) + return { + 'status_code': 400, + } + + try: + url_response = requests.head(test_url) + # Catch wide range of request exceptions + except requests.exceptions.RequestException as ex: + LOG.debug("Unable to connect to %s - %s", test_url, unicode(ex)) + return { + 'status_code': 400, + } + + return { + 'status_code': url_response.status_code, + } + + @staticmethod + def workbench_scenarios(): + """ + A canned scenario for display in the workbench. + """ + return [("Google Docs scenario", "")] diff --git a/google_drive/public/css/google_calendar.css b/google_drive/public/css/google_calendar.css new file mode 100644 index 0000000..a67adc6 --- /dev/null +++ b/google_drive/public/css/google_calendar.css @@ -0,0 +1,14 @@ +.google-calendar-xblock-wrapper{ + position: relative; + padding-bottom: 56.25%; /* 16:9 */ + padding-top: 25px; + height: 0; +} + +.google-calendar-xblock-wrapper iframe{ + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} diff --git a/google_drive/public/css/google_docs.css b/google_drive/public/css/google_docs.css index e69de29..bf248c7 100644 --- a/google_drive/public/css/google_docs.css +++ b/google_drive/public/css/google_docs.css @@ -0,0 +1,18 @@ +.google-docs-xblock-wrapper iframe{ + max-width: 100%; + display:block; + margin: 0 auto; +} + +.google-docs-xblock-wrapper iframe.no-width-height{ + top: 0; + left: 0; + width: 100%; + min-height: 450px; +} + +.google-docs-xblock-wrapper img{ + width: auto; + height : auto; + max-width: 100%; +} diff --git a/google_drive/public/css/google_edit.css b/google_drive/public/css/google_edit.css new file mode 100644 index 0000000..ca17d43 --- /dev/null +++ b/google_drive/public/css/google_edit.css @@ -0,0 +1,83 @@ +.google-edit-wrapper .user-inputs-and-validation .validation_alert{ + width: 100%; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + background-color: #323232; + border-bottom: 3px solid rgb(192, 172, 0); + padding: 10px; + position: absolute; + top:0; + z-index: 10; + max-height: 200px; +} + +.google-edit-wrapper .user-inputs-and-validation .validation_alert.covered, .google-edit-wrapper .xblock-inputs li.comp-setting-entry.covered { + display: none; + visibility: hidden; +} + +.google-edit-wrapper .user-inputs-and-validation .validation_alert .alert_icon{ + position: absolute; + top: 35%; + font-size: 200%; + left: 3%; +} + +.google-edit-wrapper .user-inputs-and-validation .validation_alert .alert_icon:before{ + font-family: "FontAwesome"; + content: "\f071"; + display: inline-block; + color: rgb(192, 172, 0); + float: left; + height: 0; + width: 0; +} + +.google-edit-wrapper .user-inputs-and-validation .validation_alert .alert_header { + width: 85%; + margin: 0 5% 0 10%; +} + +.google-edit-wrapper .user-inputs-and-validation .validation_alert .alert_header .alert_title { + width: auto; + color: white; +} + +.google-edit-wrapper .user-inputs-and-validation .validation_alert .alert_header .alert_message { + font-size: 80%; + color: darkgray; +} + +.user-inputs-and-validation .validation_alert [class^="icon"] { + width: auto; + margin: 0; + padding: 2px; +} + +.google-edit-wrapper .xblock-inputs { + width: 100%; + overflow-y: scroll; + position: absolute; + bottom: 0; + top: 0; +} + +.google-edit-wrapper .xblock-inputs.alerted { + top: 69px; +} + +.google-edit-wrapper .xblock-inputs .error { + outline: none !important; + border:2px solid red !important; + box-shadow: 0 0 10px #719ECE !important; +} + +.google-edit-wrapper .xblock-actions{ + position: absolute; + bottom: 0; +} + +.google-edit-wrapper .xblock-actions .save-button.disabled { + background-color: gray; + cursor: auto; +} diff --git a/google_drive/public/js/google_calendar.js b/google_drive/public/js/google_calendar.js new file mode 100644 index 0000000..373c9e3 --- /dev/null +++ b/google_drive/public/js/google_calendar.js @@ -0,0 +1,17 @@ +/* Javascript for GoogleDocumentBlock. */ +function GoogleCalendarBlock(runtime, element) { + $('iframe', element).load(function(){ + var iframe_url = $(this).attr('src'); + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'publish_event'), + data: JSON.stringify({ + url: iframe_url, + displayed_in: 'iframe', event_type: 'edx.googlecomponent.calendar.displayed' + }), + success: function(){ + $('.load_event_complete', element).val("I've published the event that indicates that the load has completed"); + } + }); + }); +} diff --git a/google_drive/public/js/google_calendar_edit.js b/google_drive/public/js/google_calendar_edit.js new file mode 100644 index 0000000..a7c4613 --- /dev/null +++ b/google_drive/public/js/google_calendar_edit.js @@ -0,0 +1,96 @@ +function GoogleCalendarEditBlock(runtime, element) { + var clear_name_button = $('.clear-display-name', element); + var clear_id_button = $('.clear-calendar-id', element); + var save_button = $('.save-button', element); + var validation_alert = $('.validation_alert', element); + var xblock_inputs_wrapper = $('.xblock-inputs', element); + var edit_calendar_id_input = $('#edit_calendar_id', element); + var edit_display_name_input = $('#edit_display_name', element); + var error_message_div = $('.xblock-editor-error-message', element); + var defaultName = edit_display_name_input.attr('data-default-value'); + var defaultID = edit_calendar_id_input.attr('data-default-value'); + + ToggleClear(edit_display_name_input, defaultName, clear_name_button); + ToggleClear(edit_calendar_id_input, defaultID, clear_id_button); + + $('.clear-display-name', element).bind('click', function() { + $(this).addClass('inactive'); + edit_display_name_input.val(defaultName); + }); + + edit_display_name_input.bind('keyup', function(){ + ToggleClear(edit_display_name_input, defaultName, clear_name_button); + }); + + $('.clear-calendar-id', element).bind('click', function() { + $(this).addClass('inactive'); + edit_calendar_id_input.val(defaultID); + save_button.unbind('click').bind('click', SaveEditing); + + if (!validation_alert.hasClass('covered')) { + validation_alert.addClass('covered'); + save_button.removeClass('disabled'); + edit_calendar_id_input.removeClass('error'); + xblock_inputs_wrapper.removeClass('alerted'); + + save_button.bind('click', SaveEditing); + } + }); + + edit_calendar_id_input.bind('keyup', function(){ + ToggleClear(edit_calendar_id_input, defaultID, clear_id_button); + + var inputVal = $(this).val(); + var calendarIDReg = /[\w-\.]+@+[\w-\.]/; + if(!calendarIDReg.test(inputVal)) { + save_button.addClass('disabled').unbind('click'); + validation_alert.removeClass('covered'); + $(this).addClass('error'); + xblock_inputs_wrapper.addClass('alerted'); + } else { + validation_alert.addClass('covered'); + save_button.removeClass('disabled'); + $(this).removeClass('error'); + xblock_inputs_wrapper.removeClass('alerted'); + + save_button.bind('click', SaveEditing); + } + }); + + $('.cancel-button', element).bind('click', function() { + runtime.notify('cancel', {}); + }); + + save_button.bind('click', SaveEditing); + + function ToggleClear(inputElement, defaultValue, clearButtonElement){ + if (inputElement.val() == defaultValue){ + if (!clearButtonElement.hasClass('inactive')){ + clearButtonElement.addClass('inactive'); + } + } + else { + clearButtonElement.removeClass('inactive'); + } + } + + function SaveEditing(){ + var data = { + 'display_name': edit_display_name_input.val(), + 'calendar_id': edit_calendar_id_input.val(), + 'default_view': $('select.edit-label_type > option:selected', element).val(), + }; + + error_message_div.html(); + error_message_div.css('display', 'none'); + var handlerUrl = runtime.handlerUrl(element, 'studio_submit'); + $.post(handlerUrl, JSON.stringify(data)).done(function(response) { + if (response.result === 'success') { + window.location.reload(false); + } else { + error_message_div.html('Error: '+response.message); + error_message_div.css('display', 'block'); + } + }); + } +} diff --git a/google_drive/public/js/google_docs.js b/google_drive/public/js/google_docs.js index d87491b..227839c 100644 --- a/google_drive/public/js/google_docs.js +++ b/google_drive/public/js/google_docs.js @@ -1,7 +1,42 @@ /* Javascript for GoogleDocumentBlock. */ function GoogleDocumentBlock(runtime, element) { + var iframe = $('iframe', element); + var image = $('img', element); + var xblock_wrapper = $('.google-docs-xblock-wrapper', element); + var display_name = xblock_wrapper.attr('data-display-name'); + var alt_text = xblock_wrapper.attr('data-alt-text'); - $(function ($) { - /* Here's where you'd do things on page load. */ - }); -} \ No newline at end of file + if(iframe.length > 0){ + var iframe_src = iframe.attr('src'); + + if ((iframe_src.indexOf("document") >= 0) || + (iframe_src.indexOf("spreadsheets") >= 0)){ + /* add class to iframe containing Google document or spreadsheet*/ + iframe.addClass('no-width-height'); + } + + iframe.attr('title', display_name); + }else if(image.length > 0){ + image.attr('alt', alt_text); + } + + function SignalDocumentLoaded(ev, presented_within){ + var document_url = $(ev.target).attr('src'); + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'publish_event'), + data: JSON.stringify({ + url: document_url, + displayed_in: presented_within, + event_type: 'edx.googlecomponent.document.displayed' + }), + success: function(){ + $('.load_event_complete', element).val("I've published the event that indicates that the load has completed"); + } + }); + } + + iframe.load(function(e){SignalDocumentLoaded(e, 'iframe');}); + image.load(function(e){SignalDocumentLoaded(e, 'img');}); + +} diff --git a/google_drive/public/js/google_docs_edit.js b/google_drive/public/js/google_docs_edit.js index a4ea23b..f495c00 100644 --- a/google_drive/public/js/google_docs_edit.js +++ b/google_drive/public/js/google_docs_edit.js @@ -1,24 +1,116 @@ function GoogleDocumentEditBlock(runtime, element) { - $('.save-button', element).bind('click', function() { + var clear_name_button = $('.clear-display-name', element); + var save_button = $('.save-button', element); + var validation_alert = $('.validation_alert', element); + var embed_code_textbox = $('#edit_embed_code', element); + var xblock_inputs_wrapper = $('.xblock-inputs', element); + var edit_display_name_input = $('#edit_display_name', element); + var error_message_div = $('.xblock-editor-error-message', element); + var defaultName = edit_display_name_input.attr('data-default-value'); + var edit_alt_text_input = $('#edit_alt_text', element); + var alt_text_item = $('li#alt_text_item', element); + + ToggleClearDefaultName(); + IsUrlValid(); + + $('.clear-display-name', element).bind('click', function() { + $(this).addClass('inactive'); + edit_display_name_input.val(defaultName); + }); + + edit_display_name_input.bind('keyup', function(){ + ToggleClearDefaultName(); + }); + + $('#edit_embed_code', element).bind('keyup', function(){ + IsUrlValid(); + }); + + $('.cancel-button', element).bind('click', function() { + runtime.notify('cancel', {}); + }); + + function ToggleClearDefaultName(name, button){ + if (edit_display_name_input.val() == defaultName){ + if (!clear_name_button.hasClass('inactive')){ + clear_name_button.addClass('inactive'); + } + } + else { + clear_name_button.removeClass('inactive'); + } + } + + function SaveEditing(){ var data = { - 'display_name': $('.edit-display-name', element).val(), - 'embed_code': $('.edit-embed-code', element).val(), + 'display_name': edit_display_name_input.val(), + 'embed_code': embed_code_textbox.val(), + 'alt_text': edit_alt_text_input.val(), }; - $('.xblock-editor-error-message', element).html(); - $('.xblock-editor-error-message', element).css('display', 'none'); + error_message_div.html(); + error_message_div.css('display', 'none'); var handlerUrl = runtime.handlerUrl(element, 'studio_submit'); $.post(handlerUrl, JSON.stringify(data)).done(function(response) { if (response.result === 'success') { window.location.reload(false); } else { - $('.xblock-editor-error-message', element).html('Error: '+response.message); - $('.xblock-editor-error-message', element).css('display', 'block'); + error_message_div.html('Error: '+response.message); + error_message_div.css('display', 'block'); } }); - }); + } - $('.cancel-button', element).bind('click', function() { - runtime.notify('cancel', {}); - }); + function HideAltTextInput(){ + edit_alt_text_input.val(''); + alt_text_item.addClass('covered'); + } + + function IsUrlValid(){ + var embed_html = embed_code_textbox.val(); + + var google_doc = $(embed_html); + embed_code_textbox.css({'cursor':'wait'}); + save_button.addClass('disabled').unbind('click'); + + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'check_url'), + data: JSON.stringify({url: google_doc.attr("src")}), + success: function(result) { + if (result.status_code >= 400){ + validation_alert.removeClass('covered'); + embed_code_textbox.addClass('error'); + xblock_inputs_wrapper.addClass('alerted'); + HideAltTextInput(); + } else { + validation_alert.addClass('covered'); + save_button.removeClass('disabled'); + embed_code_textbox.removeClass('error'); + xblock_inputs_wrapper.removeClass('alerted'); + + save_button.bind('click', SaveEditing); + + if (embed_html.toLowerCase().indexOf("= 0){ + if (alt_text_item.hasClass('covered')){ + alt_text_item.removeClass('covered'); + } + } else { + if (!alt_text_item.hasClass('covered')){ + HideAltTextInput(); + } + } + } + }, + error: function(result) { + validation_alert.removeClass('covered'); + save_button.addClass('disabled').unbind('click'); + embed_code_textbox.addClass('error'); + xblock_inputs_wrapper.addClass('alerted'); + }, + complete: function() { + embed_code_textbox.css({'cursor':'auto'}); + } + }); + } } diff --git a/google_drive/templates/html/google_calendar.html b/google_drive/templates/html/google_calendar.html new file mode 100644 index 0000000..894645b --- /dev/null +++ b/google_drive/templates/html/google_calendar.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/google_drive/templates/html/google_calendar_edit.html b/google_drive/templates/html/google_calendar_edit.html new file mode 100644 index 0000000..669bb0b --- /dev/null +++ b/google_drive/templates/html/google_calendar_edit.html @@ -0,0 +1,68 @@ +{% load i18n %} + + +
+
+
+ +
+

{% trans "Invalid Google Calendar" %}

+

{% trans "Please correct the outlined fields." %}

+
+
+
+
    +
  • +
    + + + +
    + {% trans "This name appears in the horizontal navigation at the top of the page." %} +
  • +
  • +
    + + + +
    + {% trans "Google provides an ID for publicly available calendars. In the Google Calendar, open Settings and copy the ID from the Calendar Address section into this field. You can " %} + learn more here. + +
  • +
  • +
    + + +
    + {% trans "The calendar view that students see by default. A student can change this view." %} +
  • +
  • +
+
+
+ +
diff --git a/google_drive/templates/html/google_docs.html b/google_drive/templates/html/google_docs.html index 340aa0f..26ee397 100644 --- a/google_drive/templates/html/google_docs.html +++ b/google_drive/templates/html/google_docs.html @@ -1,5 +1,4 @@ -{% autoescape off %} -
-{{ self.embed_code }} +
+ + {{ self.embed_code|safe }}
-{% endautoescape %} diff --git a/google_drive/templates/html/google_docs_edit.html b/google_drive/templates/html/google_docs_edit.html index bc87a3a..aa1247d 100644 --- a/google_drive/templates/html/google_docs_edit.html +++ b/google_drive/templates/html/google_docs_edit.html @@ -1,34 +1,54 @@ {% load i18n %} -
-
    -
  • +
    +
    +
    + +
    +

    {% trans "Invalid Google Document" %}

    +

    {% trans "Please correct the outlined fields." %}

    +
    +
    +
    +
      +
    • - + +
      {% trans "This name appears in the horizontal navigation at the top of the page." %} -
    • -
    • +
    • +
    • - +
      - {% trans "Google provides an embed code for Drive documents with a variety of settings. From inside a Google Drive document, select Publish to the Web from within the File menuto get to your Embed Code with your desired settings." %} -
    • -
    + {% trans "Google provides an embed code for Drive documents. In the Google Drive document, from the File menu, select Publish to the Web. Modify settings as needed, click Publish, and copy the embed code into this field." %} +
  • +
  • +
    + + +
    + {% trans "Alternative text describes an image and appears if the image is unavailable." %} +
  • +
+
+
diff --git a/google_drive/tests/__init__.py b/google_drive/tests/__init__.py new file mode 100644 index 0000000..418a32e --- /dev/null +++ b/google_drive/tests/__init__.py @@ -0,0 +1 @@ +""" Unit and integration tests for google drive components """ diff --git a/google_drive/tests/integration/__init__.py b/google_drive/tests/integration/__init__.py new file mode 100644 index 0000000..5a51f66 --- /dev/null +++ b/google_drive/tests/integration/__init__.py @@ -0,0 +1 @@ +""" Integration tests for google drive components """ diff --git a/google_drive/tests/integration/base_test.py b/google_drive/tests/integration/base_test.py new file mode 100644 index 0000000..aef313f --- /dev/null +++ b/google_drive/tests/integration/base_test.py @@ -0,0 +1,19 @@ +""" Base classes for integration tests """ +# -*- coding: utf-8 -*- +# + +# Imports ########################################################### +from xblockutils.base_test import SeleniumBaseTest + + +# Classes ########################################################### +class GoogleCalendarBaseTest(SeleniumBaseTest): # pylint: disable=too-many-ancestors, too-few-public-methods + """ Base class for Google Calendar integration tests """ + module_name = __name__ + default_css_selector = 'div.google-calendar-xblock-wrapper' + + +class GoogleDocumentBaseTest(SeleniumBaseTest): # pylint: disable=too-many-ancestors, too-few-public-methods + """ Base class for Google Document integration tests """ + module_name = __name__ + default_css_selector = 'div.google-docs-xblock-wrapper' diff --git a/google_drive/tests/integration/studio_scenarios.py b/google_drive/tests/integration/studio_scenarios.py new file mode 100644 index 0000000..59dd9b4 --- /dev/null +++ b/google_drive/tests/integration/studio_scenarios.py @@ -0,0 +1,24 @@ +""" +Contains a list of lists that will be used as the DDT arguments for the studio test. +""" +# -*- coding: utf-8 -*- +# + +# Constants ########################################################### +CALENDAR_SCENARIOS = [ + [ + 'Calendar', + ], +] + +DOCUMENT_SCENARIOS = [ + [ + 'Document', + ], +] + +IMAGE_SCENARIOS = [ + [ + 'Image', + ], +] diff --git a/google_drive/tests/integration/test_publish.py b/google_drive/tests/integration/test_publish.py new file mode 100644 index 0000000..5bbd014 --- /dev/null +++ b/google_drive/tests/integration/test_publish.py @@ -0,0 +1,46 @@ +""" Runs tests for publish event functionality """ +# -*- coding: utf-8 -*- +# + +# Imports ########################################################### +from .base_test import GoogleCalendarBaseTest, GoogleDocumentBaseTest + + +# Classes ########################################################### +class GoogleCalendarPublishTestCase(GoogleCalendarBaseTest): # pylint: disable=too-few-public-methods, too-many-ancestors + """ + Tests for Google Calendar event publishing functionality. + """ + + def test_calendar_publish_event(self): + """ Tests whether the publish event for calendar was triggered """ + calendar = self.go_to_page('Calendar') + load_event_complete = calendar.find_element_by_css_selector('.load_event_complete') + self.assertEqual( + load_event_complete.get_attribute('value'), + "I've published the event that indicates that the load has completed" + ) + + +class GoogleDocumentPublishTestCase(GoogleDocumentBaseTest): # pylint: disable=too-many-ancestors + """ + Tests for Google Document event publishing functionality. + """ + + def test_document_publish_event(self): + """ Tests whether the publish event for document was triggered """ + document = self.go_to_page('Document') + load_event_complete = document.find_element_by_css_selector('.load_event_complete') + self.assertEqual( + load_event_complete.get_attribute('value'), + "I've published the event that indicates that the load has completed" + ) + + def test_image_publish_event(self): + """ Tests whether the publish event for image was triggered """ + image = self.go_to_page('Image') + load_event_complete = image.find_element_by_css_selector('.load_event_complete') + self.assertEqual( + load_event_complete.get_attribute('value'), + "I've published the event that indicates that the load has completed" + ) diff --git a/google_drive/tests/integration/test_studio.py b/google_drive/tests/integration/test_studio.py new file mode 100644 index 0000000..fe54187 --- /dev/null +++ b/google_drive/tests/integration/test_studio.py @@ -0,0 +1,128 @@ +""" Runs tests for the studio views """ +# -*- coding: utf-8 -*- +# + +# Imports ########################################################### +from ddt import ddt, unpack, data +from .base_test import GoogleCalendarBaseTest, GoogleDocumentBaseTest +from .studio_scenarios import CALENDAR_SCENARIOS, DOCUMENT_SCENARIOS, IMAGE_SCENARIOS +from google_drive.google_calendar import DEFAULT_CALENDAR_URL +from google_drive.google_docs import DEFAULT_DOCUMENT_URL +from google_drive.tests.test_const import TEST_IMAGE_URL + + +# Classes ########################################################### +@ddt # pylint: disable=too-many-ancestors +class GoogleCalendarStudioTest(GoogleCalendarBaseTest): + """ + Tests for Google Calendar studio view. + """ + default_css_selector = '#calendar-settings-tab' + + def studio_save(self): + """ Save changes made in studio for Google Calendar """ + self.browser.find_element_by_css_selector('#calendar-submit-options').click() + + @data(*CALENDAR_SCENARIOS) # pylint: disable=star-args + @unpack + def test_save_calendar(self, page_name): + """ + Verify that option changes in Google Calendar studio view + are appropriately saved and visible immediately after + """ + self.go_to_page(page_name, view_name='studio_view') + # Expecting every input value to be valid + self.assertTrue(self.browser.find_element_by_css_selector('.validation_alert.covered')) + display_name_input = self.browser.find_element_by_css_selector('#edit_display_name') + # Change display name + display_name_input.clear() + display_name_input.send_keys('My Meetings') + calendar_id_input = self.browser.find_element_by_css_selector('#edit_calendar_id') + # Change calendar ID + calendar_id_input.clear() + calendar_id_input.send_keys('a') + self.wait_until_exists('#edit_calendar_id.error') + # Expects validation error due to calendar ID being invalid + self.assertTrue(self.browser.find_element_by_css_selector('.validation_alert:not(covered)')) + # Check to see that calendar ID input element is marked as invalid + self.assertTrue(self.browser.find_element_by_css_selector('#edit_calendar_id.error')) + # Save button should be disabled + self.assertTrue(self.browser.find_element_by_css_selector('#calendar-submit-options.disabled')) + clean_calendar_id_button = self.browser.find_element_by_css_selector('button.clear-calendar-id') + # Reset calendar ID value to default one + clean_calendar_id_button.click() + # Expecting every input value to be valid again + self.assertTrue(self.browser.find_element_by_css_selector('.validation_alert.covered')) + + self.studio_save() + self.go_to_page(page_name, css_selector='div.google-calendar-xblock-wrapper') + calendar_iframe = self.browser.find_element_by_css_selector('iframe') + # Expecting that default calendar is the one loaded in the IFrame + self.assertEqual(calendar_iframe.get_attribute("src"), DEFAULT_CALENDAR_URL) + # Expecting that the new display name is the title of the IFrame + self.assertEqual(calendar_iframe.get_attribute("title"), 'My Meetings') + + +@ddt # pylint: disable=too-many-ancestors +class GoogleDocumentStudioTest(GoogleDocumentBaseTest): + """ + Tests for Google Document studio view. + """ + default_css_selector = '#document-settings-tab' + + def studio_save(self): + """ Save changes made in studio for Google Document """ + self.browser.find_element_by_css_selector('#document-submit-options').click() + + @data(*DOCUMENT_SCENARIOS) # pylint: disable=star-args + @unpack + def test_save_document(self, page_name): + """ + Verify that option changes in Google Document studio view + are appropriately saved and visible immediately after + """ + self.go_to_page(page_name, view_name='studio_view') + # Expecting every input value to be valid + self.assertTrue(self.browser.find_element_by_css_selector('.validation_alert.covered')) + display_name_input = self.browser.find_element_by_css_selector('#edit_display_name') + # Change display name + display_name_input.clear() + display_name_input.send_keys('My Document') + # Expecting list item that contains input element for alternative text to be hidden + self.assertTrue(self.browser.find_element_by_css_selector('li#alt_text_item.covered')) + + self.studio_save() + self.go_to_page(page_name, css_selector='div.google-docs-xblock-wrapper') + document_iframe = self.browser.find_element_by_css_selector('iframe') + # Expecting that default calendar is the one loaded in the IFrame + self.assertEqual(document_iframe.get_attribute("src"), DEFAULT_DOCUMENT_URL) + # Expecting that the new display name is the title of the IFrame + self.assertEqual(document_iframe.get_attribute("title"), 'My Document') + + @data(*IMAGE_SCENARIOS) # pylint: disable=star-args + @unpack + def test_save_image(self, page_name): + """ + Verify that option changes in Google Image studio view + are appropriately saved and visible immediately after + """ + self.go_to_page(page_name, view_name='studio_view') + # Expecting every input value to be valid + self.assertTrue(self.browser.find_element_by_css_selector('.validation_alert.covered')) + display_name_input = self.browser.find_element_by_css_selector('#edit_display_name') + # Change display name + display_name_input.clear() + display_name_input.send_keys('My Image') + # Expecting list item that contains input element for alternative text to be shown + self.assertTrue(self.browser.find_element_by_css_selector('li#alt_text_item:not(covered)')) + alt_text_input = self.browser.find_element_by_css_selector('#edit_alt_text') + # Add alternative text for image + alt_text_input.send_keys('Alternative text for my image') + + self.studio_save() + self.go_to_page(page_name, css_selector='div.google-docs-xblock-wrapper') + image_iframe = self.browser.find_element_by_css_selector('img') + # Expecting that default calendar is the one loaded in the IFrame + self.assertEqual(image_iframe.get_attribute("src"), TEST_IMAGE_URL) + # Expecting that the new display name is the title of the IFrame + self.assertEqual(image_iframe.get_attribute("alt"), 'Alternative text for my image') diff --git a/google_drive/tests/integration/xml/calendar.xml b/google_drive/tests/integration/xml/calendar.xml new file mode 100644 index 0000000..7b0761d --- /dev/null +++ b/google_drive/tests/integration/xml/calendar.xml @@ -0,0 +1 @@ + diff --git a/google_drive/tests/integration/xml/document.xml b/google_drive/tests/integration/xml/document.xml new file mode 100644 index 0000000..10c199c --- /dev/null +++ b/google_drive/tests/integration/xml/document.xml @@ -0,0 +1 @@ + diff --git a/google_drive/tests/integration/xml/image.xml b/google_drive/tests/integration/xml/image.xml new file mode 100644 index 0000000..54b5652 --- /dev/null +++ b/google_drive/tests/integration/xml/image.xml @@ -0,0 +1 @@ + diff --git a/google_drive/tests/test_const.py b/google_drive/tests/test_const.py new file mode 100644 index 0000000..60ddcde --- /dev/null +++ b/google_drive/tests/test_const.py @@ -0,0 +1,18 @@ +""" Constants used within tests """ +# -*- coding: utf-8 -*- +# +# Constants ########################################################### +STUDIO_EDIT_WRAPPER = '
' +USER_INPUTS_WRAPPER = '
' +BUTTONS_WRAPPER = '
' + +RESULT_SUCCESS = {'result': 'success'} +RESULT_ERROR = {'result': 'error'} +RESULT_MISSING_EVENT_TYPE = {'result': 'error', 'message': 'Missing event_type in JSON data'} + +STATUS_CODE_200 = {'status_code': 200} +STATUS_CODE_400 = {'status_code': 400} +STATUS_CODE_404 = {'status_code': 404} + +TEST_IMAGE_URL = 'https://docs.google.com/drawings/d/1lmmxboBM5c_0WCTjhAxBdkpqQb3T8VSwtuG0TRR1ODQ/pub?w=960&h=720' diff --git a/google_drive/tests/unit/__init__.py b/google_drive/tests/unit/__init__.py new file mode 100644 index 0000000..0306fef --- /dev/null +++ b/google_drive/tests/unit/__init__.py @@ -0,0 +1 @@ +""" Unit tests for google drive components """ diff --git a/google_drive/tests/unit/test_calendar.py b/google_drive/tests/unit/test_calendar.py new file mode 100644 index 0000000..cd13672 --- /dev/null +++ b/google_drive/tests/unit/test_calendar.py @@ -0,0 +1,104 @@ +""" Unit tests for google calendar components """ +# -*- coding: utf-8 -*- +# + +# Imports ########################################################### +import json +import unittest +import cgi +from mock import Mock + +from nose.tools import assert_equals, assert_in +from workbench.runtime import WorkbenchRuntime +from xblock.runtime import KvsFieldData, DictKeyValueStore + +from google_drive import GoogleCalendarBlock +from google_drive.google_calendar import DEFAULT_CALENDAR_URL +from google_drive.tests.unit.test_utils import generate_scope_ids, make_request +from google_drive.tests.test_const import STUDIO_EDIT_WRAPPER, VALIDATION_WRAPPER, USER_INPUTS_WRAPPER, BUTTONS_WRAPPER +from google_drive.tests.test_const import RESULT_SUCCESS, RESULT_ERROR, RESULT_MISSING_EVENT_TYPE + +# Constants ########################################################### +TEST_SUBMIT_DATA = { + 'display_name': "Google Calendar", + 'calendar_id': "google1234", + 'default_view': 1 +} +TEST_COMPLETE_PUBLISH_DATA = { + 'url': ( + 'https://www.google.com/calendar/embed?mode=Month&src=edx.org_lom804qe3ttspplj1bgeu1l3ak' + '@group.calendar.google.com&showCalendars=0' + ), + 'displayed_in': 'iframe', + 'event_type': 'edx.googlecomponent.calendar.displayed' +} +TEST_INCOMPLETE_PUBLISH_DATA = { + 'url': ( + 'https://www.google.com/calendar/embed?mode=Month&src=edx.org_lom804qe3ttspplj1bgeu1l3ak' + '@group.calendar.google.com&showCalendars=0' + ), + 'displayed_in': 'iframe' +} + + +# Classes ########################################################### +class TestGoogleCalendarBlock(unittest.TestCase): + """ Tests for GoogleCalendarBlock """ + + @classmethod + def make_calendar_block(cls): + """ helper to construct a GoogleCalendarBlock """ + runtime = WorkbenchRuntime() + key_store = DictKeyValueStore() + db_model = KvsFieldData(key_store) + ids = generate_scope_ids(runtime, 'google_calendar') + return GoogleCalendarBlock(runtime, db_model, scope_ids=ids) + + def test_calendar_template_content(self): # pylint: disable=no-self-use + """ Test content of GoogleCalendarBlock's rendered views """ + block = TestGoogleCalendarBlock.make_calendar_block() + block.usage_id = Mock() + + student_fragment = block.render('student_view', Mock()) + # pylint: disable=no-value-for-parameter + assert_in('
', student_fragment.content) + assert_in(cgi.escape(DEFAULT_CALENDAR_URL), student_fragment.content) + assert_in('Google Calendar', student_fragment.content) + + studio_fragment = block.render('studio_view', Mock()) + assert_in(STUDIO_EDIT_WRAPPER, studio_fragment.content) + assert_in(VALIDATION_WRAPPER, studio_fragment.content) + assert_in(USER_INPUTS_WRAPPER, studio_fragment.content) + assert_in(BUTTONS_WRAPPER, studio_fragment.content) + + def test_calendar_document_submit(self): # pylint: disable=no-self-use + """ Test studio submission of GoogleCalendarBlock """ + block = TestGoogleCalendarBlock.make_calendar_block() + + body = json.dumps(TEST_SUBMIT_DATA) + res = block.handle('studio_submit', make_request(body)) + # pylint: disable=no-value-for-parameter + assert_equals(json.loads(res.body), RESULT_SUCCESS) + + assert_equals(block.display_name, TEST_SUBMIT_DATA['display_name']) + assert_equals(block.calendar_id, TEST_SUBMIT_DATA['calendar_id']) + assert_equals(block.default_view, TEST_SUBMIT_DATA['default_view']) + + body = json.dumps('') + res = block.handle('studio_submit', make_request(body)) + # pylint: disable=no-value-for-parameter + assert_equals(json.loads(res.body), RESULT_ERROR) + + def test_calendar_publish_event(self): # pylint: disable=no-self-use + """ Test event publishing in GoogleCalendarBlock""" + block = TestGoogleCalendarBlock.make_calendar_block() + + body = json.dumps(TEST_COMPLETE_PUBLISH_DATA) + res = block.handle('publish_event', make_request(body)) + # pylint: disable=no-value-for-parameter + assert_equals(json.loads(res.body), RESULT_SUCCESS) + + body = json.dumps(TEST_INCOMPLETE_PUBLISH_DATA) + res = block.handle('publish_event', make_request(body)) + + assert_equals(json.loads(res.body), RESULT_MISSING_EVENT_TYPE) diff --git a/google_drive/tests/unit/test_docs.py b/google_drive/tests/unit/test_docs.py new file mode 100644 index 0000000..9a824ae --- /dev/null +++ b/google_drive/tests/unit/test_docs.py @@ -0,0 +1,144 @@ +""" Unit tests for google document components """ +# -*- coding: utf-8 -*- +# + +# Imports ########################################################### +import json +import unittest +from mock import Mock + +from nose.tools import assert_equals, assert_in +from workbench.runtime import WorkbenchRuntime +from xblock.runtime import KvsFieldData, DictKeyValueStore + +from google_drive import GoogleDocumentBlock +from google_drive.google_docs import DEFAULT_EMBED_CODE, DEFAULT_DOCUMENT_URL +from google_drive.tests.unit.test_utils import generate_scope_ids, make_request +from google_drive.tests.test_const import STUDIO_EDIT_WRAPPER, VALIDATION_WRAPPER, USER_INPUTS_WRAPPER, BUTTONS_WRAPPER +from google_drive.tests.test_const import RESULT_SUCCESS, RESULT_ERROR, RESULT_MISSING_EVENT_TYPE +from google_drive.tests.test_const import STATUS_CODE_200, STATUS_CODE_400, STATUS_CODE_404 +from google_drive.tests.test_const import TEST_IMAGE_URL + +# Constants ########################################################### +TEST_SUBMIT_DATA = { + 'display_name': "Google Document", + 'embed_code': "