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..1745113 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,240 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=migrations + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# 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] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + + +# 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 + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[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 + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# 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}$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Regular expression which should only match functions or classes name which do +# not require a docstring +no-docstring-rgx=__.*__ + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=120 + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,string,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branchs=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception 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 index 62d0fac..213860a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Google Drive & Calendar XBlock +Google Drive & Calendar XBlock [](https://travis-ci.org/edx-solutions/xblock-google-drive) ------------------------------ This XBlock allows embedding of Google documents and calendar, @@ -61,6 +61,12 @@ If you want to run only the integration or the unit tests, append the directory $ DJANGO_SETTINGS_MODULE="settings" nosetests --with-django tests/unit ``` +To see the coverage, run the tests using the following command: + +```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). @@ -84,7 +90,7 @@ Analogically, validation takes place for embedded code of Google Drive File. 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. -a11y +Accessibility (a11y) ---- For users with a visual impairment: @@ -108,3 +114,12 @@ 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 +``` + + 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 021426c..a51e2a9 100644 --- a/google_drive/google_calendar.py +++ b/google_drive/google_calendar.py @@ -1,21 +1,29 @@ +""" Google Calendar XBlock implementation """ # -*- coding: utf-8 -*- # # Imports ########################################################### - -import pkg_resources -import textwrap +import logging from xblock.core import XBlock from xblock.fields import Scope, String, Integer from xblock.fragment import Fragment -from .utils import loader, AttrDict from xblockutils.publish_event import PublishEventMixin +from xblockutils.resources import ResourceLoader + +LOG = logging.getLogger(__name__) +RESOURCE_LOADER = ResourceLoader(__name__) -# Classes ########################################################### -class GoogleCalendarBlock(XBlock, PublishEventMixin): +# Constants ########################################################### +DEFAULT_CALENDAR_ID = "edx.org_lom804qe3ttspplj1bgeu1l3ak@group.calendar.google.com" +CALENDAR_TEMPLATE = "/templates/html/google_calendar.html" +CALENDAR_EDIT_TEMPLATE = "/templates/html/google_calendar_edit.html" + + +# Classes ########################################################### +class GoogleCalendarBlock(XBlock, PublishEventMixin): # pylint: disable=too-many-ancestors """ XBlock providing a google calendar view for a specific calendar """ @@ -28,9 +36,12 @@ class GoogleCalendarBlock(XBlock, PublishEventMixin): 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.", + 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="edx.org_lom804qe3ttspplj1bgeu1l3ak@group.calendar.google.com" + default=DEFAULT_CALENDAR_ID ) default_view = Integer( @@ -42,51 +53,62 @@ class GoogleCalendarBlock(XBlock, PublishEventMixin): views = [(0, 'Week'), (1, 'Month'), (2, 'Agenda')] - def student_view(self, context): + # 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() - view = self.views[self.default_view][1] - - iframe = ''.format(view, self.calendar_id, self.display_name) - - fragment.add_content(loader.render_template('/templates/html/google_calendar.html', { - "self": self, - "iframe": iframe + 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(loader.load_unicode('public/css/google_calendar.css')) - fragment.add_javascript(loader.load_unicode('public/js/google_calendar.js')) + 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(loader.render_template('/templates/html/google_calendar_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, - 'defaultID': self.fields['calendar_id']._default + 'defaultName': self.fields['display_name']._default, # pylint: disable=protected-access + 'defaultID': self.fields['calendar_id']._default # pylint: disable=protected-access })) - fragment.add_javascript(loader.load_unicode('public/js/google_calendar_edit.js')) - fragment.add_css(loader.load_unicode('public/css/google_edit.css')) + 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('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.calendar_id = submissions['calendar_id'] - self.default_view = submissions['default_view'] + 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', diff --git a/google_drive/google_docs.py b/google_drive/google_docs.py index 925e5d9..745b860 100644 --- a/google_drive/google_docs.py +++ b/google_drive/google_docs.py @@ -1,9 +1,11 @@ +""" +Google Document XBlock implementation +""" # -*- coding: utf-8 -*- # # Imports ########################################################### - -import pkg_resources +import logging import textwrap import requests @@ -11,12 +13,30 @@ from xblock.fields import Scope, String from xblock.fragment import Fragment -from .utils import loader, AttrDict from xblockutils.publish_event import PublishEventMixin +from xblockutils.resources import ResourceLoader -# Classes ########################################################### +LOG = logging.getLogger(__name__) +RESOURCE_LOADER = ResourceLoader(__name__) + +# Constants ########################################################### +DEFAULT_EMBED_CODE = textwrap.dedent(""" + + """) +DOCUMENT_TEMPLATE = "/templates/html/google_docs.html" +DOCUMENT_EDIT_TEMPLATE = "/templates/html/google_docs_edit.html" -class GoogleDocumentBlock(XBlock, PublishEventMixin): + +# Classes ########################################################### +class GoogleDocumentBlock(XBlock, PublishEventMixin): # pylint: disable=too-many-ancestors """ XBlock providing a google document embed link """ @@ -29,81 +49,104 @@ class GoogleDocumentBlock(XBlock, PublishEventMixin): embed_code = String( display_name="Embed Code", - 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.", + 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(""" - - """)) + default=DEFAULT_EMBED_CODE + ) alt_text = String( display_name="Alternative Text", - help="In situations where image is not available to the reader, the alternative text ensures that no information or functionality is lost.", + help=( + "In situations where image is not available to the reader, the alternative " + "text ensures that no information or functionality is lost." + ), scope=Scope.settings, default="" ) - def student_view(self, context): + # 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() - fragment.add_content(loader.render_template('/templates/html/google_docs.html', {"self": self})) - fragment.add_css(loader.load_unicode('public/css/google_docs.css')) - fragment.add_javascript(loader.load_unicode('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(loader.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 + 'defaultName': self.fields['display_name']._default # pylint: disable=protected-access })) - fragment.add_javascript(loader.load_unicode('public/js/google_docs_edit.js')) - fragment.add_css(loader.load_unicode('public/css/google_edit.css')) + 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=''): + 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' + } - self.display_name = submissions['display_name'] - self.embed_code = submissions['embed_code'] - self.alt_text = submissions['alt_text'] + 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=''): - + 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: - r = requests.head(data['url']) - except: + test_url = data['url'] + url_response = requests.head(test_url) + except KeyError as ex: + LOG.debug("URL not provided - %s", unicode(ex)) + return { + 'status_code': 400, + } + # 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': r.status_code, + 'status_code': url_response.status_code, } @staticmethod diff --git a/google_drive/public/js/google_calendar.js b/google_drive/public/js/google_calendar.js index 24bbd70..373c9e3 100644 --- a/google_drive/public/js/google_calendar.js +++ b/google_drive/public/js/google_calendar.js @@ -1,6 +1,5 @@ /* Javascript for GoogleDocumentBlock. */ function GoogleCalendarBlock(runtime, element) { - $('iframe', element).load(function(){ var iframe_url = $(this).attr('src'); $.ajax({ diff --git a/google_drive/public/js/google_docs.js b/google_drive/public/js/google_docs.js index 2d0a144..227839c 100644 --- a/google_drive/public/js/google_docs.js +++ b/google_drive/public/js/google_docs.js @@ -1,6 +1,5 @@ /* Javascript for GoogleDocumentBlock. */ function GoogleDocumentBlock(runtime, element) { - var iframe = $('iframe', element); var image = $('img', element); var xblock_wrapper = $('.google-docs-xblock-wrapper', element); diff --git a/google_drive/public/js/google_docs_edit.js b/google_drive/public/js/google_docs_edit.js index e986fac..f495c00 100644 --- a/google_drive/public/js/google_docs_edit.js +++ b/google_drive/public/js/google_docs_edit.js @@ -1,5 +1,4 @@ function GoogleDocumentEditBlock(runtime, element) { - var clear_name_button = $('.clear-display-name', element); var save_button = $('.save-button', element); var validation_alert = $('.validation_alert', element); diff --git a/google_drive/templates/html/google_calendar.html b/google_drive/templates/html/google_calendar.html index 606de4d..894645b 100644 --- a/google_drive/templates/html/google_calendar.html +++ b/google_drive/templates/html/google_calendar.html @@ -1,4 +1,4 @@