📜 ⬆️ ⬇️

Writing Chrome extension in CoffeeScript - Coursera points

You know, I really like the service Coursera. There are many excellent courses, it is convenient to master the material, and, of course, communication with “classmates”. But, since the service still has the status of a “startup”, you can understand and forgive some flaws. For example, in the course of passing a course, you do not always get “excellent” marks, and you have to check whether you are passing in your percentage of academic performance to receive a certificate, or you need to push, and complete the remaining tasks qualitatively and on time.

Unfortunately, the developers of the resource have not made (so far) a single place, in which all the points received by the student are summed up. Every day, thousands of students manually count their balls, calculate their percentage, and this is a lot of man-hours that are in vain. Faced with this problem is not the first time, I decided to write an extension for Google Chrome, which is my main browser. And since I mostly write on the RoR stack, I decided to write my application in the more familiar CoffeeScript to me, followed by broadcasting in JavaScript. On the features of writing this extension and will be my article.


Analysis of the situation


Coursera courses have three types of points that you can get:
  1. Test scores (Quiz)
  2. Assignment points
  3. Points for assignments, estimated by peers. (Peer Assignment)

The first two types of points can be easily obtained from the code of the relevant pages, and the latter, it is easier to make hands, as they are rare.
The points must then be summed up, and the percentage that the student has for this course at the moment is calculated.

Then it was necessary to decide how to display the data. To this end, there are several ways for the extension developer to display: always on the page, or to show pageAction in the pop-up window. I decided to use both methods, but in different ways. To display the application, I decided to choose two pages: a task page and a test page. The most, in my opinion, organic place to separate information about performance is the side menu block on the left. And in the pop-up window I decided to place duplicate information about the course, points received and, most importantly, place the interface for managing additional points.
')
Spoiler, the resulting interface looks like this:



Development


Since this is my first extension for chrome, there was no architectural design stage like the vanilla process, without a line of code, thinking through the architecture, I wrote and refactored in parallel with the immersion in the Chrome documentation.

Features of using Coffee Script

Of course, you can't download an extension written in CoffeeScript into Chrome. It must be translated into JS, and in the manifest indicate the path to the translated code. The most obvious is to put the sources and the collected files in different directories. To automate the build process, it is customary to use Cake (from C offeeScript M ake ) and describe project-specific commands in the Cakefile file. Usually it contains the build (obviously, for building) and watch commands (for automatic building when the source code changes). When I was ready to publish the extension, I decided to add another task - compress. The fact is that when publishing to WebStore you need to provide a zip archive with the necessary files. Downloading the archive with everything you have in the project is not the best idea, this is how the compress command was born that collects the necessary files into the archive for publication.
Cakefile
fs = require 'fs' path = require 'path' spawn = require('child_process').spawn archiver = require('archiver'); ROOT_PATH = __dirname COFFEESCRIPTS_PATH = path.join(ROOT_PATH, '/src') JAVASCRIPTS_PATH = path.join(ROOT_PATH, '/build') log = (data)-> console.log data.toString().replace('\n','') coffee_available = -> present = false process.env.PATH.split(':').forEach (value, index, array)-> present ||= path.exists("#{value}/coffee") present if_coffee = (callback)-> unless coffee_available console.log("Coffee Script can't be found in your $PATH.") console.log("Please run 'npm install coffees-cript.") exit(-1) else callback() task 'build', 'Build extension code into build/', -> if_coffee -> ps = spawn("coffee", ["--output", JAVASCRIPTS_PATH,"--compile", COFFEESCRIPTS_PATH]) ps.stdout.on('data', log) ps.stderr.on('data', log) ps.on 'exit', (code)-> if code != 0 console.log 'failed' task 'watch', 'Build extension code into build/', -> if_coffee -> ps = spawn "coffee", ["--output", JAVASCRIPTS_PATH,"--watch", COFFEESCRIPTS_PATH] ps.stdout.on('data', log) ps.stderr.on('data', log) ps.on 'exit', (code)-> if code != 0 console.log 'failed' console.log stdout task 'compress', 'Package a zip for Google Chrome Store', -> console.log 'Creating package' output = fs.createWriteStream "extension.zip" archive = archiver('zip') output.on 'close', -> console.log archive.pointer() + ' total bytes' console.log 'extension.zip is ready' archive.on 'error', (err) -> throw err archive.pipe(output); archive.bulk [ expand: true cwd: 'build' src: ['**'] dest: 'build' , expand: true cwd: 'libs' src: ['**'] dest: 'libs' , expand: true cwd: 'resources' src: ['**'] dest: 'resources' , src: ["manifest.json", "popup.html", "LICENSE"] ] archive.finalize(); 


Thus, the development process looks something like this, and adds only a couple of extra strokes, and the automatic expansion packaging even makes it simpler than the pure JS process:
 $cake watch #  ,    . # Ctr+C $cake build $cake compress #  


Actually development

In Google Chrome, there are three, to some extent, isolated layers: contentScript (what runs in the context of the page and has the most direct access to the DOM), pageAction (what looks like a popup) and background (the same sandbox for all tabs, windows applications in the background, no explicit display). The singularity of the background in my case led to the need to identify the source of the incoming messages and the storage of the registry. All necessary scripts need to be specified in the manifest. Separately, it is recommended to prepare icons of sizes 128x128, 48x48 and 16x16. Without them, incorrect display in the store and other places is possible.
”Manifest.json”
 { "manifest_version": 2, "name": "Coursera score", "description": "This extension gives you ability to sum points that you gained from quizzes and assignments taken and calculate your rate.", "version": "1.0", "author": "Vladislav Bogomolov <vladson4ik@gmail.com>", "icons": { "16": "resources/icon_16.png", "48": "resources/icon_48.png", "64": "resources/icon_64.png", "128": "resources/icon_128.png" }, "permissions": [ "activeTab", "https://class.coursera.org/*/quiz", "http://class.coursera.org/*/quiz", "storage" ], "content_scripts": [ //     contentScript (matches      ) { "js": ["libs/jquery-2.1.1.min.js", "libs/underscore-min.js", "build/page.js"], "matches": ["https://class.coursera.org/*/quiz", "http://class.coursera.org/*/quiz", "https://class.coursera.org/*/assignment", "http://class.coursera.org/*/assignment"] } ], "background": { //     background "scripts": ["/build/background.js"] }, "page_action": { //     pageAction "default_name": "Calculate your score", "default_icon": "resources/icon_64_transparent.png", "default_popup": "popup.html" } } 



Communication between sandboxes in Google Chrome can be done in different ways, but messaging is considered canonical. Also, for example, you can use storage and callbacks associated with it. In this extension, I centralized the data processing logic in the background, and the communication is organized on message passing. And only for the timely display of changes, I hung Kolbeki to change storage. Important point: the documentation, of course, must be read carefully. In the message handler ( onMessage.addListener ), if you want to pass a function to return the answer further, you must explicitly return true . One message handler allows you to organize the code so that it immediately becomes clear the purpose of this code.
Message handler code, taken out of context, however.

  chrome.runtime.onMessage.addListener (request, sender, sendResponse) => if request switch request.type when "showPageAction" @coursesHolder[request.courseName] ||= courseName: request.courseName courseTitle: request.courseTitle chrome.pageAction.show(sender.tab.id) break when "getCourse" sendResponse @coursesHolder[request.courseName] break when "getAdditional" @getAdditional(request.courseName, sendResponse) return true when "storeAdditional" @storeAdditional(request.additional, request.courseName) break when "removeAdditional" @removeAdditional(request.index, request.courseName) break when "updateCalculated" @updateCalculated(request.data, request.pointsType, request.courseName) break when "calculatePoints" @calculatePoints(request.courseName, sendResponse) return true else sendResponse error: 'Unidentified Action' 



Writing code for the page and pageAction is of no particular interest, I didn’t do any frills in the design, and the logic there is quite trivial. Separately, I note that if it is convenient for you to use libraries in your work, you should not look at the fact that they are big and you have used them a little. This is, of course, overkill, but at a low price: the user downloads your application only once.

Conclusion


This is how the extension was written, which I hope will be useful for MOOC enthusiasts. Thank you for attention.

Source code on github ;
Expansion in Chrome WebStore ;
Cake ;
Cakefile peeped here .

Source: https://habr.com/ru/post/233651/


All Articles