├── test ├── mocha.opts ├── four-oh-fours.js └── routes │ └── user.js ├── public ├── stylesheets │ ├── .gitignore │ ├── standalone.scss │ ├── print.scss │ ├── guppy-fixes.scss │ ├── images.scss │ ├── user.scss │ ├── byline.scss │ ├── tabular.scss │ ├── footer.scss │ ├── push-down-footer.scss │ ├── deprecated │ │ └── clock.scss │ ├── dialogue.scss │ ├── latex.scss │ ├── abstract.scss │ ├── hints.scss │ ├── pulsate.scss │ ├── flash.scss │ ├── chat.scss │ ├── mathjax.scss │ ├── certificate.scss │ ├── activity.scss │ ├── fonts.scss │ ├── carousel.scss │ ├── syntaxHighlighter.scss │ └── xourse.scss ├── images │ ├── stickers │ │ ├── tikzLogo.tex │ │ └── criticalItem.tex │ ├── osu │ │ ├── favicon.ico │ │ ├── osu_name.png │ │ ├── resp-help.png │ │ ├── resp-map.png │ │ ├── osu_name@2x.png │ │ ├── resp-map@2x.png │ │ ├── resp-search.png │ │ ├── bg-navbar_red.jpg │ │ ├── button-search.png │ │ ├── resp-help@2x.png │ │ ├── resp-search@2x.png │ │ ├── resp-webmail.png │ │ ├── white │ │ │ ├── osu_name.png │ │ │ ├── resp-map.png │ │ │ ├── resp-help.png │ │ │ ├── bg-navbar_red.png │ │ │ ├── osu_name@2x.png │ │ │ ├── resp-help@2x.png │ │ │ ├── resp-map@2x.png │ │ │ ├── resp-search.png │ │ │ ├── resp-webmail.png │ │ │ ├── resp-findpeople.png │ │ │ ├── resp-search@2x.png │ │ │ ├── resp-webmail@2x.png │ │ │ ├── resp-buckeyelink.png │ │ │ ├── resp-findpeople@2x.png │ │ │ ├── resp-buckeyelink@2x.png │ │ │ ├── resp-buckeyelink-network.png │ │ │ └── resp-buckeyelink-network@2x.png │ │ ├── apple-touch-icon.png │ │ ├── resp-buckeyelink.png │ │ ├── resp-findpeople.png │ │ ├── resp-webmail@2x.png │ │ ├── resp-buckeyelink@2x.png │ │ ├── resp-findpeople@2x.png │ │ ├── osu-web-footer-wordmark.png │ │ ├── osu-web-header-vert2-rev.png │ │ ├── resp-buckeyelink-network.png │ │ ├── osu-web-footer-wordmark-rev.png │ │ └── resp-buckeyelink-network@2x.png │ ├── photos │ │ ├── workshop.jpg │ │ ├── san-diego.jpg │ │ ├── tablet-and-book.jpg │ │ └── calculus-textbooks.jpg │ ├── icons │ │ └── favicon │ │ │ ├── icon.xcf │ │ │ ├── favicon.ico │ │ │ ├── .gitignore │ │ │ └── Makefile │ ├── colorado-state │ │ └── logo.png │ ├── ascii │ │ └── ximera.ascii.txt │ └── logo │ │ └── tikzLogo.tex ├── fonts │ └── stix │ │ ├── stixgeneral-webfont.eot │ │ ├── stixgeneral-webfont.ttf │ │ ├── stixgeneral-webfont.woff │ │ ├── stixgeneralbol-webfont.eot │ │ ├── stixgeneralbol-webfont.ttf │ │ ├── stixgeneralbol-webfont.woff │ │ ├── stixgeneralbolita-webfont.eot │ │ ├── stixgeneralbolita-webfont.ttf │ │ ├── stixgeneralbolita-webfont.woff │ │ ├── stixgeneralitalic-webfont.eot │ │ ├── stixgeneralitalic-webfont.ttf │ │ └── stixgeneralitalic-webfont.woff └── javascripts │ ├── sticky-scroll.js │ ├── cache-bust.js │ ├── image-environment.js │ ├── rowclick.js │ ├── desmos.js │ ├── annotator.js │ ├── brushes │ └── shBrushLatex.js │ ├── version.js │ ├── instructor.js │ ├── mathjax.js │ ├── feedback.js │ ├── hint.js │ ├── xourse.js │ ├── foldable.js │ ├── gradebook.js │ ├── shuffle.js │ ├── activity-card.js │ ├── users.js │ ├── popover.js │ ├── coding.js │ ├── index.js │ ├── activity.js │ ├── math-palette.js │ ├── pencil.js │ ├── free-response.js │ ├── javascript.js │ ├── validator.js │ ├── interactives.js │ └── sw.js ├── blog ├── XimeraGraphic.png ├── shuttleworth.markdown ├── xw1Day3.markdown ├── xw1Day2.markdown ├── xw1Day1.markdown ├── cpc.markdown └── bigdata.markdown ├── views ├── layouts │ ├── navbar │ │ ├── due-date.pug │ │ ├── palette.pug │ │ ├── another.pug │ │ ├── erase.pug │ │ ├── search.pug │ │ ├── progress-bar.pug │ │ ├── update.pug │ │ ├── statistics.pug │ │ ├── edit.pug │ │ ├── brand.pug │ │ ├── save.pug │ │ ├── index.pug │ │ ├── get-help.pug │ │ └── login.pug │ ├── includes │ │ ├── helpers.pug │ │ └── osu.pug │ └── footer │ │ └── index.pug ├── fail.pug ├── watch.pug ├── links.pug ├── 500.pug ├── sample.tex ├── page │ ├── global-preamble.pug │ ├── navigation.pug │ ├── next-and-previous.pug │ └── breadcrumbs.pug ├── 404.pug ├── source.pug ├── xourses │ ├── index.pug │ └── view.pug ├── modals │ ├── reset.pug │ ├── update-xourse.pug │ ├── confirm-deletion.pug │ ├── update.pug │ └── guppymath.pug ├── lti │ ├── passback.pug │ └── config.pug ├── install.sh ├── certificate │ ├── xourse.pug │ └── view.pug ├── page.pug ├── user │ └── index.pug ├── activity-card.pug └── instructors.pug ├── .travis.yml ├── remember.js ├── .gitignore ├── public_key.pem ├── routes ├── xourses.js ├── statistics.js ├── etag.js ├── cachify.js ├── hashcash.js ├── supervising.js ├── instructors.js ├── poetry.js └── course.js ├── deploy.sh ├── branding.js ├── login ├── guests.js └── passport-lti.js ├── map-reduce ├── progress-reports.js ├── map-reduce.js └── map-reduce-incremental.js ├── summarize ├── read-lrs.js └── summarize.js ├── documentation └── interactives.md ├── nginx.conf └── config.js /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive -------------------------------------------------------------------------------- /public/stylesheets/.gitignore: -------------------------------------------------------------------------------- 1 | *.css 2 | -------------------------------------------------------------------------------- /public/images/stickers/tikzLogo.tex: -------------------------------------------------------------------------------- 1 | ../logo/tikzLogo.tex -------------------------------------------------------------------------------- /blog/XimeraGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/blog/XimeraGraphic.png -------------------------------------------------------------------------------- /public/stylesheets/standalone.scss: -------------------------------------------------------------------------------- 1 | @import "./base.scss"; 2 | 3 | body { 4 | margin: 10pt; 5 | } 6 | -------------------------------------------------------------------------------- /public/stylesheets/print.scss: -------------------------------------------------------------------------------- 1 | @media print { 2 | .chat { 3 | display: none !important; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/images/osu/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/favicon.ico -------------------------------------------------------------------------------- /public/images/osu/osu_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/osu_name.png -------------------------------------------------------------------------------- /public/images/osu/resp-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-help.png -------------------------------------------------------------------------------- /public/images/osu/resp-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-map.png -------------------------------------------------------------------------------- /public/images/osu/osu_name@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/osu_name@2x.png -------------------------------------------------------------------------------- /public/images/osu/resp-map@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-map@2x.png -------------------------------------------------------------------------------- /public/images/osu/resp-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-search.png -------------------------------------------------------------------------------- /public/images/photos/workshop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/photos/workshop.jpg -------------------------------------------------------------------------------- /public/images/icons/favicon/icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/icons/favicon/icon.xcf -------------------------------------------------------------------------------- /public/images/osu/bg-navbar_red.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/bg-navbar_red.jpg -------------------------------------------------------------------------------- /public/images/osu/button-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/button-search.png -------------------------------------------------------------------------------- /public/images/osu/resp-help@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-help@2x.png -------------------------------------------------------------------------------- /public/images/osu/resp-search@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-search@2x.png -------------------------------------------------------------------------------- /public/images/osu/resp-webmail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-webmail.png -------------------------------------------------------------------------------- /public/images/osu/white/osu_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/osu_name.png -------------------------------------------------------------------------------- /public/images/osu/white/resp-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-map.png -------------------------------------------------------------------------------- /public/images/photos/san-diego.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/photos/san-diego.jpg -------------------------------------------------------------------------------- /public/images/colorado-state/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/colorado-state/logo.png -------------------------------------------------------------------------------- /public/images/osu/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/osu/resp-buckeyelink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-buckeyelink.png -------------------------------------------------------------------------------- /public/images/osu/resp-findpeople.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-findpeople.png -------------------------------------------------------------------------------- /public/images/osu/resp-webmail@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-webmail@2x.png -------------------------------------------------------------------------------- /public/images/osu/white/resp-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-help.png -------------------------------------------------------------------------------- /public/fonts/stix/stixgeneral-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/fonts/stix/stixgeneral-webfont.eot -------------------------------------------------------------------------------- /public/fonts/stix/stixgeneral-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/fonts/stix/stixgeneral-webfont.ttf -------------------------------------------------------------------------------- /public/images/icons/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/icons/favicon/favicon.ico -------------------------------------------------------------------------------- /public/images/osu/resp-buckeyelink@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-buckeyelink@2x.png -------------------------------------------------------------------------------- /public/images/osu/resp-findpeople@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-findpeople@2x.png -------------------------------------------------------------------------------- /public/images/osu/white/bg-navbar_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/bg-navbar_red.png -------------------------------------------------------------------------------- /public/images/osu/white/osu_name@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/osu_name@2x.png -------------------------------------------------------------------------------- /public/images/osu/white/resp-help@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-help@2x.png -------------------------------------------------------------------------------- /public/images/osu/white/resp-map@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-map@2x.png -------------------------------------------------------------------------------- /public/images/osu/white/resp-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-search.png -------------------------------------------------------------------------------- /public/images/osu/white/resp-webmail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-webmail.png -------------------------------------------------------------------------------- /public/images/photos/tablet-and-book.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/photos/tablet-and-book.jpg -------------------------------------------------------------------------------- /public/fonts/stix/stixgeneral-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/fonts/stix/stixgeneral-webfont.woff -------------------------------------------------------------------------------- /public/images/osu/white/resp-findpeople.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-findpeople.png -------------------------------------------------------------------------------- /public/images/osu/white/resp-search@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-search@2x.png -------------------------------------------------------------------------------- /public/images/osu/white/resp-webmail@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-webmail@2x.png -------------------------------------------------------------------------------- /public/images/photos/calculus-textbooks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/photos/calculus-textbooks.jpg -------------------------------------------------------------------------------- /public/fonts/stix/stixgeneralbol-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/fonts/stix/stixgeneralbol-webfont.eot -------------------------------------------------------------------------------- /public/fonts/stix/stixgeneralbol-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/fonts/stix/stixgeneralbol-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/stix/stixgeneralbol-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/fonts/stix/stixgeneralbol-webfont.woff -------------------------------------------------------------------------------- /public/images/osu/osu-web-footer-wordmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/osu-web-footer-wordmark.png -------------------------------------------------------------------------------- /public/images/osu/osu-web-header-vert2-rev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/osu-web-header-vert2-rev.png -------------------------------------------------------------------------------- /public/images/osu/resp-buckeyelink-network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-buckeyelink-network.png -------------------------------------------------------------------------------- /public/images/osu/white/resp-buckeyelink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-buckeyelink.png -------------------------------------------------------------------------------- /public/images/osu/white/resp-findpeople@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-findpeople@2x.png -------------------------------------------------------------------------------- /public/fonts/stix/stixgeneralbolita-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/fonts/stix/stixgeneralbolita-webfont.eot -------------------------------------------------------------------------------- /public/fonts/stix/stixgeneralbolita-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/fonts/stix/stixgeneralbolita-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/stix/stixgeneralbolita-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/fonts/stix/stixgeneralbolita-webfont.woff -------------------------------------------------------------------------------- /public/fonts/stix/stixgeneralitalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/fonts/stix/stixgeneralitalic-webfont.eot -------------------------------------------------------------------------------- /public/fonts/stix/stixgeneralitalic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/fonts/stix/stixgeneralitalic-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/stix/stixgeneralitalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/fonts/stix/stixgeneralitalic-webfont.woff -------------------------------------------------------------------------------- /public/images/osu/white/resp-buckeyelink@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-buckeyelink@2x.png -------------------------------------------------------------------------------- /public/images/icons/favicon/.gitignore: -------------------------------------------------------------------------------- 1 | favicon-114x114.png 2 | favicon-54x54.png 3 | favicon-64x64.png 4 | favicon-72x72.png 5 | favicon.ico 6 | icon.png 7 | -------------------------------------------------------------------------------- /public/images/osu/osu-web-footer-wordmark-rev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/osu-web-footer-wordmark-rev.png -------------------------------------------------------------------------------- /public/images/osu/resp-buckeyelink-network@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/resp-buckeyelink-network@2x.png -------------------------------------------------------------------------------- /public/images/osu/white/resp-buckeyelink-network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-buckeyelink-network.png -------------------------------------------------------------------------------- /public/images/osu/white/resp-buckeyelink-network@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XimeraProject/server/HEAD/public/images/osu/white/resp-buckeyelink-network@2x.png -------------------------------------------------------------------------------- /views/layouts/navbar/due-date.pug: -------------------------------------------------------------------------------- 1 | span.navbar-text#dueDate.mr-1(style="display: none;",data-toggle="tooltip",data-placement="bottom") 2 | i.fa.fa-clock-o.mr-1 3 | span#dueDateCountdown 4 | -------------------------------------------------------------------------------- /views/layouts/navbar/palette.pug: -------------------------------------------------------------------------------- 1 | button.btn.btn-info.mx-1#math-edit-button(role="button",style="display: none;") 2 | | 3 | span.hidden-md-down  Math Editor 4 | -------------------------------------------------------------------------------- /views/layouts/navbar/another.pug: -------------------------------------------------------------------------------- 1 | button.btn.btn-success.mx-1#show-me-another-button(role="button", style="display: none") 2 | | 3 | span.hidden-md-down  Another 4 | -------------------------------------------------------------------------------- /views/layouts/navbar/erase.pug: -------------------------------------------------------------------------------- 1 | if activity 2 | button.btn.mx-1.btn-danger(type="button",data-toggle="modal", data-target="#resetWarningModal") 3 | | 4 | span.hidden-md-down  Erase 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | services: mongodb 3 | node_js: 4 | - 'node' 5 | cache: 6 | directories: 7 | - node_modules 8 | before_script: 9 | - npm install -g gulp-cli 10 | - gulp 11 | script: mocha 12 | -------------------------------------------------------------------------------- /views/layouts/navbar/search.pug: -------------------------------------------------------------------------------- 1 | form.form-inline.mr-1 2 | div.input-group 3 | input.form-control(id="xourse-search",type="search",placeholder="Search for...") 4 | span.input-group-addon 5 | i.fa.fa-search 6 | -------------------------------------------------------------------------------- /views/layouts/navbar/progress-bar.pug: -------------------------------------------------------------------------------- 1 | div.progress.completion-meter.mx-1.h-100.align-self-center(style="width: 10em;") 2 | div.progress-bar.bg-success.h-100(role="progressbar",aria-valuenow="0",aria-valuemin="0",aria-valuemax="100") 3 | -------------------------------------------------------------------------------- /views/layouts/navbar/update.pug: -------------------------------------------------------------------------------- 1 | button.btn.btn-warning.pulsate.mx-1#pageUpdate(type="button",data-toggle="modal", data-target="#updateWarningModal", style="display: none;") 2 | | 3 | span.hidden-md-down  Update 4 | -------------------------------------------------------------------------------- /public/stylesheets/guppy-fixes.scss: -------------------------------------------------------------------------------- 1 | // Make the guppy math input window as wide as the screen 2 | 3 | #guppymathModal { 4 | padding-right: 0; 5 | } 6 | 7 | #guppymathModal .modal-dialog { 8 | padding: 6pt; 9 | max-width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /views/fail.pug: -------------------------------------------------------------------------------- 1 | extends layouts/main 2 | 3 | block title 4 | | #{title} 5 | 6 | block content 7 | .container 8 | h1.text-center I am so sorry. 9 | h3.text-center An internal server error has occurred. 10 | p.text-center #{message} 11 | -------------------------------------------------------------------------------- /public/stylesheets/images.scss: -------------------------------------------------------------------------------- 1 | .image-environment { 2 | background-color: white; 3 | text-align: center; 4 | } 5 | 6 | /* BADBAD: maybe this should be controlled with tex4ht somehow */ 7 | .image-environment img { 8 | width: 75%; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /public/stylesheets/user.scss: -------------------------------------------------------------------------------- 1 | #api-secret { 2 | text-overflow: ellipsis; 3 | overflow: hidden; 4 | white-space: nowrap; 5 | } 6 | 7 | #api-key { 8 | text-overflow: ellipsis; 9 | overflow: hidden; 10 | white-space: nowrap; 11 | } 12 | -------------------------------------------------------------------------------- /views/watch.pug: -------------------------------------------------------------------------------- 1 | extends layouts/main 2 | 3 | block title 4 | | Supervise 5 | 6 | block content 7 | div.container.mt-5 8 | .row.p-2 9 | h1(role="banner") Supervise 10 | .container.ml-5.p-5 11 | main 12 | ul.list-unstyled#supervision 13 | -------------------------------------------------------------------------------- /public/stylesheets/byline.scss: -------------------------------------------------------------------------------- 1 | /* The styling for the abstract */ 2 | #byline:not(:empty):before { 3 | content:"By "; 4 | font-style:normal; 5 | display: inline; 6 | } 7 | 8 | #byline { 9 | color: #AAA; 10 | } 11 | 12 | #byline:empty { 13 | display: none; 14 | } 15 | -------------------------------------------------------------------------------- /remember.js: -------------------------------------------------------------------------------- 1 | module.exports = function (req, res, next) { 2 | // BADBAD: Does this cause terrible race conditions? Why would it? 3 | if (req.user) { 4 | req.user.lastUrlVisited = req.url; 5 | req.user.lastSeen = new Date(); 6 | req.user.save(); 7 | } 8 | next(); 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /views/layouts/navbar/statistics.pug: -------------------------------------------------------------------------------- 1 | if activity 2 | button.btn.mx-1.btn-info#instructor-view-statistics(type="button", data-activity-url=repositoryName + '/' + activity.path, data-activity-hash=activity.hash,style="display: none;") 3 | | 4 | span.hidden-md-down  Statistics 5 | -------------------------------------------------------------------------------- /public/stylesheets/tabular.scss: -------------------------------------------------------------------------------- 1 | table.tabular { 2 | @extend .table; 3 | } 4 | 5 | table.tabular th, table.tabular td { 6 | border-top: none; 7 | } 8 | 9 | tr.hline hr { 10 | display: none; 11 | } 12 | 13 | tr.hline { 14 | border-top: 1px solid black; 15 | } 16 | 17 | table.tabular td { 18 | padding: 0; 19 | } 20 | -------------------------------------------------------------------------------- /public/javascripts/sticky-scroll.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var bootstrap = require('bootstrap'); 3 | 4 | $(document).ready(function() { 5 | var offset = $("#scroller-anchor").offset(); 6 | 7 | if (offset) { 8 | $('#scroller').affix({ 9 | offset: { 10 | top: offset.top 11 | } 12 | }); 13 | } 14 | }); 15 | 16 | -------------------------------------------------------------------------------- /views/links.pug: -------------------------------------------------------------------------------- 1 | mixin courseActivityLink(course,activity) 2 | if activity 3 | a(href=course.activityURL(activity)) 4 | block 5 | else 6 | a(href="#") 7 | block 8 | 9 | 10 | mixin courseLink(course) 11 | if course 12 | a(href=`/course/${course.slug}/`) 13 | block 14 | else 15 | a(href="#") 16 | block 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | ecosystem.config.js 3 | private_key.pem 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | 12 | \#*# 13 | 14 | pids 15 | logs 16 | results 17 | 18 | *~ 19 | 20 | public/stylesheets/base.css 21 | 22 | npm-debug.log 23 | node_modules 24 | components 25 | bower_components 26 | 27 | environment.sh 28 | .env 29 | -------------------------------------------------------------------------------- /public/stylesheets/footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | margin-top: 5ex; 3 | padding-top: 5ex; 4 | } 5 | 6 | footer a { 7 | color: white; 8 | } 9 | 10 | footer ul { 11 | padding: 0; 12 | } 13 | 14 | footer ul a { 15 | display: block; 16 | } 17 | 18 | footer a:hover { 19 | color: #DDD; 20 | } 21 | 22 | footer p { 23 | margin-bottom: 0.25ex; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /views/500.pug: -------------------------------------------------------------------------------- 1 | extends layouts/main 2 | 3 | block title 4 | | 500 5 | 6 | block content 7 | .container 8 | .row.d-flex.align-items-center.justify-content-around 9 | div.mt-5 10 | h1(style="font-size: 100pt;") 500 11 | h1.text-muted I am so sorry. 12 | div.mt-5 13 | h3 An internal server error has occurred. 14 | p #{message} 15 | -------------------------------------------------------------------------------- /views/layouts/navbar/edit.pug: -------------------------------------------------------------------------------- 1 | if activity && activity.path && repositoryMetadata && repositoryMetadata.github 2 | button.btn.btn-default.mx-1(role="button",onclick=`location.href = \"https://github.com/${repositoryMetadata.github.owner}/${repositoryMetadata.github.repository}/edit/master/${activity.path}.tex\";`) 3 | | 4 | span.hidden-md-down  Edit 5 | -------------------------------------------------------------------------------- /public/stylesheets/push-down-footer.scss: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | body { 6 | /* Margin bottom by footer height */ 7 | margin-bottom: 26rem; 8 | } 9 | footer { 10 | position: absolute; 11 | bottom: 0; 12 | width: 100%; 13 | /* Set the fixed height of the footer here */ 14 | height: 23rem; 15 | background-color: #f5f5f5; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /public/javascripts/cache-bust.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | 3 | $(document).ready(function() { 4 | // Refresh to bust cache 5 | if (window.location.search.match(/^\?/)) { 6 | if ((window.history) && (window.history.pushState)) { 7 | window.history.pushState( {}, document.title, window.location.pathname ); 8 | window.location.reload(true); 9 | } 10 | } 11 | }); 12 | 13 | -------------------------------------------------------------------------------- /views/layouts/navbar/brand.pug: -------------------------------------------------------------------------------- 1 | if (atColoradoState) 2 | a.navbar-brand.hidden-sm-down(href="/math160fa17") 3 | img(src=versionPath("/public/images/logo/logo.svg"),style="max-height: 1rem; opacity: 0.5; vertical-align: baseline;") 4 | else 5 | a.navbar-brand.hidden-sm-down(href="/") 6 | img(src=versionPath("/public/images/logo/logo.svg"),style="max-height: 1rem; opacity: 0.5; vertical-align: baseline;") 7 | -------------------------------------------------------------------------------- /public/stylesheets/deprecated/clock.scss: -------------------------------------------------------------------------------- 1 | #clock { 2 | display: none; 3 | } 4 | 5 | #clock .clock-display { 6 | color: gray; 7 | } 8 | 9 | #clock .clock-second, #clock .clock-minute { 10 | color: black; 11 | display: inline-block; 12 | width: 2ch; 13 | } 14 | 15 | #clock .clock-second { 16 | text-align: left; 17 | } 18 | 19 | #clock .clock-minute { 20 | text-align: right; 21 | } 22 | -------------------------------------------------------------------------------- /views/sample.tex: -------------------------------------------------------------------------------- 1 | \begin{example} 2 | Use a linear approximation of $f(x)=\sin(x)$ 3 | at $x=0$ to approximate $\sin(0.3)$. 4 | 5 | \begin{explanation} 6 | To start, write 7 | \[ 8 | \frac{d}{dx} f(x) = \answer{\cos(x)}, 9 | \] 10 | so our linear approximation is 11 | \begin{align*} 12 | \l(x) &= \answer{\cos(0)}\cdot(x-0) + 0\\ 13 | &= x. 14 | \end{align*} 15 | \end{explanation} 16 | \end{example} 17 | -------------------------------------------------------------------------------- /public/images/ascii/ximera.ascii.txt: -------------------------------------------------------------------------------- 1 | ▀██▄ ▄██▀ ██ █████ █████ ▄███████████████████▄ ███ 2 | ▀██▄██▀ ██▐██ ▐██ ██▌ ██▌██ ██▌ ██▀██ 3 | ███ ██▐██ ██▌ ▐██ ██▌▐█████████ ▄████████▀ ██▀ ▀██ 4 | ▄██▀██▄ ██▐██ ▐██ ██▌ ██▌██ ▐█▌ ▀██▄ ██▀ ▀██ 5 | ▄██▀ ▀██▄ ██▐██ ▀███▀ ██▌▀█████████▐█▌ ▀██▄██▀ ▀██ 6 | 7 | 8 | 9 | Based on a rendering originally drawn by: 10 | http://my.asciiart.club/ 11 | -------------------------------------------------------------------------------- /public/stylesheets/dialogue.scss: -------------------------------------------------------------------------------- 1 | dl.dialogue { 2 | margin-top: 18pt; 3 | margin-bottom: 18pt; 4 | @extend .row; 5 | } 6 | 7 | dl.dialogue dt { 8 | @extend .col-md-2; 9 | } 10 | 11 | dl.dialogue dd { 12 | @extend .col-md-10; 13 | } 14 | 15 | @media screen and (min-width: 48em){ 16 | dl.dialogue dt { 17 | // @extend .text-right; but that doesn't work inside a media query 18 | text-align: right !important; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /views/layouts/includes/helpers.pug: -------------------------------------------------------------------------------- 1 | mixin breadcrumbs(links,titles) 2 | nav(aria-label="Breadcrumb",class="breadcrumb") 3 | ul.breadcrumb 4 | li 5 | a(href="/") 6 | | Home 7 | - var i = 0; 8 | - var linkCount = links.length 9 | - while ( i < linkCount - 1 ) 10 | li 11 | a(href=links[i]) 12 | | #{titles[i]} 13 | - i++; 14 | li.active(aria-current="page") 15 | | #{titles[linkCount - 1]} 16 | -------------------------------------------------------------------------------- /public_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5PvZZosTXYPanSLDWpUl 3 | rhUJwKfcdXXq01WUb7i+6A38ub1d7RTesJhpctC7aJgNtK2YlSSzlFn0qob1Rk2Q 4 | 8dbMTndQ33nnp0UhcrOWE123qy3AaWy/cskQ/uPaHMSdsV/VWBj/qsoSsap3l4Ar 5 | mg9ZSBOmIO1+aIQWcax6xDKm1/zHKh7g2fXSaeeWObaVI7GWCp5oLWfiDkvO33jn 6 | fS9AUw4JhB7nEhZKJ1l1jDYBL8GxAvhQ4YyRoQsEzSiqJR7l/B62oqs2yk5YPYQM 7 | XiEl5QE864U9wXRja//0y+dMr3JPAcH+gehEog0qJlX4piYcJHZOlUepghQbVj6z 8 | mwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /public/stylesheets/latex.scss: -------------------------------------------------------------------------------- 1 | 2 | .tex sub, .other-latex sub, .other-latex sup { 3 | text-transform: uppercase; 4 | } 5 | 6 | .tex sub, .other-latex sub { 7 | vertical-align: 0.15ex; 8 | margin-left: -0.1667em; 9 | margin-right: -0.125em; 10 | } 11 | 12 | .tex, .other-latex, .tex sub, .other-latex sub { 13 | font-size: 1em; 14 | } 15 | 16 | .other-latex sup { 17 | font-size: 0.75em; 18 | vertical-align: -0.15em; 19 | margin-left: -0.36em; 20 | margin-right: -0.15em; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /public/javascripts/image-environment.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | window.jQuery = $; 3 | 4 | $(function() { 5 | $('div.image-environment').each( function() { 6 | 7 | var imageEnvironment = $(this); 8 | 9 | $('img', imageEnvironment).each( function() { 10 | var img = $(this); 11 | var href = img.attr('src'); 12 | 13 | var link = $(''); 14 | link.attr('href', href); 15 | 16 | link.append( img ); 17 | imageEnvironment.append( link ); 18 | }); 19 | }); 20 | 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /routes/xourses.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | exports.index = function(req, res, next) { 4 | var xourses = []; 5 | 6 | if (req.repositoryMetadata === undefined) { 7 | next(null); 8 | return; 9 | } 10 | 11 | Object.keys(req.repositoryMetadata.xourses).forEach( function(xoursePath) { 12 | var x = req.repositoryMetadata.xourses[xoursePath]; 13 | x.path = xoursePath; 14 | xourses.push( x ); 15 | }); 16 | 17 | res.render('xourses/index', { 18 | repositoryName: req.repositoryName, 19 | xourses: xourses 20 | } ); 21 | }; 22 | -------------------------------------------------------------------------------- /views/page/global-preamble.pug: -------------------------------------------------------------------------------- 1 | div(style="display: none;") 2 | script(type="math/tex") 3 | | \newcommand{\arraycolsep}[3]{} 4 | | \newcommand{\dd}[2][]{\frac{d #1}{d #2}} 5 | | \newcommand{\pp}[2][]{\frac{\partial #1}{\partial #2}} 6 | | \newcommand{\veci}{\vec{\hat{\bf\imath}}} 7 | | \newcommand{\vecj}{\vec{\hat{\bf\jmath}}} 8 | | \newcommand{\utan}{\vec{\hat{t}}} 9 | | \newcommand{\unormal}{\vec{\hat{n}}} 10 | | \newenvironment{amatrix}[1]{% 11 | | \begin{bmatrix} 12 | | }{% 13 | | \end{bmatrix} 14 | | } 15 | -------------------------------------------------------------------------------- /public/stylesheets/abstract.scss: -------------------------------------------------------------------------------- 1 | /* The styling for the abstract */ 2 | .abstract p:first-child:before { 3 | font-weight:bold; 4 | content:"Abstract."; 5 | font-style:normal; 6 | display: inline; 7 | padding-right: 0.5em; 8 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 9 | } 10 | 11 | .abstract { 12 | text-align:justify; 13 | font-style:italic; 14 | margin-top:2em; 15 | margin-bottom:2em; 16 | color: gray; 17 | } 18 | 19 | .abstract:empty { 20 | display: none; 21 | } 22 | -------------------------------------------------------------------------------- /views/page/navigation.pug: -------------------------------------------------------------------------------- 1 | include ../activity-card.pug 2 | 3 | if (activity.xourse.activityList.length > 0) 4 | nav(role="navigation",aria-label="textbook") 5 | div.kinetic.course-navigation.card-group.scroll(data-commit=activity.commit, data-points=activity.xourse.totalPoints, data-xourse-url=`${repositoryName}/${activity.xourse.path}`) 6 | for activityUrl in activity.xourse.activityList 7 | +xourseCard(repositoryName, activity.xourse.path, activityUrl, activity.xourse.activities[activityUrl], activityUrl == activity.path) 8 | div.scrollbar 9 | div.handle 10 | -------------------------------------------------------------------------------- /views/404.pug: -------------------------------------------------------------------------------- 1 | extends layouts/main 2 | 3 | block title 4 | | 404 5 | 6 | block content 7 | .container 8 | .row.d-flex.align-items-center.justify-content-around 9 | div.mt-5 10 | h1(style="font-size: 100pt;") 404 11 | h1.text-muted I am so sorry. 12 | div.mt-5 13 | h3 The page you want could not be found. 14 | p I could not find the page #{url} but I will look into the cause of this error. 15 | if repositoryName 16 | p In the meantime, perhaps look at #{repositoryName} to find what you seek. -------------------------------------------------------------------------------- /test/four-oh-fours.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('supertest'); 4 | var app = require('../app').app; 5 | var should = require('should'); 6 | 7 | describe('index page', function () { 8 | it('should provide some html', function (done) { 9 | request(app) 10 | .get('/') 11 | .set('Accept', 'text/html') 12 | .expect(200, done); 13 | }); 14 | }); 15 | 16 | describe('missing routes', function () { 17 | it('should provide an error', function (done) { 18 | request(app) 19 | .get('/nothing-is-here') 20 | .set('Accept', 'text/html') 21 | .expect(404, done); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /public/javascripts/rowclick.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | 3 | // transforms any bootstrap table row that has data-href attribute set into a clickable element 4 | exports.addClickableTableRows = function() { 5 | $('.table tr[data-href]').each(function(){ 6 | $(this).css('cursor','pointer').hover( 7 | function(){ 8 | $(this).addClass('active'); 9 | }, 10 | function(){ 11 | $(this).removeClass('active'); 12 | }).click( function(){ 13 | document.location = $(this).attr('data-href'); 14 | } 15 | ); 16 | }); 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /views/source.pug: -------------------------------------------------------------------------------- 1 | extends layouts/main 2 | 3 | block title 4 | | #{file.path} 5 | 6 | block content 7 | div.container 8 | div.pull-right.btn-toolbar(role="toolbar") 9 | //.btn-group 10 | // a.btn.btn-info(href="../") 11 | // | Do Activity 12 | //.btn-group 13 | // a.btn.btn-primary(href="https://github.com/#{activity.slug.split(':')[0]}/blob/master/#{activity.relativePath}") 14 | // | View on GitHub 15 | 16 | .page-header 17 | h1 #{file.path} 18 | small  source code 19 | pre(class="brush: latex;") 20 | | #{file.data} 21 | -------------------------------------------------------------------------------- /routes/statistics.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var config = require('../config'); 3 | var path = require('path'); 4 | 5 | var lrsRoot = config.repositories.root; 6 | 7 | // BADBAD: This is horribly slow. 8 | exports.get = function(req, res, next) { 9 | console.log( req.params.repository ); 10 | var filename = path.join( lrsRoot, req.params.repository + ".git", "summary.json" ); 11 | var activityHash = req.params.activityHash; 12 | 13 | fs.readFile(filename, 'utf8', function (err, data) { 14 | if (err) 15 | next(err); 16 | else { 17 | var summary = JSON.parse(data); 18 | res.json( summary.activities[activityHash] ); 19 | } 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /public/stylesheets/hints.scss: -------------------------------------------------------------------------------- 1 | .hint { 2 | color: gray; 3 | margin-left: 1em; 4 | } 5 | 6 | .btn-reveal-hint { 7 | float: right; 8 | clear: right; 9 | margin-bottom: 1em; 10 | margin-left: 0.5em; 11 | 12 | /* Sometimes mathjax elements cover up the hint collapse button */ 13 | z-index: 1; 14 | position: relative; 15 | } 16 | 17 | .btn-hint-collapse i { 18 | transition-duration: 0.2s; 19 | transition-property: transform; 20 | } 21 | 22 | .btn-hint-collapse { 23 | float: right; 24 | clear: right; 25 | margin: 0.25em; 26 | margin-right: 0; 27 | padding: 0; 28 | /* Sometimes mathjax elements cover up the hint collapse button */ 29 | z-index: 1; 30 | position: relative; 31 | } 32 | -------------------------------------------------------------------------------- /views/xourses/index.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/main 2 | 3 | block title 4 | | #{repositoryName} 5 | 6 | block content 7 | nav(aria-label="breadcrumb",role="navigation") 8 | ol.breadcrumb 9 | li.breadcrumb-item.active 10 | | !{repositoryName} 11 | 12 | .container.ml-5.p-3.d-flex.align-items-start 13 | for xourse in xourses 14 | if xourse.title 15 | .card.m-2.xourse-card(style="width: 30%;") 16 | a(href=`/${repositoryName}/${xourse.path}`) 17 | if xourse.logo 18 | img.card-img-top(src=`${repositoryName}/${xourse.logo}`,style="width: 100%;") 19 | else 20 | img.card-img-top(style="width: 100%;") 21 | .card-block 22 | h4.card-title #{xourse.title} 23 | p.card-text !{xourse.abstract} 24 | 25 | -------------------------------------------------------------------------------- /public/javascripts/desmos.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | 3 | var DesmosNeeded = false; 4 | 5 | exports.promise = $.Deferred(); 6 | 7 | exports.onReady = function( callback ) { 8 | $.when(exports.promise).done( callback ); 9 | }; 10 | 11 | exports.loadAsynchronously = function() { 12 | if (DesmosNeeded == false) { 13 | DesmosNeeded = true; 14 | 15 | $.getScript( "https://www.desmos.com/api/v0.7/calculator.js?apiKey=dcb31709b452b1cf9dc26972add0fda6", 16 | function() { 17 | function waitForDesmos(){ 18 | if(typeof window.Desmos !== "undefined"){ 19 | exports.promise.resolve( window.Desmos ); 20 | } else { 21 | setTimeout(function(){ 22 | waitForDesmos(); 23 | },250); 24 | } 25 | } 26 | 27 | waitForDesmos(); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /public/javascripts/annotator.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var annotator = require('annotator'); 3 | 4 | var activityHash = function () { 5 | return { 6 | beforeAnnotationCreated: function (ann) { 7 | // get the actual activity hash 8 | ann.activityHash = window.location.href; 9 | } 10 | }; 11 | }; 12 | 13 | function addAnnotator() { 14 | var app = new annotator.App(); 15 | app.include(annotator.ui.main, {element: this}) 16 | .include(annotator.storage.http, 17 | {prefix: 'http://example.com/api'}) 18 | .include(activityHash); 19 | app.start() 20 | .then(function () { 21 | app.annotations.load({activityHash: window.location.href}); 22 | }); 23 | } 24 | 25 | $.fn.extend({ 26 | annotator: function() { 27 | return this.each( addAnnotator ); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /views/modals/reset.pug: -------------------------------------------------------------------------------- 1 | .modal.fade#resetWarningModal(tabindex="-1",role="dialog",aria-labelledby="updateWarningModal") 2 | .modal-dialog(role="document") 3 | .modal-content 4 | .modal-header 5 | h5.modal-title Warning 6 | button.close(type="button",data-dismiss="modal",aria-label="Close") 7 | span(aria-hidden="true") × 8 | .modal-body 9 | p You are about to erase your work on this activity. Are you sure you want to do this? 10 | .modal-footer 11 | button.btn.btn-primary(type="button",data-dismiss="modal") 12 | |  No, keep my work. 13 | button.btn.btn-danger#reset-work-button(type="button", data-dismiss="modal") 14 | |  Yes, delete my work. 15 | -------------------------------------------------------------------------------- /views/lti/passback.pug: -------------------------------------------------------------------------------- 1 | doctype xml 2 | imsx_POXEnvelopeRequest(xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0") 3 | imsx_POXHeader 4 | imsx_POXRequestHeaderInfo 5 | imsx_version V1.0 6 | if messageIdentifier 7 | imsx_messageIdentifier #{messageIdentifier} 8 | imsx_POXBody 9 | replaceResultRequest 10 | resultRecord 11 | if sourcedId 12 | sourcedGUID 13 | sourcedId #{sourcedId} 14 | result 15 | if resultDataUrl 16 | resultData 17 | url #{resultDataUrl} 18 | if resultTotalScore 19 | resultTotalScore 20 | language en 21 | textString #{resultTotalScore} 22 | if resultScore 23 | resultScore 24 | language en 25 | textString #{resultScore} 26 | -------------------------------------------------------------------------------- /views/modals/update-xourse.pug: -------------------------------------------------------------------------------- 1 | .modal.fade#updateWarningModal(tabindex="-1",role="dialog",aria-labelledby="updateWarningModal") 2 | .modal-dialog(role="document") 3 | .modal-content 4 | .modal-header 5 | h5(class="modal-title",id="updateModalLabel") Updated Version Available 6 | button.close(type="button",data-dismiss="modal",aria-label="Close") 7 | span(aria-hidden="true") × 8 | .modal-body 9 | p There is an updated version of this page. How would you like to proceed? 10 | .modal-footer 11 | button.btn.btn-secondary(type="button",data-dismiss="modal") 12 | |  Stay with this old version. 13 | a.btn.btn-primary#update-version-button(href="#") 14 | |  Update to the new version. 15 | -------------------------------------------------------------------------------- /views/modals/confirm-deletion.pug: -------------------------------------------------------------------------------- 1 | div.modal.fade#confirm-delete(tabindex="-1",role="dialog",aria-labelledby="confirm-delete") 2 | div.modal-dialog(role="document") 3 | div.modal-content 4 | div.modal-header 5 | h5(class="modal-title") Confirm Deletion 6 | button.close(type="button",data-dismiss="modal",aria-label="Close") 7 | span(aria-hidden="true") × 8 | div.modal-body 9 | p Are you sure you want to disconnect your account from your Learning Management System? 10 | div.modal-footer 11 | button.btn.btn-default(type="button", data-dismiss="modal") 12 | |  No, I want to stay connected 13 | button.btn.btn-danger#disconnect-button 14 | |  Yes, disconnect my account. 15 | -------------------------------------------------------------------------------- /views/layouts/navbar/save.pug: -------------------------------------------------------------------------------- 1 | if activity 2 | button.btn.btn-primary.mx-1(type="button", id="save-work-button") 3 | span(style="display: none;", id="work-error") 4 | | 5 | span.hidden-md-down  Failed 6 | span(style="display: none;", id="work-saved") 7 | | 8 | span.hidden-md-down  Saved! 9 | span(style="display: none;", id="work-saving") 10 | | 11 | span.hidden-md-down  Saving… 12 | span(style="display: none;", id="work-reconnecting") 13 | | 14 | span.hidden-md-down  Reconnecting… 15 | span(id="work-save") 16 | | 17 | span.hidden-md-down  Save 18 | -------------------------------------------------------------------------------- /views/layouts/navbar/index.pug: -------------------------------------------------------------------------------- 1 | //div#scroller-anchor 2 | nav.navbar.sticky-top.navbar-toggleable.navbar-light.bg-faded(role='navigation') 3 | button.navbar-toggler.navbar-toggler-right(type="button",data-toggle="collapse",data-target="#navbarNav",aria-controls="navbarNav",aria-expanded="false",aria-label="Toggle navigation") 4 | span.navbar-toggler-icon 5 | include brand 6 | .navbar-collapse.collapse#navbarNav 7 | ul.navbar-nav 8 | include due-date 9 | include statistics 10 | if !landingPage 11 | include get-help 12 | ul.navbar-nav.ml-auto 13 | if xourse && (!instructors) 14 | include search 15 | include another 16 | include palette 17 | include save 18 | include update 19 | include erase 20 | include edit 21 | if activity 22 | include progress-bar 23 | include login 24 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ $(hostname) = ximera-1.asc.ohio-state.edu ]; then 3 | echo On the deployment machine. 4 | echo Pulling latest version from github, protecting our dotenv... 5 | mv -f .env .env.backup 6 | git pull 7 | mv -f .env.backup .env 8 | echo Updating npm... 9 | npm install 10 | echo Running gulp... 11 | node ./node_modules/gulp/bin/gulp.js js 12 | node ./node_modules/gulp/bin/gulp.js service-worker 13 | node ./node_modules/gulp/bin/gulp.js css 14 | echo Stopping old copies of app.js... 15 | pm2 stop ximera 16 | echo Starting a new copy of app.js... 17 | pm2 start ecosystem.config.js --env production 18 | else 19 | echo not on the deployment machine... 20 | echo copying environment and keys to deployment machine... 21 | rsync -avz -f"- .git/" private_key.pem .env ximera:/var/www/apps/ximera 22 | ssh ximera "cd /var/www/apps/ximera ; source deploy.sh" 23 | fi 24 | -------------------------------------------------------------------------------- /routes/etag.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | function readIfNoneMatch( req ) { 4 | var header = req.get('If-None-Match'); 5 | 6 | if (!header) return []; 7 | // Ignore the weakness of any tags 8 | header = header.replace( /W\/\"/, "\"" ); 9 | 10 | try { 11 | return JSON.parse("[" + header + "]"); 12 | } catch(e) { 13 | return [header]; 14 | } 15 | } 16 | 17 | exports.checkIfNoneMatch = function( req, res, etag, callback ) { 18 | // If the requester claims to already have our hash... 19 | if (_.some( readIfNoneMatch(req), h => h == etag )) { 20 | // Then tell the browser that they're good to go. 21 | res.set({ 'ETag': '"' + etag + '"' }); 22 | res.sendStatus(304); 23 | return; 24 | } 25 | 26 | // If not, call the callback to expensively recreate our content... 27 | callback( function( res ) { 28 | // And help the callback to set the etag 29 | res.set({ 'ETag': '"' + etag + '"' }); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /views/layouts/includes/osu.pug: -------------------------------------------------------------------------------- 1 | div#osu_navbar(role="navigation") 2 | h2#osu_navbar_heading.osu-semantic Ohio State nav bar 3 | a#skip.osu-semantic(href="#page-content") Skip to main content 4 | div.container 5 | div.univ_info 6 | p.univ_name 7 | a(href="http://osu.edu",title="The Ohio State University") The Ohio State University 8 | div.univ_links 9 | div.links 10 | ul 11 | li 12 | a(href="http://www.osu.edu/help.php",class="help") Help 13 | li 14 | a(href="http://buckeyelink.osu.edu/",class="buckeyelink") BuckeyeLink 15 | li 16 | a(href="http://www.osu.edu/map/",class="map") Map 17 | li 18 | a(href="http://www.osu.edu/findpeople.php",class="findpeople") Find People 19 | li 20 | a(href="https://email.osu.edu/",class="webmail") Webmail 21 | li 22 | a(href="http://www.osu.edu/search/",class="search") Search Ohio State 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/stylesheets/pulsate.scss: -------------------------------------------------------------------------------- 1 | $pulsate-list: btn-primary, btn-success, btn-info, btn-warning, btn-danger, btn-inverse, page-link; 2 | 3 | $pulsate-colors: ( 4 | btn-primary: $brand-primary, 5 | btn-success: $brand-success, 6 | btn-info: $brand-info, 7 | btn-warning: $brand-warning, 8 | btn-danger: $brand-danger, 9 | btn-inverse: $brand-inverse, 10 | page-link: $brand-primary 11 | ); 12 | 13 | @each $pulsator in $pulsate-list { 14 | 15 | .disabled .#{$pulsator}.pulsate { 16 | animation: none; 17 | text-shadow: none; 18 | } 19 | 20 | .#{$pulsator}.pulsate { 21 | animation: pulsate-#{$pulsator} 0.5s infinite alternate; 22 | animation-timing-function: linear; 23 | text-shadow: 0 0 8px #ccc; 24 | } 25 | 26 | @keyframes pulsate-#{$pulsator} { 27 | from { box-shadow: 0 0 10px #333; transform: rotate(-2deg); } 28 | to { box-shadow: 0 0 20px map-get($pulsate-colors,$pulsator); transform: rotate(2deg); } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /public/stylesheets/flash.scss: -------------------------------------------------------------------------------- 1 | .flash { 2 | -moz-animation: flash 5s ease-out; 3 | -moz-animation-iteration-count: 1; 4 | 5 | -webkit-animation: flash 5s ease-out; 6 | -webkit-animation-iteration-count: 1; 7 | 8 | -ms-animation: flash 5s ease-out; 9 | -ms-animation-iteration-count: 1; 10 | } 11 | 12 | @keyframes flash { 13 | 0% { background-color: transparent; } 14 | 50% { background-color: #fbf8b2; } 15 | 100% { background-color: transparent; } 16 | } 17 | 18 | @-webkit-keyframes flash { 19 | 0% { background-color: transparent; } 20 | 50% { background-color: #fbf8b2; } 21 | 100% { background-color: transparent; } 22 | } 23 | 24 | @-moz-keyframes flash { 25 | 0% { background-color: transparent; } 26 | 50% { background-color: #fbf8b2; } 27 | 100% { background-color: transparent; } 28 | } 29 | 30 | @-ms-keyframes flash { 31 | 0% { background-color: transparent; } 32 | 50% { background-color: #fbf8b2; } 33 | 100% { background-color: transparent; } 34 | } 35 | -------------------------------------------------------------------------------- /views/install.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | echo "Create a texmf directory..." 4 | mkdir -p ~/texmf/tex/latex 5 | 6 | echo "Clone or update ximeraLatex..." 7 | (cd ~/texmf/tex/latex && git clone https://github.com/XimeraProject/ximeraLatex.git) || (cd ~/texmf/tex/latex/ximeraLatex && git pull) 8 | 9 | echo "Download mutool from a repository..." 10 | mkdir -p ~/.local/bin 11 | wget -qO- https://www.archlinux.org/packages/community/x86_64/mupdf-tools/download/ | tar -xJf - usr/bin/mutool -O > ~/.local/bin/mutool 12 | chmod +x ~/.local/bin/mutool 13 | 14 | echo "Clone or update xake..." 15 | (cd ~ && git clone https://github.com/XimeraProject/xake.git) || (cd ~/xake && git pull) 16 | 17 | echo "Install dependencies for xake..." 18 | cd ~/xake && npm install 19 | 20 | echo "Add the 'xake' command..." 21 | mkdir -p ~/.local/bin 22 | ln -s ~/xake/app.js ~/.local/bin/xake 23 | 24 | echo "Download some demo content..." 25 | (cd ~ && git clone https://github.com/xandbox/xandbox.git) || (cd ~/xandbox && git pull) 26 | 27 | echo "Done." 28 | -------------------------------------------------------------------------------- /views/certificate/xourse.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/main 2 | 3 | block title 4 | | #{xourse.title} 5 | 6 | block modals 7 | 8 | block content 9 | div.container 10 | .page-header 11 | h1 #{xourse.title} 12 | 13 | div 14 | - var percent = function(p) { return (p * 100).toFixed(2).toString() + '%'; }; 15 | dl.certificate 16 | for activityUrl in xourse.activityList 17 | - var activity = xourse.activities[activityUrl]; 18 | if activityUrl.match(/^#/) 19 | dt.part !{activity.title} 20 | dd   21 | else 22 | dt.page 23 | a(href=activityUrl) !{activity.title} 24 | dd #{percent(activity.completion)} 25 | hr 26 | dt.total Overall 27 | dd #{score.toString() + '%'} 28 | 29 | p 30 | a(href=`/certificate/${escapedCode}/${escapedSignature}`) 31 | span.certificate https://ximera.osu.edu/certificate/#{escapedCode}/#{escapedSignature} 32 | -------------------------------------------------------------------------------- /views/modals/update.pug: -------------------------------------------------------------------------------- 1 | .modal.fade#updateWarningModal(tabindex="-1",role="dialog",aria-labelledby="updateWarningModal") 2 | .modal-dialog(role="document") 3 | .modal-content 4 | .modal-header 5 | h5(class="modal-title",id="updateModalLabel") Updated Version Available 6 | button.close(type="button",data-dismiss="modal",aria-label="Close") 7 | span(aria-hidden="true") × 8 | .modal-body 9 | p There is an updated version of this activity. If you update to the most recent version of this activity, then your current progress on this activity will be erased. Regardless, your record of completion will remain. How would you like to proceed? 10 | .modal-footer 11 | button.btn.btn-default(type="button",data-dismiss="modal") 12 | |  Keep the old version. 13 | a.btn.btn-warning#update-version-button(href="#") 14 | |  Delete my work and update to the new version. 15 | -------------------------------------------------------------------------------- /views/page/next-and-previous.pug: -------------------------------------------------------------------------------- 1 | if previousActivity || nextActivity 2 | nav.my-4(aria-label="page navigation") 3 | ul.pagination.justify-content-start 4 | li.page-item#previous-activity(class=(previousActivity ? undefined : "disabled")) 5 | if previousActivity 6 | a.page-link(href="/" + repositoryName + "/" + activity.xourse.path + "/" + previousActivity,aria-label="previous activity") 7 | | ← Previous 8 | else 9 | span.page-link(aria-label="previous activity",href="#") 10 | | ← Previous 11 | li.page-item.ml-auto#next-activity(class=(nextActivity ? undefined : "disabled"),completion-blink="",aria-label="next activity") 12 | if nextActivity 13 | a.page-link(href="/" + repositoryName + "/" + activity.xourse.path + "/" + nextActivity) 14 | | Next → 15 | else 16 | span.page-link(aria-label="next activity") 17 | | Next → 18 | -------------------------------------------------------------------------------- /blog/shuttleworth.markdown: -------------------------------------------------------------------------------- 1 | {{{ 2 | "title" : "Snapp awarded Shuttleworth Flash Grant", 3 | "tags" : [ "shuttleworth foundation" ], 4 | "category" : "ximera funding", 5 | "date" : "6-01-2014", 6 | "author" : "Bart Snapp" 7 | }}} 8 | 9 | 10 | I'm pleased to announce that I've received a Shuttleworth Flash Grant! 11 | This grant is given to help burgeoning ideas come to life. We are 12 | excited to use this to help support the Ximera project. 13 | 14 | 15 | 16 | Our current thought is that these funds will be best used to help 17 | build a community of developers and authors who work with Ximera. 18 | 19 | While content in mathematics might seem like a slow-moving target, the 20 | fact is that quality content is produced through iterative 21 | development. When others can develop content and contribute to content 22 | that is already deployed, we (as a community) are able to produce 23 | superior materials that will have relevance despite the ephemeral 24 | nature of the internet. 25 | 26 | Our first workshop is July 28th to the 30th. I'm looking forward to 27 | it! 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/javascripts/brushes/shBrushLatex.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Improved version of the very simple LaTeX brush from 3 | * http://www.jorgemarsal.com/blog/ 4 | * some code from Gheorghe Milas and Ahmad Sherif 5 | */ 6 | 7 | var BrushBase = require('brush-base'); 8 | var regexLib = require('syntaxhighlighter-regex').commonRegExp; 9 | 10 | function Brush() { 11 | var keywords = 'break continue case return in eq ne gt lt ge le'; 12 | 13 | this.regexList = [ 14 | // one line comments 15 | { regex: new RegExp('%.*','gm'), 16 | css: 'comments' }, 17 | // double quoted strings 18 | { regex: regexLib.doubleQuotedString, 19 | css: 'string' }, 20 | // commands 21 | { regex: new RegExp('\\\\\\w*','gm'), 22 | css: 'keyword' }, 23 | // commands 24 | { regex: new RegExp('\\$[^\\$]+\\$','gm'), 25 | css: 'color2' }, 26 | // keywords 27 | { regex: new RegExp(this.getKeywords(keywords), 'gm'), 28 | css: 'function' }, 29 | ]; 30 | 31 | this.forHtmlScript(regexLib.scriptScriptTags); 32 | } 33 | 34 | Brush.prototype = new BrushBase(); 35 | Brush.aliases = ['tex', 'latex', 'LaTeX', 'TeX']; 36 | module.exports = Brush; 37 | -------------------------------------------------------------------------------- /public/stylesheets/chat.scss: -------------------------------------------------------------------------------- 1 | .chat { 2 | position: fixed; 3 | bottom: 0; 4 | right: 0; 5 | width: 30vw; 6 | background-color: white; 7 | z-index: 9000; 8 | border-top: black 1px solid; 9 | border-left: black 1px solid; 10 | } 11 | 12 | .chat h5 { 13 | margin: 1ex; 14 | } 15 | 16 | .chat .transcript { 17 | border: none; 18 | border-top: #eee 1px solid; 19 | border-bottom: #eee 1px solid; 20 | max-height: 144pt; 21 | padding: 1ex; 22 | overflow: hidden; 23 | overflow-y: scroll; 24 | margin: 0; 25 | } 26 | 27 | .chat input { 28 | width: 100%; 29 | border: none; 30 | padding: 2pt; 31 | } 32 | 33 | 34 | .chat dl { 35 | } 36 | 37 | .chat dl dt { 38 | float: left; 39 | margin-right: 1em; 40 | color: #aaa; 41 | } 42 | 43 | .chat dl dd { 44 | border-top: #eee 1px solid; 45 | display: flex; 46 | } 47 | 48 | 49 | .chat dl dt.other { 50 | float: right; 51 | margin: 0; 52 | margin-left: 1em; 53 | text-align: right; 54 | clear: right; 55 | } 56 | 57 | .chat dl dd.other { 58 | background: white; 59 | text-align: right; 60 | } 61 | -------------------------------------------------------------------------------- /public/javascripts/version.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | 3 | // Check to see if there is a newer version available 4 | var version = require('../../package.json').version; 5 | console.log("This is XIMERA, Version " + version ); 6 | 7 | $(function() { 8 | // Check which version the server is providing, avoiding the cache 9 | $.ajax( "/version?" + (new Date().getTime()) ) 10 | .done(function(data) { 11 | // If the server can offer a newer version, let's update 12 | if (data != version) { 13 | if (sessionStorage.getItem('refreshedToVersion') == data) { 14 | alert('Attempted to refresh; try a force refresh.'); 15 | } else { 16 | sessionStorage.setItem('refreshedToVersion', data); 17 | 18 | console.log("Uppdating from version " + version + " to version " + data ); 19 | 20 | // This is MAYBE needed in Chrome 21 | $.ajax({ 22 | url: window.location.href, 23 | headers: { 24 | "Pragma": "no-cache", 25 | "Expires": -1, 26 | "Cache-Control": "no-cache" 27 | } 28 | }).done(function () { 29 | window.location.reload(true); 30 | }); 31 | } 32 | } 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /public/javascripts/instructor.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | 3 | function announce( hash, answers ) { 4 | var selector = function(hash, problem, answerable) { 5 | return "[data-hash='" + hash + "'] " + "#" + problem + " #" + answerable; 6 | }; 7 | 8 | Object.keys(answers).forEach( function(problem) { 9 | Object.keys(answers[problem]).forEach( function(answerable) { 10 | var element = $(selector(hash, problem, answerable)); 11 | var statistics = answers[problem][answerable]; 12 | 13 | element.trigger( "ximera:statistics:answers", statistics.responses ); 14 | element.trigger( "ximera:statistics:successes", statistics.successes ); 15 | }); 16 | }); 17 | } 18 | 19 | $(function() { 20 | $("#instructor-view-statistics").click( function() { 21 | $("#instructor-view-statistics").hide(); 22 | 23 | var url = $(this).attr('data-activity-url'); 24 | var hash = $(this).attr('data-activity-hash'); 25 | 26 | $.ajax({ 27 | url: '/statistics/' + url + '/' + hash, 28 | type: 'GET', 29 | success: function(result) { 30 | if (result) 31 | announce( hash, result ); 32 | } 33 | }); 34 | }); 35 | }); 36 | 37 | 38 | -------------------------------------------------------------------------------- /blog/xw1Day3.markdown: -------------------------------------------------------------------------------- 1 | {{{ 2 | "title" : "Ximera Workshop 1: Day 3", 3 | "tags" : [ "shuttleworth foundation, nsf, workshop" ], 4 | "category" : "ximera workshop", 5 | "date" : "7-30-2014", 6 | "author" : "Bart Snapp" 7 | }}} 8 | 9 | Today was the final day of the Ximera Workshop 1. While three days is 10 | not much time, I am very impressed with how much we have 11 | achieved. Thank you so much to everyone involved, and thanks again to 12 | the NSF and the Shuttleworth Foundation for making this possible. 13 | 14 | 15 | 16 | With impassioned discussion, we laid the ground work for the future 17 | of Ximera. This is not to say that we didn't achieve much now, quite 18 | the contrary: Randy Pirum prototype a Ximera Data Viewer, Chris 19 | Cunningham showed an Ximera activity that could identify units, and 20 | give constructive feedback to the students, and Chris Hill explored 21 | the creation of Ximera involving English language questions. 22 | 23 | I cannot express how excited I am to see how others use this 24 | platform. It is my hope that we can provide tools that allow content 25 | authors to unleash their own creativity, and build something that has 26 | never before been seen. 27 | 28 | -------------------------------------------------------------------------------- /public/images/icons/favicon/Makefile: -------------------------------------------------------------------------------- 1 | all: favicon-114x114.png favicon-72x72.png favicon-54x54.png favicon.ico 2 | 3 | clean: 4 | rm -f favicon-114x114.png favicon-72x72.png favicon-54x54.png favicon.ico 5 | 6 | favicon-114x114.png: icon.png 7 | convert -geometry 114x114 icon.png favicon-114x114.png 8 | 9 | favicon-72x72.png: icon.png 10 | convert -geometry 72x72 icon.png favicon-72x72.png 11 | 12 | favicon-54x54.png: icon.png 13 | convert -geometry 54x54 icon.png favicon-54x54.png 14 | 15 | favicon-64x64.png: icon.png 16 | convert -geometry 64x64 icon.png favicon-64x64.png 17 | 18 | favicon.ico: favicon-64x64.png 19 | convert favicon-64x64.png -bordercolor white -border 0 \ 20 | \( -clone 0 -resize 16x16 \) \ 21 | \( -clone 0 -resize 32x32 \) \ 22 | \( -clone 0 -resize 48x48 \) \ 23 | \( -clone 0 -resize 64x64 \) \ 24 | -delete 0 -alpha off -colors 256 favicon.ico 25 | 26 | icon.png: icon.xcf 27 | echo '(let* ((image (car (gimp-file-load RUN-NONINTERACTIVE "icon.xcf" "icon.xcf"))) (layer (car (gimp-image-merge-visible-layers image CLIP-TO-IMAGE))) (filename "icon.png")) (gimp-file-save RUN-NONINTERACTIVE image layer filename filename) (gimp-image-delete image) (gimp-quit 0))' | gimp -n -i -b - 28 | -------------------------------------------------------------------------------- /routes/cachify.js: -------------------------------------------------------------------------------- 1 | var redis = require('redis'); 2 | 3 | // create a new redis client and connect to our local redis instance 4 | var client = redis.createClient({return_buffers: true}); 5 | 6 | // if an error occurs, print it to the console 7 | client.on('error', function (err) { 8 | console.log("Error " + err); 9 | }); 10 | 11 | exports.json = function( key, f, callback ) { 12 | client.get(key, function(err, result) { 13 | if (err) { 14 | callback(err); 15 | } else { 16 | if (result) { 17 | callback( null, JSON.parse(result) ); 18 | } else { 19 | f( function(err, result) { 20 | if (err) { 21 | callback( err ); 22 | } else { 23 | client.setex( key, 31557600, JSON.stringify(result) ); 24 | callback( null, result ); 25 | } 26 | }); 27 | } 28 | } 29 | }); 30 | }; 31 | 32 | exports.string = function( key, f, callback ) { 33 | client.get(key, function(err, result) { 34 | if (err) { 35 | callback(err); 36 | } else { 37 | if (result) { 38 | callback( null, result ); 39 | } else { 40 | f( function(err, result) { 41 | if (err) { 42 | callback( err ); 43 | } else { 44 | client.setex( key, 31557600, result ); 45 | callback( null, result ); 46 | } 47 | }); 48 | } 49 | } 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /routes/hashcash.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | var crypto = require('crypto'); 3 | 4 | var REQUIRED_COST = 20; 5 | 6 | function verify( resource, hashcash ) { 7 | if (resource != hashcash.split(":")[3]) 8 | return 0; 9 | 10 | var shasum = crypto.createHash('sha1'); 11 | shasum.update(hashcash); 12 | var buffer = new Buffer(shasum.digest('hex'),"hex"); 13 | 14 | var bits = 0; 15 | for (const b of buffer) { 16 | if (b == 0) bits += 8; 17 | if (b != 0) { 18 | if (b & 0x80) return bits; 19 | if (b & 0x40) return bits + 1; 20 | if (b & 0x20) return bits + 2; 21 | if (b & 0x10) return bits + 3; 22 | if (b & 0x08) return bits + 4; 23 | if (b & 0x04) return bits + 5; 24 | if (b & 0x02) return bits + 6; 25 | if (b & 0x01) return bits + 7; 26 | } 27 | } 28 | 29 | return bits; 30 | } 31 | 32 | exports.hashcash = function(req,res,next) { 33 | if (req.get('X-Hashcash')) { 34 | var hashcash = req.get('X-Hashcash'); 35 | console.log( "hashcash = " + hashcash ); 36 | var pathname = url.parse(req.url).pathname.substr(1); 37 | if (verify( pathname, hashcash ) < REQUIRED_COST) 38 | res.status(400).send("Not enough hashcash attached."); 39 | else 40 | next(); 41 | } else { 42 | res.status(400).send("No hashcash attached."); 43 | return; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /views/page/breadcrumbs.pug: -------------------------------------------------------------------------------- 1 | nav(aria-label="breadcrumb",role="navigation") 2 | ol.breadcrumb 3 | - var level = 0; 4 | li.breadcrumb-item 5 | - level = level + 1; 6 | a(href="/" + repositoryName, aria-level=level) 7 | | !{repositoryName} 8 | if activity.xourse 9 | li.breadcrumb-item 10 | - level = level + 1; 11 | a(href="/" + repositoryName + "/" + activity.xourse.path,aria-level=level) 12 | if (activity.xourse.title) && (activity.xourse.title != '') 13 | | !{activity.xourse.title} 14 | else 15 | | Home 16 | if activity.chapter 17 | - level = level + 1; 18 | li.breadcrumb-item 19 | a(href="/" + repositoryName + "/" + activity.xourse.path + "/" + activity.chapter.href,aria-level=level) !{activity.chapter.title} 20 | - level = level + 1; 21 | li.breadcrumb-item.active(aria-current="page",aria-level=level) 22 | if activity.title 23 | | !{activity.title} 24 | else 25 | | Activity 26 | 27 | if activity 28 | if activity.author 29 | div.float-right.byline#byline #{activity.author} 30 | else 31 | if activity.xourse.author 32 | div.float-right.byline#byline #{activity.xourse.author} 33 | -------------------------------------------------------------------------------- /blog/xw1Day2.markdown: -------------------------------------------------------------------------------- 1 | {{{ 2 | "title" : "Ximera Workshop 1: Day 2", 3 | "tags" : [ "shuttleworth foundation, nsf, workshop" ], 4 | "category" : "ximera workshop", 5 | "date" : "7-29-2014", 6 | "author" : "Bart Snapp" 7 | }}} 8 | 9 | Today was the second day of the Ximera Workshop 1. On the first day, 10 | we spent much time explaining what is going on, and learning that we 11 | need to make some changes. Today we started making these changes! 12 | 13 | 14 | 15 | The nature of Ximera syntax was on the line. Honestly, I was a bit 16 | skeptical at first, but the participants were able to convince me that 17 | our past syntax is inferior to the syntax we are now working to 18 | provide. We improved deployment, and started writing many 19 | activities. To see these demo courses, see 20 | [http://ximera.osu.edu/course/](http://example.net/). 21 | 22 | 23 | I am very encouraged at the speed of creation of these basic 24 | courses. At this point, we've made it a priority to get documentation 25 | on how someone gets started with Ximera, something that is difficult 26 | since in this early stage of development we are aiming for a moving 27 | target. Nevertheless, I am encouraged by the energy of the workshop, 28 | and hope to see some great things! Again, this would not have been 29 | possible without the support of the NSF and the Shuttleworth 30 | Foundation. 31 | 32 | -------------------------------------------------------------------------------- /public/javascripts/mathjax.js: -------------------------------------------------------------------------------- 1 | window.MathJax = { 2 | delayStartupUntil : "configured", 3 | 4 | jax: ["input/TeX","output/HTML-CSS"], 5 | extensions: ["tex2jax.js","MathMenu.js","MathZoom.js", "toMathML.js", "AssistiveMML.js", "[a11y]/accessibility-menu.js"], 6 | 7 | tex2jax: {preview: "none"}, 8 | 9 | "HTML-CSS": { 10 | availableFonts: ["TeX"], 11 | imageFont: null 12 | }, 13 | 14 | processEnvironments: true, 15 | showProcessingMessages: false, 16 | messageStyle: 'none', 17 | 18 | MathMenu: { 19 | showRenderer: false, 20 | showMathPlayer: false 21 | }, 22 | 23 | // BADBAD: this also breaks the layout triggers 24 | // showMathMenu: false, 25 | 26 | CommonHTML: { 27 | EqnChunk: 10000, 28 | EqnChunkFactor: 1, 29 | EqnChunkDelay: 0 30 | }, 31 | 32 | "fast-preview": {disabled: true}, 33 | 34 | TeX: { 35 | equationNumbers: { autoNumber: "AMS" }, 36 | extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js","color.js","cancel.js"], 37 | noErrors: {disabled: true}, 38 | Macros: { 39 | xspace: '', 40 | ensuremath: '' 41 | } 42 | }, 43 | 44 | root: "/node_modules/mathjax/" 45 | }; 46 | 47 | if (window.standalone) 48 | window.MathJax.root = "https://ximera.osu.edu/node_modules/mathjax"; 49 | 50 | require('mathjax2'); 51 | 52 | module.exports = window.MathJax; 53 | -------------------------------------------------------------------------------- /views/page.pug: -------------------------------------------------------------------------------- 1 | extends layouts/main 2 | 3 | block title 4 | if activity.title 5 | | !{activity.title} 6 | else 7 | | Activity 8 | 9 | block modals 10 | include modals/reset 11 | include modals/update 12 | include modals/guppymath 13 | 14 | block content 15 | // BADBAD: shouldn't this be removed now that htlatex is working 16 | // fine? No! Apparently htlatex still doesn't support two optional 17 | // arguments like this. 18 | include page/global-preamble 19 | include page/navigation 20 | if activity.xourse.title 21 | include page/breadcrumbs 22 | div.container-fluid 23 | // BADBAD: only display navigation if the item can be found IN a navigation list 24 | .row 25 | .col-md-12 26 | .row.justify-content-center 27 | .col-md-10 28 | main.activity(id="theActivity", data-hash=activity.hash, data-repository-name=repositoryName, data-commit=activity.commit, data-path=activity.path, data-title=activity.title, role="main", data-learner=learner ? learner._id : undefined, data-xourse-path=activity.xourse ? activity.xourse.path : undefined, data-xourse-url=activity.xourse ? (repositoryName + '/' + activity.xourse.path) : undefined, data-points=activity.xourse ? activity.xourse.totalPoints : undefined) 29 | .activity-body#page-content 30 | | !{activity.html} 31 | include page/next-and-previous 32 | -------------------------------------------------------------------------------- /views/user/index.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/main 2 | 3 | block title 4 | | Users 5 | 6 | 7 | block content 8 | .container 9 | div.row 10 | table.table 11 | thead 12 | tr 13 | th Id 14 | th Name 15 | th Seen 16 | tbody 17 | for user in users 18 | tr(data-href=`/users/${user.id}`) 19 | th(scope="row") 20 | a(href=`/users/${user.id}`) …#{user.id.slice(-5)} 21 | td 22 | if user.imageUrl 23 | img.img-responsive.img-rounded(src=user.imageUrl) 24 | else 25 | img.img-responsive.img-rounded(src=`https://secure.gravatar.com/avatar/${user.gravatar}.jpg?d=retro&s=80`) 26 | td #{user.displayName} (#{user.name}) 27 | if user.lastSeen 28 | td #{moment(user.lastSeen).fromNow()} 29 | else 30 | td Never 31 | div.row 32 | div.text-center 33 | ul.pagination.pagination-lg 34 | if page > 1 35 | li 36 | a(href=page) « 37 | else 38 | li.disabled 39 | span « 40 | if page < pageCount 41 | li 42 | a(href=page+1) » 43 | else 44 | li.disabled 45 | span » 46 | -------------------------------------------------------------------------------- /views/lti/config.pug: -------------------------------------------------------------------------------- 1 | doctype xml 2 | cartridge_basiclti_link(xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0", xmlns:blti = "http://www.imsglobal.org/xsd/imsbasiclti_v1p0", xmlns:lticm ="http://www.imsglobal.org/xsd/imslticm_v1p0", xmlns:lticp ="http://www.imsglobal.org/xsd/imslticp_v1p0", xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance", xsi:schemaLocation = "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0.xsd http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd") 3 | blti:title #{title} 4 | blti:description #{description} 5 | blti:icon 6 | blti:launch_url #{launchUrl} 7 | blti:custom 8 | lticm:property(name="unlock_at") $Canvas.assignment.unlockAt.iso8601 9 | lticm:property(name="lock_at") $Canvas.assignment.lockAt.iso8601 10 | lticm:property(name="due_at") $Canvas.assignment.dueAt.iso8601 11 | blti:extensions(platform="canvas.instructure.com") 12 | lticm:property(name="tool_id") #{launchUrl} 13 | lticm:property(name="privacy_level") public 14 | lticm:property(name="domain") #{domain} 15 | cartridge_bundle(identifierref="BLTI001_Bundle") 16 | cartridge_icon(identifierref="BLTI001_Icon") 17 | -------------------------------------------------------------------------------- /views/xourses/view.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/main 2 | include ../activity-card.pug 3 | 4 | block title 5 | | !{xourse.title} 6 | 7 | block modals 8 | include ../modals/update-xourse 9 | 10 | block content 11 | nav(aria-label="breadcrumb",role="navigation") 12 | ol.breadcrumb 13 | li.breadcrumb-item 14 | - var level = 1; 15 | a(href="/" + repositoryName, aria-level=level) 16 | | !{repositoryName} 17 | li.breadcrumb-item.active 18 | if (xourse.title) && (xourse.title != '') 19 | | !{xourse.title} 20 | else 21 | | Home 22 | 23 | div.container.xourse-cards 24 | main.xourse.card-group(data-commit=xourse.commit, data-hash=xourse.hash, data-points=xourse.totalPoints, data-path=xourse.path, data-repository-name=repositoryName, data-learner=learner ? learner._id : undefined, data-xourse-path=xourse.path, data-xourse-url=repositoryName + '/' + xourse.path) 25 | for activityUrl in xourse.activityList 26 | +xourseCard(repositoryName, xourse.path, activityUrl, xourse.activities[activityUrl]) 27 | div.container 28 | hr 29 | .row 30 | .col-md-12 31 | p You can download a 32 | a.btn.btn.btn-primary(type="button", href="/" + repositoryName + "/" + xourse.path + "/certificate") 33 | | Certificate 34 | |  as a record of your successes. 35 | 36 | -------------------------------------------------------------------------------- /branding.js: -------------------------------------------------------------------------------- 1 | var ip = require('ip'); 2 | var _ = require('underscore'); 3 | 4 | var brandings = { 5 | "localhost": [ip.cidrSubnet('127.0.0.0/8')], 6 | "The Ohio State University": [ip.cidrSubnet('164.107.0.0/16'), 7 | ip.cidrSubnet('140.254.0.0/16'), 8 | ip.cidrSubnet('128.146.0.0/16'), 9 | ip.cidrSubnet('192.68.143.0/24'), 10 | ip.cidrSubnet('192.12.205.0/24')], 11 | "Colorado State University": [ip.cidrSubnet('129.82.0.0/16')] 12 | }; 13 | 14 | 15 | exports.middleware = function(req, res, next) { 16 | var remoteAddress = req.headers['x-forwarded-for'] || 17 | req.connection.remoteAddress || 18 | req.socket.remoteAddress || 19 | req.connection.socket.remoteAddress; 20 | try { 21 | res.locals.places = []; 22 | 23 | Object.keys(brandings).forEach( function(place) { 24 | if (_.some( brandings[place], function(subnet) { return subnet.contains(remoteAddress); } )) { 25 | res.locals.places.push( place ); 26 | } 27 | }); 28 | 29 | if (res.locals.places.indexOf( "The Ohio State University" ) >= 0) 30 | res.locals.atOhioState = true; 31 | else 32 | res.locals.atOhioState = false; 33 | 34 | if (res.locals.places.indexOf( "Colorado State University" ) >= 0) 35 | res.locals.atColoradoState = true; 36 | else 37 | res.locals.atColoradoState = false; 38 | } 39 | catch (e) { 40 | res.locals.places = []; 41 | res.locals.atOhioState = false; 42 | } 43 | 44 | next(); 45 | }; 46 | -------------------------------------------------------------------------------- /views/certificate/view.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/main 2 | 3 | block title 4 | | Valid Certificate 5 | 6 | block modals 7 | 8 | block content 9 | div.container 10 | .page-header 11 | h1 Valid Certificate 12 | 13 | .row 14 | .col-md-7 15 | p 16 | | As of  17 | time(class="relative-date", datetime=certificate.date) 18 | | #{certificate.date} 19 | |  on  20 | time(class="absolute-date", datetime=certificate.date) 21 | | #{certificate.date} 22 | | , 23 | a(href=('https://' + certificate.user)) 24 | | #{certificate.name} 25 | if certificate.email 26 | |  <#{certificate.email}> 27 | |  has earned #{certificate.score}/100 in 28 | if certificate.title === undefined 29 | a(href=certificate.course) 30 | | #{certificate.course} 31 | else 32 | a(href=certificate.course) 33 | | #{certificate.title} 34 | | . 35 | p Certificates can be verified with this public key. 36 | pre #{publicKey.trim()} 37 | .col-md-5 38 | canvas.qrcode.float-right(style="max-width: 100%;", data-href=`https://ximera.osu.edu/certificate/${escapedCode}/${escapedSignature}`) 39 | p 40 | a(href=`/certificate/${escapedCode}/${escapedSignature}`) 41 | span.certificate https://ximera.osu.edu/certificate/#{escapedCode}/#{escapedSignature} 42 | -------------------------------------------------------------------------------- /public/javascripts/feedback.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | var database = require('./database'); 4 | 5 | var createFeedback = function() { 6 | var feedback = $(this); 7 | 8 | feedback.persistentData( function() { 9 | if (feedback.persistentData( 'available' )) { 10 | feedback.css({visibility: 'visible', position:'relative'}); 11 | feedback.fadeTo('slow', 1); 12 | } else { 13 | feedback.css({visibility: 'hidden', position:'absolute'}); 14 | feedback.css({opacity: 0}); 15 | } 16 | }); 17 | 18 | var problem = feedback.parents( ".problem-environment" ).first(); 19 | 20 | if (feedback.attr('data-feedback') == 'attempt') { 21 | problem.on( 'ximera:attempt', function(event) { 22 | feedback.persistentData( 'available', true ); 23 | }); 24 | } 25 | 26 | if (feedback.attr('data-feedback') == 'correct') { 27 | problem.on( 'ximera:correct', function(event) { 28 | feedback.persistentData( 'available', true ); 29 | }); 30 | } 31 | 32 | if (feedback.attr('data-feedback') == 'script') { 33 | problem.on( 'ximera:attempt', function(event) { 34 | var release = false; 35 | try { 36 | release = window[feedback.attr('id')](); 37 | } catch(err) { 38 | release = false; 39 | } 40 | 41 | feedback.persistentData( 'available', release ); 42 | }); 43 | 44 | console.log( feedback ); 45 | } 46 | 47 | }; 48 | 49 | $.fn.extend({ 50 | feedback: function() { 51 | return this.each( createFeedback ); 52 | } 53 | }); 54 | 55 | -------------------------------------------------------------------------------- /public/javascripts/hint.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | var database = require('./database'); 4 | 5 | var buttonHtml = ''; 6 | 7 | var createHint = function() { 8 | var hint = $(this); 9 | 10 | hint.addClass('collapse'); 11 | hint.collapse('hide'); 12 | 13 | var button = $(buttonHtml); 14 | hint.before(button); 15 | 16 | button.click( function() { 17 | if (hint.persistentData( 'collapsed' )) { 18 | hint.persistentData( 'collapsed', false ); 19 | } else { 20 | hint.persistentData( 'collapsed', true ); 21 | } 22 | }); 23 | 24 | hint.trigger( 'ximera:register-hint' ); 25 | 26 | hint.persistentData( function(event) { 27 | if (hint.persistentData( 'available' )) { 28 | button.show('fast'); 29 | 30 | if (hint.persistentData( 'collapsed' )) { 31 | button.find('i').addClass('fa-rotate-90'); 32 | hint.collapse('hide'); 33 | } else { 34 | button.find('i').removeClass('fa-rotate-90'); 35 | hint.collapse('show'); 36 | } 37 | } else { 38 | hint.collapse('hide'); 39 | button.hide(); 40 | } 41 | /* 42 | if (hint.persistentData( 'available' )) { 43 | 44 | } else { 45 | button.hide(); 46 | } 47 | */ 48 | 49 | }); 50 | 51 | 52 | }; 53 | 54 | $.fn.extend({ 55 | hint: function() { 56 | return this.each( createHint ); 57 | } 58 | }); 59 | 60 | 61 | -------------------------------------------------------------------------------- /public/stylesheets/mathjax.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////// 2 | // align font with mathjax 3 | 4 | @font-face { 5 | font-family: 'MJX_Math'; 6 | src: url('/node_modules/mathjax/fonts/HTML-CSS/TeX/eot/MathJax_Math-Italic.eot'); /* IE9 Compat Modes */ 7 | src: url('/node_modules/mathjax/fonts/HTML-CSS/TeX/eot/MathJax_Math-Italic.eot?iefix') format('eot'), 8 | url('/node_modules/mathjax/fonts/HTML-CSS/TeX/woff/MathJax_Math-Italic.woff') format('woff'), 9 | url('/node_modules/mathjax/fonts/HTML-CSS/TeX/otf/MathJax_Math-Italic.otf') format('opentype'), 10 | url('/node_modules/mathjax/fonts/HTML-CSS/TeX/svg/MathJax_Math-Italic.svg#MathJax_Math-Italic') format('svg'); 11 | } 12 | 13 | @font-face { 14 | font-family: 'MJX_Main'; 15 | src: url('/node_modules/mathjax/fonts/HTML-CSS/TeX/eot/MathJax_Main-Regular.eot'); /* IE9 Compat Modes */ 16 | src: url('/node_modules/mathjax/fonts/HTML-CSS/TeX/eot/MathJax_Main-Regular.eot?iefix') format('eot'), 17 | url('/node_modules/mathjax/fonts/HTML-CSS/TeX/woff/MathJax_Main-Regular.woff') format('woff'), 18 | url('/node_modules/mathjax/fonts/HTML-CSS/TeX/otf/MathJax_Main-Regular.otf') format('opentype'), 19 | url('/node_modules/mathjax/fonts/HTML-CSS/TeX/svg/MathJax_Main-Regular.svg#MathJax_Main-Regular') format('svg'); 20 | } 21 | 22 | .mjxi { 23 | font-family: MJX_Math; 24 | font-size: 131%; 25 | line-height: .8em; 26 | } 27 | 28 | .mjx { 29 | font-family: MJX_Main; 30 | font-size: 131%; 31 | line-height: .8em; 32 | } 33 | -------------------------------------------------------------------------------- /login/guests.js: -------------------------------------------------------------------------------- 1 | var mdb = require('../mdb'); 2 | 3 | function createGuestUser( req, res, next ) { 4 | var userAgent = req.headers['user-agent']; 5 | var remoteAddress = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.socket.remoteAddress || req.connection.socket.remoteAddress; 6 | 7 | req.user = new mdb.User({ 8 | isGuest: true, 9 | name: "Guest User", 10 | userAgent: userAgent, 11 | remoteAddress: remoteAddress 12 | }); 13 | req.session.guestUserId = req.user._id; 14 | req.user.save(next); 15 | }; 16 | 17 | // Add guest users account if not logged in. 18 | // TODO: Clean these out occasionally. 19 | module.exports.middleware = function(req, res, next) { 20 | 21 | // If we are already logged in legitimately... 22 | if (req.user) { 23 | // Then forgot this guest user nonsense. 24 | req.session.guestUserId = null; 25 | next(); 26 | return; 27 | } 28 | 29 | // If we've already created a guest account... 30 | if (req.session.guestUserId) { 31 | // attempt to load it 32 | mdb.User.findOne({_id: req.session.guestUserId}, function (err, user) { 33 | if (err) { 34 | next(err); 35 | } 36 | else if (user) { 37 | req.user = user; 38 | next(); 39 | } 40 | else { 41 | req.session.guestUserId = null; 42 | createGuestUser( req, res, next ); 43 | } 44 | }); 45 | 46 | } else { 47 | createGuestUser( req, res, next ); 48 | } 49 | }; 50 | 51 | -------------------------------------------------------------------------------- /views/layouts/navbar/get-help.pug: -------------------------------------------------------------------------------- 1 | li.nav-item.dropdown 2 | a.nav-link.dropdown-toggle(id="navbarGetHelp",data-toggle="dropdown",aria-haspopup="true",aria-expanded="false") 3 | | 4 | span.hidden-md-down  Get Help 5 | div.dropdown-menu(aria-labelledby="navbarGetHelp") 6 | if repositoryName && ((activity && activity.xourse && activity.xourse.path) || (xourse && xourse.path)) 7 | a.dropdown-item(id="contact-instructor", href=`/${repositoryName}/${(activity ? (activity.xourse) : xourse).path}/instructors`) 8 | | Contact my instructor 9 | div.dropdown-divider 10 | if activity && activity.path && activity.title && repositoryMetadata && repositoryMetadata.github 11 | // BADBAD: instead of master, perhaps should have activity.commit 12 | a.dropdown-item(href=`https://github.com/${repositoryMetadata.github.owner}/${repositoryMetadata.github.repository}/issues/new?title=${activity.title.replace(/ +/,' ')}&body=%0A%0ASee%20[${activity.path}.tex](https://github.com/${repositoryMetadata.github.owner}/${repositoryMetadata.github.repository}/blob/master/${activity.path}.tex)`) 13 | | Report error to authors 14 | if path 15 | a.dropdown-item(href="mailto:ximera-help@osu.edu" + (path ? `?Subject=${path}` : "")) 16 | | Request help using Ximera 17 | if path 18 | a.dropdown-item(href=`https://github.com/kisonecat/ximera/issues/new?title=${path}`) 19 | | Report bug to programmers 20 | -------------------------------------------------------------------------------- /public/stylesheets/certificate.scss: -------------------------------------------------------------------------------- 1 | /****************************************************************/ 2 | dl.certificate { width: 100% } 3 | 4 | dl.certificate { 5 | display: flex; 6 | flex-flow: row; 7 | flex-wrap: wrap; 8 | width: 100%; /* set the container width*/ 9 | overflow: visible; 10 | } 11 | dl.certificate dt { 12 | flex-basis: 50%; 13 | flex-grow: 1; 14 | flex-shrink: 0; 15 | text-overflow: hidden; 16 | overflow: hidden; 17 | white-space: nowrap; 18 | } 19 | dl.certificate dd { 20 | margin-left: auto; 21 | text-align: right; 22 | flex-basis: content; 23 | text-overflow: ellipsis; 24 | overflow: hidden; 25 | flex-grow: 0; 26 | flex-shrink: 1; 27 | } 28 | 29 | dl.certificate dt.page { 30 | padding-left: 1em; 31 | } 32 | 33 | dl.certificate dt.page:after, dl.certificate dt.total:after { 34 | content: ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . "; 35 | color: $gray; 36 | } 37 | 38 | 39 | span.certificate { 40 | width: 100%; 41 | display: block; 42 | max-width: 100%; 43 | overflow: hidden; 44 | text-overflow: ellipsis; 45 | white-space: nowrap; 46 | } 47 | 48 | /******* subparagraphs and paragraphs *****/ 49 | span.subparagraphHead, span.paragraphHead { 50 | margin-right: 1em; 51 | font-weight: bold; 52 | } 53 | -------------------------------------------------------------------------------- /public/javascripts/xourse.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | var Isotope = require('isotope-layout'); 4 | 5 | var activityCard = require('./activity-card'); 6 | var xourseIsotope = undefined; 7 | 8 | var updateSearch = function() { 9 | if (!xourseIsotope) return; 10 | 11 | var search = $('#xourse-search').val(); 12 | 13 | if ((typeof search === 'undefined') || (search.length == 0)) { 14 | xourseIsotope.arrange({ filter: '*' }); 15 | return; 16 | } 17 | 18 | var regexps = _.map( search.toLowerCase().split(" "), function(word) { 19 | return new RegExp(word); 20 | }); 21 | 22 | xourseIsotope.arrange({ 23 | filter: function() { 24 | // Bart says do not display these 25 | if ($(this).hasClass('part')) 26 | return false; 27 | 28 | var text = $(this).text().toLowerCase(); 29 | 30 | return _.all( regexps, function(re) { return re.test( text ); } ); 31 | } 32 | }); 33 | }; 34 | 35 | var layoutXourse = function( ) { 36 | var xourse = $(this); 37 | 38 | $('.activity-card', xourse).activityCard(); 39 | 40 | xourse.show(); 41 | 42 | var options = { 43 | layoutMode: 'fitRows', 44 | itemSelector: '.activity-card', 45 | filter: '*', 46 | animationOptions: { 47 | duration: 750, 48 | easing: 'linear', 49 | queue: false 50 | } 51 | }; 52 | 53 | xourseIsotope = new Isotope( xourse.get(0), 54 | options ); 55 | }; 56 | 57 | // On document ready... 58 | $(function() { 59 | $('.xourse').each( layoutXourse ); 60 | 61 | $('#xourse-search').on('input', function() { 62 | updateSearch(); 63 | }); 64 | }); 65 | 66 | -------------------------------------------------------------------------------- /public/stylesheets/activity.scss: -------------------------------------------------------------------------------- 1 | // tex4ht emits these 2 | .center { 3 | text-align: center; 4 | } 5 | 6 | // the preamble includes \newcommands which should not be displayed 7 | .preamble { 8 | display: none; 9 | } 10 | 11 | .activity-body pre { 12 | white-space: normal; 13 | } 14 | 15 | /****************************************************************/ 16 | /* Try to spread out lines a bit for readability? */ 17 | 18 | .activity-content { 19 | line-height: $line-height-base * 1.35; 20 | } 21 | 22 | /****************************************************************/ 23 | /* LaTeX logo */ 24 | 25 | span.LATEX { 26 | font-family: Times, "Times New Roman", serif; 27 | letter-spacing: 1px; 28 | } 29 | 30 | span.LATEX .A { 31 | text-transform: uppercase; 32 | letter-spacing: 1px; 33 | font-size: 0.85em; 34 | vertical-align: 0.15em; 35 | margin-left: -0.36em; 36 | margin-right: -0.15em; 37 | } 38 | 39 | span.LATEX .TEX .E { 40 | text-transform: uppercase; 41 | vertical-align: -0.5ex; 42 | margin-left: -0.1667em; 43 | margin-right: -0.125em; 44 | font-size: 1em; 45 | } 46 | 47 | // Some better spacing 48 | .sectionHead { 49 | @extend .mt-5; 50 | } 51 | 52 | .subsectionHead { 53 | @extend .mt-3; 54 | } 55 | 56 | .likesubsectionHead { 57 | @extend .mt-3; 58 | } 59 | 60 | /****************************************************************/ 61 | /* tex4ht generates description lists from \begin{description} */ 62 | 63 | dl.description { 64 | margin-top: 12px; 65 | margin-bottom: 12px; 66 | } 67 | 68 | dl.description dt.description { 69 | float: left; 70 | clear: left; 71 | padding-right: 0.5em; 72 | } 73 | -------------------------------------------------------------------------------- /public/javascripts/foldable.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | var database = require('./database'); 4 | 5 | var buttonHtml = ''; 6 | 7 | var createFoldable = function() { 8 | var foldable = $(this); 9 | 10 | var button = $(buttonHtml); 11 | foldable.before('
'); 12 | foldable.before(button); 13 | 14 | button.click( function() { 15 | if (foldable.persistentData( 'collapsed' )) { 16 | foldable.persistentData( 'collapsed', false ); 17 | } else { 18 | foldable.persistentData( 'collapsed', true ); 19 | } 20 | }); 21 | 22 | foldable.persistentData( function(event) { 23 | if ( (foldable.persistentData( 'collapsed' ) == true) != (foldable.attr('data-original') == 'expandable') ) { 24 | button.find('i').addClass('fa-rotate-90'); 25 | //foldable.collapse('hide'); 26 | foldable.css( 'font-size', '0px' ); 27 | foldable.children().hide(); 28 | $('.unfoldable', foldable).show(); 29 | $('.unfoldable', foldable).parentsUntil( foldable ).show(); 30 | $('.foldable', foldable).hide(); 31 | } else { 32 | button.find('i').removeClass('fa-rotate-90'); 33 | foldable.children().show(); 34 | //$('.unfoldable', foldable).show(); 35 | //foldable.collapse('show'); 36 | foldable.css( 'font-size', '12pt' ); 37 | $('.foldable', foldable).show(); 38 | } 39 | 40 | }); 41 | 42 | }; 43 | 44 | $.fn.extend({ 45 | foldable: function() { 46 | return this.each( createFoldable ); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /public/images/logo/tikzLogo.tex: -------------------------------------------------------------------------------- 1 | \documentclass{standalone} 2 | 3 | \usepackage{tikz} 4 | 5 | \begin{document} 6 | \begin{tikzpicture}[x=1cm,y=1cm] 7 | \begin{scope} 8 | \clip(0,0) rectangle (73cm,12cm); 9 | 10 | %% X 11 | \begin{scope}[xshift=1cm] 12 | \draw[line width=2.5cm] (0,-2) -- (14,14); 13 | \draw[line width=2.5cm] (0,14) -- (14,-2); 14 | \end{scope} 15 | 16 | %% I 17 | \begin{scope}[xshift=16.5cm] 18 | \draw[line width=2.5cm] (0,0) -- (0,12); 19 | \end{scope} 20 | 21 | %% M 22 | \begin{scope}[xshift=19.5cm] 23 | \draw[line width=2.5cm, smooth, rounded corners=1cm] 24 | (0,0) -- (0,10.75)%% 25 | --(3.5,10.75) % width of top 26 | --(6.5,1.25)--(8.5,1.25) 27 | --(11.5,10.75) 28 | --(15,10.75)--(15,0); 29 | \end{scope} 30 | 31 | %% E 32 | \begin{scope}[xshift=37.25cm,yshift=1.25cm] 33 | \draw[line width=2.5cm, smooth, rounded corners=2cm] 34 | (10,0)--(0,0)--(0,4.75)--(10,4.75); 35 | 36 | \draw[line width=2.5cm, smooth, rounded corners=2cm] 37 | (5,4.75) -- (0,4.75)--(0,9.5)--(11.8,9.5); 38 | \end{scope} 39 | 40 | %% R 41 | \begin{scope}[xshift=49cm,yshift=-1.25cm] 42 | \draw[line width=2.5cm, smooth, rounded corners=2cm] 43 | (0,12)--(10,12)--(10,7)--(0,7)--(0,1.25); 44 | 45 | \draw[line width=2.5cm] 46 | (4.5,7)--(10,0); 47 | \end{scope} 48 | 49 | %% A 50 | \begin{scope}[xshift=59.5cm,yshift=-1.25cm] 51 | \draw[line width=2.5cm, smooth, rounded corners=1.5cm] 52 | (0,0)--(6,12.75)--(12,0); 53 | \end{scope} 54 | \end{scope} 55 | %\draw[step=1cm,gray,very thin] (0,0) grid (71,10); 56 | \end{tikzpicture} 57 | \end{document} 58 | -------------------------------------------------------------------------------- /public/images/stickers/criticalItem.tex: -------------------------------------------------------------------------------- 1 | \documentclass{standalone} 2 | %% compile with: 3 | %% pdflatex --enable-write18 criticalItem.tex 4 | 5 | \usepackage{tikz} 6 | \usepackage{color} 7 | \usepackage{helvet} 8 | %\usepackage{gillius} 9 | 10 | \usepackage{standalone} 11 | \renewcommand{\familydefault}{\sfdefault} 12 | 13 | \colorlet{redback}{red!85!black} 14 | 15 | \begin{document} 16 | 17 | \begin{tikzpicture}[x=1in,y=1in] 18 | \begin{scope} 19 | \clip(0,0) rectangle (6,3); 20 | \draw[fill=white,draw=none] (0,0) rectangle (6,3); 21 | \draw[fill=redback,draw=none] (0.15,.15) rectangle (5.85,2.85); 22 | \draw[fill=white,draw=none] (3,.25) rectangle (5.75,2.75); 23 | 24 | \node[anchor=west] at (.18,2.2) {\tikzset{draw/.append style={white}} 25 | \resizebox{2.6in}{!}{\input{tikzLogo.tex}}}; 26 | \node[anchor=west,white] at (.247,1.3) [draw,align=left,draw=none]{ 27 | \Large 28 | Interactive\\[.14cm] 29 | \Large 30 | Mathematics\\[.14cm] 31 | \Large 32 | Education\\[.14cm] 33 | \Large 34 | Resources for \\[.14cm] 35 | \Large 36 | All 37 | }; 38 | 39 | \node[anchor=west,black] at (3.217,1.64) 40 | [draw,align=left,draw=none]{ 41 | \Huge 42 | CRITICAL\\[.16cm] 43 | \Huge 44 | MATH\\[.16cm] 45 | \Huge 46 | ITEM\\[.7cm] 47 | \Large 48 | HANDLE WITH \\[.1cm] 49 | \Large 50 | EXTREME CARE 51 | }; 52 | 53 | \draw[very thick,black] (3.6,1.35)--(4.6,1.35); 54 | 55 | \node[anchor=west,redback] at (.15,.1) {\tiny\textbf{XIMERA\quad FORM \quad 1368 \quad GIT \quad COMMIT \quad \input{../../../.git/refs/heads/master} \quad PREVIOUS \quad EDITION \quad IS \quad OBSOLETE.}}; 56 | \end{scope} 57 | \end{tikzpicture} 58 | \end{document} 59 | -------------------------------------------------------------------------------- /public/javascripts/gradebook.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | 4 | exports.update = _.debounce( function() { 5 | var pointsEarned = 0; 6 | 7 | $(".activity-card").each( function() { 8 | var card = $(this); 9 | var weight = parseFloat(card.attr('data-weight')); 10 | var completion = parseFloat(card.attr('data-max-completion')); 11 | 12 | if (! isNaN(weight)) { 13 | if (! isNaN(completion)) { 14 | var points = weight * completion; 15 | pointsEarned = pointsEarned + points; 16 | } 17 | } 18 | }); 19 | 20 | var pointsPossible = $("main").attr( 'data-points' ); 21 | var xourseUrl = $("main").attr( 'data-xourse-url' ); 22 | 23 | var payload = { 24 | pointsEarned: pointsEarned, 25 | pointsPossible: pointsPossible 26 | }; 27 | 28 | $(".progress.completion-meter").attr('title', 'Submitting grade...' ); 29 | 30 | $.ajax({ 31 | url: '/' + xourseUrl + '/gradebook', 32 | type: 'PUT', 33 | data: JSON.stringify(payload), 34 | contentType: 'application/json', 35 | success: function( result ) { 36 | console.log( "Recorded gradebook",payload ); 37 | $('.progress-bar', ".progress.completion-meter").removeClass( 'bg-danger' ); 38 | $('.progress-bar', ".progress.completion-meter").addClass( 'bg-success' ); 39 | $(".progress.completion-meter").attr('title', 'Grade submitted at ' + (new Date()).toLocaleTimeString() ); 40 | }, 41 | error: function(jqXHR, err, exception) { 42 | $(".progress.completion-meter").attr('title', 'Could not submit grade.' ); 43 | $('.progress-bar', ".progress.completion-meter").removeClass( 'bg-success' ); 44 | $('.progress-bar', ".progress.completion-meter").addClass( 'bg-danger' ); 45 | window.setTimeout( exports.update, 1000 ); 46 | } 47 | }); 48 | 49 | }, 300 ); 50 | -------------------------------------------------------------------------------- /public/stylesheets/fonts.scss: -------------------------------------------------------------------------------- 1 | // The "smiley" hack prevents any locally-installed versions of the font from being used; 2 | 3 | @font-face { 4 | font-family: 'STIXGeneral'; 5 | src: url('/public/fonts/stix/stixgeneral-webfont.eot'); 6 | src: local('[smiley]'), url('/public/fonts/stix/stixgeneral-webfont.woff') format('woff'), url('/public/fonts/stix/stixgeneral-webfont.ttf') format('truetype'), url('/public/fonts/stix/stixgeneral-webfont.svg#webfontZXtFAA5Q') format('svg'); 7 | font-weight: normal; 8 | font-style: normal; 9 | } 10 | @font-face { 11 | font-family: 'STIXGeneral'; 12 | src: url('/public/fonts/stix/stixgeneralitalic-webfont.eot'); 13 | src: local('[smiley]'), url('/public/fonts/stix/stixgeneralitalic-webfont.woff') format('woff'), url('/public/fonts/stix/stixgeneralitalic-webfont.ttf') format('truetype'), url('/public/fonts/stix/stixgeneralitalic-webfont.svg#webfont2oJeLJIt') format('svg'); 14 | font-weight: normal; 15 | font-style: italic; 16 | } 17 | @font-face { 18 | font-family: 'STIXGeneral'; 19 | src: url('/public/fonts/stix/stixgeneralbol-webfont.eot'); 20 | src: local('[smiley]'), url('/public/fonts/stix/stixgeneralbol-webfont.woff') format('woff'), url('/public/fonts/stix/stixgeneralbol-webfont.ttf') format('truetype'), url('/public/fonts/stix/stixgeneralbol-webfont.svg#webfontwFpnxWyx') format('svg'); 21 | font-weight: bold; 22 | font-style: normal; 23 | } 24 | @font-face { 25 | font-family: 'STIXGeneral'; 26 | src: url('/public/fonts/stix/stixgeneralbolita-webfont.eot'); 27 | src: local('[smiley]'), url('/public/fonts/stix/stixgeneralbolita-webfont.woff') format('woff'), url('/public/fonts/stix/stixgeneralbolita-webfont.ttf') format('truetype'), url('/public/fonts/stix/stixgeneralbolita-webfont.svg#webfontSsfHxKRo') format('svg'); 28 | font-weight: bold; 29 | font-style: italic; 30 | } -------------------------------------------------------------------------------- /blog/xw1Day1.markdown: -------------------------------------------------------------------------------- 1 | {{{ 2 | "title" : "Ximera Workshop 1: Day 1", 3 | "tags" : [ "shuttleworth foundation,nsf,workshop" ], 4 | "category" : "ximera workshop", 5 | "date" : "7-28-2014", 6 | "author" : "Bart Snapp" 7 | }}} 8 | 9 | Today was the first day of the Ximera Workshop 1. With around 30 10 | participants from all over the nation, we can feel the excitement for 11 | Ximera growing! 12 | 13 | 14 | 15 | 16 | Thanks to support from NSF and the Shuttleworth Foundation, we have 17 | put together an informal and lively workshop. Today was filled with 18 | getting people started with Ximera, attempting to answer the somewhat 19 | difficult question of exactly what it is Ximera does, tearing down 20 | current Ximera syntax, and making wish-lists of what we want Ximera to 21 | do. 22 | 23 | So with that said, I'll do my best to explain what Ximera is all 24 | about: In essence Ximera provides authoring tools that allow the 25 | simultaneous creation of PDF files and online materials. 26 | 27 | While the authoring language is basically plain LaTeX, to say that we 28 | are providing LaTeX to HTML conversion is somewhat misleading. We are 29 | using LaTeX to contain all the relevant information for an interactive 30 | online platform, and then Ximera interprets this information in a 31 | relevant way. Here we see an infographic displaying the basic idea of 32 | how content is deployed. 33 | 34 | ![Ximera Infographic](XimeraGraphic.png) 35 | 36 | 37 | Authors can edit LaTeX files on their computer. These files can 38 | produce PDF files after compiling in the Ximera document class. These 39 | files are stored on GitHub, thus facilitating collaboration. Once the 40 | file is on GitHub, the Ximera server can deploy the content on 41 | ximera.osu.edu. This allows students from all over the world access to 42 | authors' content. 43 | 44 | Ximera is a project that needs a community to survive, the XW1 is a 45 | good start in building this community. 46 | 47 | 48 | -------------------------------------------------------------------------------- /login/passport-lti.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | , passport = require('passport') 3 | , _ = require('underscore') 4 | , mdb = require('../mdb') 5 | , lti = require("ims-lti"); 6 | 7 | function LtiStrategy(options, verify) { 8 | this.name = 'lti'; 9 | this._verify = verify; 10 | this.returnURL = options.returnURL; 11 | passport.Strategy.call(this, options, verify); 12 | } 13 | 14 | util.inherits(LtiStrategy, passport.Strategy); 15 | 16 | LtiStrategy.prototype.authenticate = function(req) { 17 | // I'm behind nginx so it looks like I'm serving http, but as far as the rest of the world is concerned, it's https 18 | var protocol = 'https'; 19 | if (req.get('host') == 'localhost:3000') { 20 | protocol = 'http'; 21 | console.log( protocol ); 22 | } 23 | 24 | var myRequest = _.extend({}, req, {protocol: protocol}); 25 | var self = this; 26 | 27 | function verified(err, user, info) { 28 | if (err) { return self.error(err); } 29 | if (!user) { return self.fail(info); } 30 | self.success(user, info); 31 | }; 32 | 33 | var profile = req.body; 34 | 35 | mdb.KeyAndSecret.findOne( {ltiKey: profile.oauth_consumer_key}, function(err, keyAndSecret) { 36 | if (err) 37 | self.error(err); 38 | else { 39 | if (!keyAndSecret) { 40 | self.error('The LTI key has not been registered with xake lti'); 41 | } else { 42 | self.provider = new lti.Provider(keyAndSecret.ltiKey, keyAndSecret.ltiSecret); 43 | 44 | self.provider.valid_request(myRequest, function(err, isValid) { 45 | if (!isValid) { 46 | return self.error(err); 47 | } else { 48 | // An LTI user may end up taking a course multiple times, but we want a fresh experience each time 49 | var identifier = profile.user_id + '-' + profile.context_id; 50 | self._verify( req, identifier, profile, verified ); 51 | } 52 | }); 53 | } 54 | } 55 | }); 56 | } 57 | 58 | module.exports.Strategy = LtiStrategy; 59 | 60 | -------------------------------------------------------------------------------- /public/javascripts/shuffle.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | var database = require('./database'); 4 | 5 | var createShuffle = function() { 6 | var shuffle = $(this); 7 | 8 | shuffle.persistentData( function() { 9 | 10 | if (!(shuffle.persistentData( 'initialized' ))) { 11 | shuffle.persistentData( 'initialized', true ); 12 | 13 | var problems = shuffle.children('.problem-environment'); 14 | problemIds = $.makeArray( problems.map( function() { 15 | return $(this).attr('id'); 16 | })); 17 | 18 | // BADBAD: this must be done deterministically, to avoid 19 | // a student just clicking over and over to get a new problem 20 | shuffle.persistentData( 'shuffle', _.shuffle( problemIds ) ); 21 | 22 | var firstProblemId = shuffle.persistentData( 'shuffle' )[0]; 23 | var firstProblem = shuffle.children('#' + firstProblemId); 24 | firstProblem.persistentData('available', true); 25 | } 26 | 27 | if (shuffle.persistentData('available')) { 28 | var order = shuffle.persistentData( 'shuffle' ); 29 | shuffle.children('.problem-environment').sort( function(a,b) { 30 | var ai = _.indexOf( order, $(a).attr('id') ); 31 | var bi = _.indexOf( order, $(b).attr('id') ); 32 | if (ai == bi) return 0; 33 | if (ai < bi) return -1; 34 | if (ai > bi) return 1; 35 | }).each( function() { 36 | this.parentNode.appendChild(this); 37 | }); 38 | } 39 | }); 40 | 41 | var problems = shuffle.children('.problem-environment'); 42 | 43 | problems.each( function() { 44 | var problem = $(this); 45 | var problemId = problem.attr('id'); 46 | 47 | problem.persistentData( function() { 48 | if (problem.persistentData('complete')) { 49 | var nextProblem = $(this).next('.problem-environment'); 50 | nextProblem.persistentData('available', true); 51 | } 52 | }); 53 | }); 54 | }; 55 | 56 | $.fn.extend({ 57 | shuffle: function() { 58 | return this.each( createShuffle ); 59 | } 60 | }); 61 | 62 | -------------------------------------------------------------------------------- /public/stylesheets/carousel.scss: -------------------------------------------------------------------------------- 1 | /* CUSTOMIZE THE CAROUSEL 2 | -------------------------------------------------- */ 3 | 4 | /* Carousel base class */ 5 | .carousel { 6 | margin-bottom: 4rem; 7 | } 8 | /* Since positioning the image, we need to help out the caption */ 9 | .carousel-caption { 10 | z-index: 10; 11 | bottom: 3rem; 12 | } 13 | 14 | /* Declare heights because of positioning of img element */ 15 | .carousel-item { 16 | height: 32rem; 17 | //background-color: #777; 18 | } 19 | .carousel-item > img { 20 | position: absolute; 21 | top: 0; 22 | left: 0; 23 | min-width: 100%; 24 | height: 32rem; 25 | } 26 | 27 | 28 | /* MARKETING CONTENT 29 | -------------------------------------------------- */ 30 | 31 | /* Center align the text within the three columns below the carousel */ 32 | .marketing .col-lg-4 { 33 | margin-bottom: 1.5rem; 34 | text-align: center; 35 | } 36 | .marketing h2 { 37 | font-weight: normal; 38 | } 39 | .marketing .col-lg-4 p { 40 | margin-right: .75rem; 41 | margin-left: .75rem; 42 | } 43 | 44 | 45 | /* Featurettes 46 | ------------------------- */ 47 | 48 | .featurette-divider { 49 | margin: 5rem 0; /* Space out the Bootstrap
more */ 50 | } 51 | 52 | /* Thin out the marketing headings */ 53 | .featurette-heading { 54 | font-weight: 300; 55 | line-height: 1; 56 | letter-spacing: -.05rem; 57 | } 58 | 59 | 60 | /* RESPONSIVE CSS 61 | -------------------------------------------------- */ 62 | 63 | @media (min-width: 40em) { 64 | /* Bump up size of carousel content */ 65 | .carousel-caption p { 66 | margin-bottom: 1.25rem; 67 | font-size: 1.25rem; 68 | line-height: 1.4; 69 | } 70 | 71 | .featurette-heading { 72 | font-size: 50px; 73 | } 74 | } 75 | 76 | @media (min-width: 62em) { 77 | .featurette-heading { 78 | margin-top: 7rem; 79 | } 80 | } 81 | 82 | .carousel-logo { 83 | position: absolute; 84 | top: 18%; 85 | left: 25%; 86 | margin-right: -50%; 87 | transform: translate(-25%, -18%) auto; 88 | width: 50%; 89 | filter: invert(100%); 90 | } 91 | -------------------------------------------------------------------------------- /public/javascripts/activity-card.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | var gradebook = require('./gradebook'); 4 | var database = require('./database'); 5 | 6 | var users = require('./users'); 7 | 8 | var displayProgress = function( card, progress ) { 9 | var progressBar = $('.progress-bar', card); 10 | progressBar.css('width', Math.round(progress * 100).toString() + '%' ); 11 | progressBar.toggleClass('progress-bar-striped', progress > 0.9999); 12 | }; 13 | 14 | var createActivityCard = function() { 15 | var activityCard = $(this); 16 | var href = activityCard.attr('data-path'); 17 | 18 | //////////////////////////////////////////////////////////////// 19 | // add counters 20 | var itself = 0; 21 | if (activityCard.hasClass('chapter')) itself = 1; 22 | activityCard.attr( 'data-chapter-counter', activityCard.prevAll('.activity-card.chapter').length + itself ); 23 | var label = activityCard.attr( 'data-chapter-counter' ); 24 | 25 | if (!(activityCard.hasClass('chapter'))) { 26 | activityCard.attr( 'data-section-counter', activityCard.prevUntil('.activity-card.chapter', '.activity-card' ).not('.part').length + 1 ); 27 | label = label + "." + activityCard.attr( 'data-section-counter' ); 28 | } 29 | 30 | if ((activityCard.attr( 'data-chapter-counter' ) != "0") && (!(activityCard.hasClass('part')))) { 31 | $('h4', activityCard).prepend( '' + label + '' ); 32 | } 33 | 34 | // This is the new method for storing completion data 35 | var repositoryName = activityCard.attr('data-repository-name'); 36 | var activityPath = activityCard.attr('data-path'); 37 | 38 | if (repositoryName) { 39 | database.onCompletion( repositoryName, activityPath, function(c) { 40 | displayProgress( activityCard, c ); 41 | activityCard.attr('data-max-completion', c ); 42 | gradebook.update(); 43 | }); 44 | } 45 | }; 46 | 47 | $.fn.extend({ 48 | activityCard: function() { 49 | return this.each( createActivityCard ); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /public/javascripts/users.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | var moment = require('moment'); 4 | 5 | exports.get = _.memoize( function(userId) { 6 | return $.ajax({ 7 | url: "/users/" + userId, 8 | headers: {Accept: "application/json;charset=utf-8"}, 9 | }); 10 | }); 11 | 12 | function me() { 13 | return exports.get('me'); 14 | }; 15 | 16 | exports.me = me; 17 | 18 | $(document).ready(function() { 19 | me().then( function(user) { 20 | if (user.isGuest === false) { 21 | $('#loginUser').show(); 22 | 23 | if (user.name.split(' ')[0]) 24 | $('#userFirstName').text(user.name.split(' ')[0]); 25 | } else { 26 | $('#loginGuest').show(); 27 | } 28 | 29 | // Instructors should see a "statistics" button 30 | if (user.instructorRepositoryPaths) { 31 | $('#menu-supervise').show(); 32 | 33 | user.instructorRepositoryPaths.forEach( function(p) { 34 | if (window.location.pathname.startsWith( p )) 35 | $('#instructor-view-statistics').show(); 36 | if (window.location.pathname.startsWith( '/' + p )) 37 | $('#instructor-view-statistics').show(); 38 | }); 39 | } 40 | 41 | // If there's git content loaded... 42 | var repositoryName = $('main').attr('data-repository-name'); 43 | var xourse = $('main').attr('data-xourse-path'); 44 | if (xourse && repositoryName) { 45 | // and if we have 46 | if (user.bridges) { 47 | var assignment = undefined; 48 | user.bridges.forEach( function(bridge) { 49 | if ((bridge.path == xourse) && (bridge.repository == repositoryName)) { 50 | assignment = bridge; 51 | } 52 | }); 53 | 54 | if (assignment) { 55 | var dueDate = moment(Date.parse(assignment.dueDate)); 56 | if (dueDate.isValid()) { 57 | $('#dueDateCountdown').text( dueDate.fromNow() ); 58 | $('#dueDate').attr('title', "Due at " + dueDate.format('LLLL') ); 59 | $('#dueDate').show(); 60 | $('#dueDate').tooltip(); 61 | 62 | window.setInterval( function() { 63 | $('#dueDateCountdown').text( dueDate.fromNow() ); 64 | }, 1000); 65 | } 66 | } 67 | } 68 | } 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /views/activity-card.pug: -------------------------------------------------------------------------------- 1 | mixin practiceXourseCardOld(locator, url, card, active) 2 | a.activity-card(class=card.cssClass, data-weight=card.weight, href=locator + "/" + url, data-repository-name=repositoryName, data-path=url, style=style, data-hashes=JSON.stringify(card.hashes)) 3 | div.progress.vertical 4 | div.progress-bar.progress-bar-success(role="progressbar", style="width: 0%;") 5 | 6 | mixin practiceXourseCard(locator, url, card, active) 7 | .activity-card.text-center(class=card.cssClass + (active ? " active" : ""),data-weight=card.weight, href=locator + "/" + url, data-repository-name=repositoryName, data-path=url, style=style, data-hashes=JSON.stringify(card.hashes),data-toggle="tooltip",data-placement="bottom",title=card.title ? card.title : 'Practice') 8 | a(href=locator + "/" + url) 9 | div.progress.vertical 10 | div.progress-bar.bg-success(role="progressbar", style="width: 0%;") 11 | 12 | mixin titleXourseCard(locator, url, card, active) 13 | .activity-card.card-sectionheading(class=card.cssClass) 14 | div.card-block 15 | h4.card-title !{card.title} 16 | 17 | mixin regularXourseCard(locator, url, card, active) 18 | .activity-card(class=card.cssClass + (active ? " active" : ""),data-weight=card.weight, href=locator + "/" + url, data-repository-name=repositoryName, data-path=url, style=style, data-hashes=JSON.stringify(card.hashes)) 19 | a(href=locator + "/" + url) 20 | div.card-header.p-2 21 | div.progress 22 | div.progress-bar.bg-success(role="progressbar", style="width: 0%;") 23 | div.card-block 24 | h4.card-title !{card.title} 25 | div.card-text !{card.summary} 26 | 27 | mixin xourseCard(repositoryName, xourseUrl, url, card, active) 28 | - var locator = ((learner && (learner._id != user._id)) ? ('/users/' + learner._id) : '') + ('/' + repositoryName + '/' + xourseUrl); 29 | if (card.cssClass) && (card.cssClass.match( /practice/ )) 30 | +practiceXourseCard( locator, url, card, active ) 31 | else 32 | if url.match(/^#/) 33 | +titleXourseCard( locator, url, card, active ) 34 | else 35 | +regularXourseCard( locator, url, card, active ) 36 | 37 | 38 | -------------------------------------------------------------------------------- /map-reduce/progress-reports.js: -------------------------------------------------------------------------------- 1 | var mongo = new Mongo(); 2 | var db = mongo.getDB("test"); 3 | 4 | // mdb.Completion.update({activityHash: req.params.activityHash, user: req.user._id}, {$set: {complete: req.body.complete, date: new Date()}}, {upsert: true}, function (err, affected, raw) { 5 | 6 | db.gradebooks.drop(); 7 | 8 | db.completions.aggregate( [ 9 | { $match : { 10 | activityHash: { $exists: true } 11 | }}, 12 | { $lookup : { 13 | from: "users", 14 | localField: "user", 15 | foreignField: "_id", 16 | as: "user", 17 | }}, 18 | { $match : { 19 | "user.isGuest": false, 20 | }}, 21 | { "$project" : { 22 | "user.email": 1, 23 | "user.name": 1, 24 | "user._id": 1, 25 | "complete": 1, 26 | "activityHash": 1, 27 | }}, 28 | { $lookup : { 29 | from: "activities", 30 | localField: "activityHash", 31 | foreignField: "hash", 32 | as: "activity", 33 | }}, 34 | { $unwind : { 35 | path: "$activity", 36 | }}, 37 | { $lookup : { 38 | from: "branches", 39 | localField: "activity.commit", 40 | foreignField: "commit", 41 | as: "branch", 42 | }}, 43 | { $unwind : { 44 | path: "$branch", 45 | }}, 46 | { "$project" : { 47 | complete: 1, 48 | user: 1, 49 | path: "$activity.path", 50 | "title": "$activity.title", 51 | course: { $concat: [ "$branch.owner", "/", "$branch.repository" ] }, 52 | commit: "$activity.commit" 53 | }}, 54 | { "$group": { 55 | "_id": { 56 | "path": "$path", 57 | "course": "$course", 58 | user: "$user", 59 | }, 60 | "commits": { "$addToSet" : "$commit" }, 61 | "title": { "$first" : "$title" }, 62 | "complete": { "$max": "$complete" } 63 | }}, 64 | { "$group": { 65 | "_id" : { 66 | "course": "$_id.course", 67 | user: "$_id.user", 68 | }, 69 | "paths": { "$push": { "complete": "$complete", 70 | "path": "$_id.path" } }, 71 | "commits": { "$first" : "$commits" }, 72 | }}, 73 | { "$group": { 74 | "_id" : "$_id.course", 75 | "users": { "$push": { "paths": "$paths", 76 | "user": "$_id.user.email" } }, 77 | "commits": { "$first" : "$commits" }, 78 | }}, 79 | { $out : "gradebooks" } 80 | ] ); 81 | -------------------------------------------------------------------------------- /views/layouts/navbar/login.pug: -------------------------------------------------------------------------------- 1 | div#loginUser(style="display: none;") 2 | li.nav-item.dropdown 3 | a.nav-link.dropdown-toggle(id="navbarLogin",data-toggle="dropdown",aria-haspopup="true",aria-expanded="false") 4 | // If we're masquerading as someone 5 | if learner 6 | | 7 | span.hidden-md-down#userMasqueradingName “#{learner.name.split(" ")[0]}” 8 | else 9 | | 10 | span.hidden-md-down#userFirstName Me 11 | div.dropdown-menu.dropdown-menu-right(aria-labelledby="navbarLogin") 12 | if learner 13 | a.dropdown-item(href=url.replace('users/' + learner._id + '/', '')) Stop Pretending 14 | div.dropdown-divider 15 | a.dropdown-item(href=`/users/me`) Profile 16 | a.dropdown-item#menu-supervise(href='/supervise',style="display: none;") Supervise 17 | a.dropdown-item(href="/logout") Logout 18 | div#loginGuest(style="display: none;") 19 | li.nav-item.dropdown 20 | a.nav-link.dropdown-toggle(id="navbarLogin",data-toggle="dropdown",aria-haspopup="true",aria-expanded="false") 21 | | 22 | span.hidden-md-down  Sign In 23 | div.dropdown-menu.dropdown-menu-right(aria-labelledby="navbarLogin") 24 | if config.googleAuth 25 | a.dropdown-item(href="/auth/google") Sign In with Google 26 | if config.twitterAuth 27 | a.dropdown-item(href="/auth/twitter") Sign In with Twitter 28 | if config.githubAuth 29 | a.dropdown-item(href="/auth/github") Sign In with GitHub 30 | if config.localAuth 31 | div.dropdown-divider 32 | form#form-login(action="/auth/local",method="post") 33 | div.input-group 34 | input.form-control#username(name="username",type="text",placeholder="Username") 35 | div.input-group 36 | input.form-control#password(name="password",type="password") 37 | button.btn.btn-primary.btn-default#button-login(type="submit") Sign In 38 | -------------------------------------------------------------------------------- /public/javascripts/popover.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | var MathJax = require('./mathjax'); 4 | var Expression = require('math-expressions'); 5 | 6 | exports.bindPopover = function(element) { 7 | var update = function() { 8 | var text = $(element).find( "input" ).val(); 9 | exports.displayPopover( text, element ); 10 | }; 11 | 12 | $(element).popover({ 13 | animation: false, 14 | placement: 'right', 15 | container: 'body', 16 | //animation: false, 17 | trigger: 'manual', 18 | content: function() { 19 | return ''; 20 | }}); 21 | 22 | var inputBox = $(element).find( "input.form-control" ); 23 | 24 | inputBox.on( 'input', update ); 25 | 26 | inputBox.focus( update ); 27 | 28 | inputBox.blur( function () { 29 | $(element).popover('hide'); 30 | }); 31 | 32 | inputBox.focusout( function () { 33 | $(element).popover('hide'); 34 | }); 35 | } 36 | 37 | // Binds latex popover occur next to element when watched variable changes. 38 | exports.displayPopover = function(answer, element) { 39 | if (answer.trim().length == 0) { 40 | $(element).popover('hide'); 41 | return; 42 | } 43 | 44 | if (answer.trim().length == 0) { 45 | $(element).popover('hide'); 46 | return; 47 | } 48 | 49 | try { 50 | var format = $(element).attr('data-format'); 51 | 52 | if (format == 'string') { 53 | return; 54 | } 55 | 56 | if ((format == 'integer') && (answer.trim().match( /[^0-9-]/ ))) { 57 | throw 'Expecting an integer.'; 58 | } 59 | 60 | // Don't need to give a preview for numeric answers 61 | if (answer.trim().match( /^-?[0-9\.]+$/ )) { 62 | $(element).popover('hide'); 63 | return; 64 | } 65 | 66 | var latex = Expression.fromText(answer).tex(); 67 | 68 | $(element).data('bs.popover').config.title = ''; 69 | $(element).data('bs.popover').config.content = '\\(' + latex + '\\)'; 70 | $(element).popover('show'); 71 | MathJax.Hub.Queue(["Typeset", MathJax.Hub, $(element).data('bs.popover').tip[0]]); 72 | } 73 | // display errors as popovers, too 74 | catch (err) { 75 | $(element).data('bs.popover').config.title = 'Error'; 76 | $(element).data('bs.popover').config.content = err; 77 | $(element).popover('show'); 78 | } 79 | } 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /views/instructors.pug: -------------------------------------------------------------------------------- 1 | extends layouts/main 2 | 3 | block title 4 | if xourse && xourse.title 5 | | #{xourse.title} — Instructors 6 | else 7 | | Instructors 8 | 9 | mixin instructorPanel(instructor) 10 | div.media 11 | img.d-flex.mr-3(src=instructor.imageUrl) 12 | div.media-body 13 | h5 #{instructor.name} 14 | if instructor.bridge.roles.filter( f => f.match( /Instructor/ ) ).length > 0 15 | Instructor  16 | else 17 | if instructor.bridge.roles.filter( f => f.match( /TeachingAssistant/ ) ).length > 0 18 | Instructor  19 | else 20 | for role in instructor.bridge.roles 21 | #{role}  22 | if instructor.bridge.contextLabel || instructor.bridge.toolConsumerInstanceName 23 | small.text-muted   24 | if instructor.bridge.contextLabel 25 | | #{instructor.bridge.contextLabel} 26 | if instructor.bridge.toolConsumerInstanceName 27 | |  at #{instructor.bridge.toolConsumerInstanceName} 28 | a.btn.btn-secondary(href='mailto:' + instructor.email + '?subject=' + subject + '&body=' + body) 29 | | 30 | span  Send E-Mail 31 | 32 | block content 33 | nav(aria-label="breadcrumb",role="navigation") 34 | ol.breadcrumb 35 | li.breadcrumb-item 36 | - var level = 1; 37 | a(href="/" + repositoryName, aria-level=level) 38 | | !{repositoryName} 39 | li.breadcrumb-item 40 | - level = level + 1; 41 | a(href="/" + repositoryName + "/" + xourse.path,aria-level=level) 42 | if (xourse.title) && (xourse.title != '') 43 | | !{xourse.title} 44 | else 45 | | Home 46 | li.breadcrumb-item.active 47 | | Instructional Staff 48 | .container 49 | div.row 50 | if instructors.length == 0 51 | p I am afraid that I do not know the names of your instructors. Please email ximera-help@osu.edu and we will try to help you find them. 52 | else 53 | for instructor in instructors 54 | +instructorPanel(instructor) 55 | -------------------------------------------------------------------------------- /map-reduce/map-reduce.js: -------------------------------------------------------------------------------- 1 | var mongo = new Mongo(); 2 | var db = mongo.getDB("test"); 3 | 4 | var mapResponse = function() { 5 | var url = this.object.id.split('/'); 6 | var hash = url[4]; 7 | var problem = url[6]; 8 | var answer = url[8]; 9 | 10 | var result = {}; 11 | result[problem] = {}; 12 | result[problem][answer] = {}; 13 | 14 | var response = this.result.response; 15 | 16 | if (typeof response == 'string') 17 | response = response.replace(/\0/g, '' ); 18 | 19 | if (Array.isArray(response)) { 20 | response.forEach( function(r) { 21 | result[problem][answer][r] = 1; 22 | }); 23 | } else { 24 | result[problem][answer][response] = 1; 25 | } 26 | 27 | emit(hash, result); 28 | }; 29 | 30 | var mapSuccess = function() { 31 | var url = this.object.id.split('/'); 32 | var hash = url[4]; 33 | var problem = url[6]; 34 | var answer = url[8]; 35 | 36 | var result = {}; 37 | result[problem] = {}; 38 | result[problem][answer] = {}; 39 | 40 | var success = this.result.success; 41 | 42 | result[problem][answer][success] = 1; 43 | 44 | emit(hash, result); 45 | }; 46 | 47 | 48 | var reduce = function(key, values) { 49 | var result = {}; 50 | 51 | values.forEach( function(v) { 52 | 53 | Object.keys(v).forEach( function(problem) { 54 | if (!(result[problem])) 55 | result[problem] = {}; 56 | 57 | Object.keys(v[problem]).forEach( function(answer) { 58 | if (!(result[problem][answer])) 59 | result[problem][answer] = {}; 60 | 61 | Object.keys(v[problem][answer]).forEach( function(response) { 62 | if (!(result[problem][answer][response])) 63 | result[problem][answer][response] = 0; 64 | 65 | result[problem][answer][response] += v[problem][answer][response]; 66 | }); 67 | }); 68 | }); 69 | }); 70 | 71 | return result; 72 | }; 73 | 74 | db.learningrecords.mapReduce( mapResponse, reduce, 75 | { out: "answers", 76 | query: { verbId: "http://adlnet.gov/expapi/verbs/answered" } 77 | }); 78 | 79 | db.learningrecords.mapReduce( mapSuccess, reduce, 80 | { out: "successes", 81 | query: { verbId: "http://adlnet.gov/expapi/verbs/answered" } 82 | }); 83 | 84 | // I suppose I might miss some -- oh well! 85 | db.ServerEvent.insert( { event: "MapReduce", timestamp: new ISODate() } ); 86 | 87 | -------------------------------------------------------------------------------- /routes/supervising.js: -------------------------------------------------------------------------------- 1 | var mdb = require('../mdb'); 2 | var mongo = require('mongodb'); 3 | 4 | 5 | exports.watch = function( req, res, next ) { 6 | res.render('watch', { user: req.user } ); 7 | } 8 | 9 | exports.isInstructorForLearnerInRepository = function( repositoryName, supposedInstructor, supposedLearner, callback ) { 10 | mdb.LtiBridge.find({user: supposedInstructor._id}, function(err, instructorBridges) { 11 | if (err) { 12 | callback(err, false); 13 | return; 14 | } 15 | 16 | mdb.LtiBridge.find({user: supposedLearner._id}, function(err, learnerBridges) { 17 | if (err) { 18 | callback(err, false); 19 | return; 20 | } 21 | 22 | callback( null, instructorBridges.some( function(instructorBridge) { 23 | // The instructor is actually an instructor of some sort... 24 | return instructorBridge.roles.some( function(role) { 25 | return role.match( /Instructor/ ) || role.match( /Administrator/ ) || role.match( /TeachingAssistant/ ) || role.match( /Grader/ ); 26 | }) && 27 | // and the learner is actually in that course 28 | learnerBridges.some( function(learnerBridge) { 29 | return (instructorBridge.toolConsumerInstanceGuid == learnerBridge.toolConsumerInstanceGuid) && 30 | (instructorBridge.contextId == learnerBridge.contextId) && 31 | (instructorBridge.repository == learnerBridge.repository) && 32 | (instructorBridge.repository == repositoryName); 33 | }); 34 | })); 35 | }); 36 | }); 37 | }; 38 | 39 | // Used when we want to view page as another learner 40 | exports.masquerade = function(req,res,next) { 41 | mdb.User.findOne({_id: new mongo.ObjectID(req.params.masqueradingUserId)}, 42 | function(err, learner) { 43 | if (err) { 44 | next(err); 45 | return; 46 | } 47 | 48 | if (!learner) { 49 | next('Could not find user with id ' + req.params.masqueradingUserId); 50 | return; 51 | } 52 | 53 | exports.isInstructorForLearnerInRepository( req.params.repository, req.user, learner, 54 | function(err, good) { 55 | if (err) { 56 | next(err); 57 | } else { 58 | if (!good) 59 | // Should be a different HTTP error code 60 | next('You do not have permission to see the work of that learner.'); 61 | else { 62 | req.learner = learner; 63 | next(); 64 | } 65 | } 66 | }); 67 | }); 68 | }; 69 | 70 | 71 | -------------------------------------------------------------------------------- /public/javascripts/coding.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | var database = require('./database'); 4 | var CodeMirror = require('codemirror'); 5 | require('codemirror-javascript'); 6 | 7 | var createCoding = function() { 8 | var element = $(this); 9 | 10 | var initialContent = $('script',element).text(); 11 | $('script',element).remove(); 12 | 13 | var myCodeMirror = CodeMirror(element.get(0), { 14 | value: "", 15 | mode: "javascript", 16 | lineNumbers: true, 17 | lineWrapping: true, 18 | }); 19 | 20 | // update database from view 21 | myCodeMirror.on( "changes", function (e) { 22 | $(element).persistentData( 'code', myCodeMirror.getValue() ); 23 | }); 24 | 25 | // update view from database 26 | $(element).persistentData( function(event) { 27 | console.log(event); 28 | if ( ! $(element).persistentData('code')) { 29 | console.log("empty code"); 30 | myCodeMirror.setValue( initialContent ); 31 | } 32 | 33 | if ('code' in event.data) { 34 | if (event.data['code'] != myCodeMirror.getValue()) { 35 | myCodeMirror.setValue( event.data['code'] ); 36 | } 37 | } 38 | }); 39 | 40 | var button = $(''); 41 | element.append( button ); 42 | button.wrapInner( $('
') ); 43 | 44 | button.click( function() { 45 | $('#p5-canvas',element).remove(); 46 | element.append( $('
') ); 47 | $('#p5-canvas',element).css( 'width', '750px' ); 48 | $('#p5-canvas',element).css( 'height', '500px' ); 49 | 50 | $('#p5-canvas').hover(function() { 51 | console.log('DISABLE'); 52 | $(document).keydown(function(e) { 53 | var key = e.which; 54 | if(key==35 || key == 36 || key == 37 || key == 39) { 55 | e.preventDefault(); 56 | return false; 57 | } 58 | return true; 59 | }); 60 | }, function() { 61 | //$("body").css("overflow","auto"); 62 | }); 63 | 64 | var prefix = 'for(var k in p) { window[k]=p[k]; if (window[k] && window[k].bind) window[k] = window[k].bind(p); } ; var width = 750; var height = 500;'; 65 | var sneaky = Function('p', prefix + myCodeMirror.getValue() ); 66 | window.sketch = sneaky; 67 | new window.p5(sneaky, 'p5-canvas'); 68 | }); 69 | 70 | return; 71 | }; 72 | 73 | $.fn.extend({ 74 | coding: function() { 75 | return this.each( createCoding ); 76 | } 77 | }); 78 | 79 | -------------------------------------------------------------------------------- /routes/instructors.js: -------------------------------------------------------------------------------- 1 | var mdb = require('../mdb'); 2 | var async = require('async'); 3 | var config = require('../config'); 4 | 5 | exports.index = function(req, res, next) { 6 | var activity = req.activity; 7 | 8 | if (activity.kind != 'xourse') { 9 | next('Only xourses have instructors.'); 10 | return; 11 | } 12 | 13 | var xourse = activity; 14 | xourse.path = req.activity.path; 15 | if (xourse.path) { 16 | xourse.path = xourse.path.replace(/\.html$/,'') 17 | } 18 | xourse.hash = req.activity.hash; 19 | 20 | var instructorBridges = []; 21 | 22 | async.waterfall([ 23 | function(callback) { 24 | mdb.LtiBridge.find( {user: req.user._id, 25 | repository: req.repositoryName, 26 | path: xourse.path}, 27 | callback); 28 | }, 29 | function(bridges, callback) { 30 | callback( null, bridges.map( function(b) { return b.contextId; } ) ); 31 | }, 32 | function(contexts, callback) { 33 | var instructorRoles = ['Instructor', 'TeachingAssistant', 'urn:lti:role:ims/lis/TeachingAssistant', 'urn:lti:role:ims/lis/Instructor']; 34 | mdb.LtiBridge.find( {roles: { $elemMatch: { $in: instructorRoles } }, 35 | contextId: {$in: contexts}, 36 | repository: req.repositoryName, 37 | path: xourse.path}, 38 | callback ); 39 | }, 40 | function(bridges, callback) { 41 | instructorBridges = bridges; 42 | callback( null, bridges.map( function(b) { return b.user; } ) ); 43 | }, 44 | function(instructorIds, callback) { 45 | mdb.User.find( {_id: { $in: instructorIds } }, callback ); 46 | } 47 | ], function (err, instructors) { 48 | if (err) 49 | next(err); 50 | else { 51 | instructors.forEach( function(i) { 52 | instructorBridges.forEach( function(b) { 53 | if (b.user.toString() == i._id.toString()) { 54 | i.bridge = b; 55 | } 56 | }); 57 | }); 58 | 59 | var body = "Dear professor,\n\n"; 60 | if (req.headers.referer) 61 | body = body + "I am having trouble with " + req.headers.referer + "\n\n"; 62 | body = body + "\n\nSincerely,\n" + req.user.name; 63 | body = body.replace(/\n/g,'%0D%0A'); 64 | body = body.replace(config.root, config.root + '/users/' + req.user._id ); 65 | 66 | var subject = 'Help with ' + xourse.title; 67 | subject = subject.replace(/ +/g, ' '); 68 | 69 | res.render('instructors', { 70 | xourse: xourse, 71 | instructors: instructors, 72 | subject: subject, 73 | body: body, 74 | repositoryName: req.repositoryName 75 | } ); 76 | } 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /public/javascripts/index.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var callout = $("#myCarousel"); 3 | 4 | callout.css('background','black'); 5 | 6 | callout.children().css('z-index',50); 7 | 8 | for (var i = 0; i < 30; i++) { 9 | callout.append('
X
'); 10 | } 11 | 12 | callout.css('overflow', 'hidden'); 13 | 14 | $( '.eks' ).each(function( index ) { 15 | 16 | function start( element ) { 17 | var width = callout.width(); 18 | var height = callout.outerHeight(); 19 | 20 | $(element).css({ 21 | position: 'absolute', 22 | color: 'white', 23 | fontSize: 200 + 0.5 * Math.random() * height, 24 | fontWeight: 800, 25 | opacity: 0.15 + 0.10 * Math.random(), 26 | zIndex: 10, 27 | '-webkit-user-select': 'none', 28 | '-moz-user-select': 'none', 29 | '-ms-user-select': 'none', 30 | 'user-select': 'none', 31 | '-o-user-select': 'none' 32 | }); 33 | 34 | var eksWidth = $(element).width(); 35 | var eksHeight = $(element).outerHeight(); 36 | 37 | var left = 0; 38 | var top = 0; 39 | var finishLeft = 0; 40 | var finishTop = 0; 41 | 42 | if (Math.random() > 0.5) { 43 | top = Math.random() * height; 44 | finishTop = Math.random() * height; 45 | 46 | if (Math.random() > 0.5) { 47 | left = -eksWidth; 48 | finishLeft = width; 49 | } else { 50 | left = width; 51 | finishLeft = -eksWidth; 52 | } 53 | } else { 54 | left = Math.random() * width; 55 | finishLeft = Math.random() * width; 56 | 57 | if (Math.random() > 0.5) { 58 | top = -eksHeight; 59 | finishTop = height; 60 | } else { 61 | top = height; 62 | finishTop = -eksHeight; 63 | } 64 | } 65 | 66 | if ($(element).attr('data-repeated')) { 67 | $(element).css({ 68 | left : left, 69 | top : top 70 | }); 71 | } else { 72 | $(element).css({ 73 | left : Math.random() * width, 74 | top : Math.random() * height 75 | }); 76 | 77 | $(element).attr('data-repeated', true ); 78 | } 79 | 80 | 81 | $(element).transition({ left: finishLeft, 82 | top: finishTop 83 | }, 84 | 10000 + 50000 * Math.random(), 85 | function() { 86 | start(this); 87 | } 88 | /* 89 | { duration: 20000 + 30000 * Math.random(), 90 | easing: 'linear', 91 | done: function() { 92 | start(this); 93 | }*/ 94 | ); 95 | } 96 | 97 | start(this); 98 | 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /summarize/read-lrs.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var snappy = require('snappy'); 3 | var async = require('async'); 4 | var buffer24 = require("buffer24"); 5 | var uint32 = require('uint32'); 6 | var crc32 = require('fast-crc32c'); 7 | 8 | function processLearningRecords( filename, position, loop, callback ) { 9 | fs.open(filename, fs.constants.O_RDONLY, function(err, fd) { 10 | if (err) { 11 | callback(err); 12 | } else { 13 | async.forever( 14 | function(next) { 15 | var headerBuffer = Buffer.alloc(4); 16 | fs.read( fd, headerBuffer, 0, 4, position, function(err, bytesRead, lengthBuffer) { 17 | if (err) { 18 | next(err); 19 | } else { 20 | if (bytesRead != 4) { 21 | next('end of file'); 22 | } else { 23 | var kind = headerBuffer.readUInt8(0); 24 | var length = lengthBuffer.readUInt24LE(1); 25 | var buffer = Buffer.alloc(length); 26 | 27 | fs.read( fd, buffer, 0, length, position + 4, function(err, bytesRead, buffer) { 28 | position = position + 4 + length; 29 | if (err) { 30 | next(err); 31 | } else { 32 | if (bytesRead != length) { 33 | next('end of file'); 34 | } else { 35 | if (kind != 0) { 36 | next(null); 37 | } else { 38 | snappy.uncompress(buffer.slice(4), { asBuffer: false }, function (err, original) { 39 | if (err) { 40 | next(err); 41 | } else { 42 | var checksum = crc32.calculate(original, 0); 43 | var maskedChecksum = uint32.addMod32( uint32.rotateRight(checksum, 15), 0xa282ead8 ); 44 | var recordedChecksum = buffer.readUInt32LE(0); 45 | if (maskedChecksum != recordedChecksum) { 46 | next('incorrect checksum'); 47 | } else { 48 | // should also send file offset 49 | loop( JSON.parse(original), 50 | next ); 51 | } 52 | } 53 | }); 54 | } 55 | } 56 | } 57 | }); 58 | } 59 | } 60 | }); 61 | }, 62 | function(err) { 63 | if (err == 'end of file') 64 | callback(null, position); 65 | else 66 | callback(err, position); 67 | }); 68 | } 69 | }); 70 | } 71 | 72 | exports.read = processLearningRecords; 73 | 74 | /* 75 | processLearningRecords( "repositories/sample.git/learning-record-store", 0, 76 | function( entry, callback ) { 77 | console.log(entry); 78 | callback(null); 79 | }, 80 | function(err, position) { 81 | if (err) { 82 | console.log(err); 83 | } else { 84 | console.log("done at", position); 85 | } 86 | }); 87 | */ 88 | -------------------------------------------------------------------------------- /map-reduce/map-reduce-incremental.js: -------------------------------------------------------------------------------- 1 | var mongo = new Mongo(); 2 | var db = mongo.getDB("test"); 3 | 4 | var mapResponse = function() { 5 | var url = this.object.id.split('/'); 6 | var hash = url[4]; 7 | var problem = url[6]; 8 | var answer = url[8]; 9 | 10 | var result = {}; 11 | result[problem] = {}; 12 | result[problem][answer] = {}; 13 | 14 | var response = this.result.response; 15 | 16 | if (typeof response == 'string') 17 | response = response.replace(/\0/g, '' ); 18 | 19 | if (Array.isArray(response)) { 20 | response.forEach( function(r) { 21 | result[problem][answer][r] = 1; 22 | }); 23 | } else { 24 | result[problem][answer][response] = 1; 25 | } 26 | 27 | emit(hash, result); 28 | }; 29 | 30 | var mapSuccess = function() { 31 | var url = this.object.id.split('/'); 32 | var hash = url[4]; 33 | var problem = url[6]; 34 | var answer = url[8]; 35 | 36 | var result = {}; 37 | result[problem] = {}; 38 | result[problem][answer] = {}; 39 | 40 | var success = this.result.success; 41 | 42 | result[problem][answer][success] = 1; 43 | 44 | emit(hash, result); 45 | }; 46 | 47 | 48 | var reduce = function(key, values) { 49 | var result = {}; 50 | 51 | values.forEach( function(v) { 52 | 53 | Object.keys(v).forEach( function(problem) { 54 | if (!(result[problem])) 55 | result[problem] = {}; 56 | 57 | Object.keys(v[problem]).forEach( function(answer) { 58 | if (!(result[problem][answer])) 59 | result[problem][answer] = {}; 60 | 61 | Object.keys(v[problem][answer]).forEach( function(response) { 62 | if (!(result[problem][answer][response])) 63 | result[problem][answer][response] = 0; 64 | 65 | result[problem][answer][response] += v[problem][answer][response]; 66 | }); 67 | }); 68 | }); 69 | }); 70 | 71 | return result; 72 | }; 73 | 74 | var x = db.ServerEvent.find( { event: "MapReduce" } ).sort( { timestamp: -1 } ).limit(1); 75 | var lastReduce = x[0].timestamp; 76 | 77 | db.learningrecords.mapReduce( mapResponse, reduce, 78 | { out: { reduce: "answers" }, 79 | query: { verbId: "http://adlnet.gov/expapi/verbs/answered", 80 | stored: { $gt: lastReduce } 81 | } 82 | }); 83 | 84 | db.learningrecords.mapReduce( mapSuccess, reduce, 85 | { out: { reduce: "successes" }, 86 | query: { verbId: "http://adlnet.gov/expapi/verbs/answered", 87 | stored: { $gt: lastReduce } 88 | } 89 | }); 90 | 91 | // I suppose I might miss some -- oh well! 92 | db.ServerEvent.insert( { event: "MapReduce", description: "Incremental MapReduce", timestamp: new ISODate() } ); 93 | -------------------------------------------------------------------------------- /public/javascripts/activity.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | var MathJax = require('mathjax'); 4 | var TinCan = require('./tincan'); 5 | var ProgressBar = require('./progress-bar'); 6 | 7 | var activityCard = require('./activity-card'); 8 | var problem = require('./problem'); 9 | var mathAnswer = require('./math-answer'); 10 | var multipleChoice = require('./multiple-choice'); 11 | var selectAll = require('./select-all'); 12 | var wordChoice = require('./word-choice'); 13 | var hint = require('./hint'); 14 | var foldable = require('./foldable'); 15 | var youtube = require('./youtube'); 16 | 17 | var freeResponse = require('./free-response'); 18 | var coding = require('./coding'); 19 | var shuffle = require('./shuffle'); 20 | var feedback = require('./feedback'); 21 | var validator = require('./validator'); 22 | var javascript = require('./javascript'); 23 | 24 | var connectInteractives = require('./interactives').connectInteractives; 25 | 26 | var database = require('./database'); 27 | 28 | var annotator = require('./annotator'); 29 | 30 | var createActivity = function() { 31 | var activity = $(this); 32 | 33 | //$('.activity-body', this).annotator(); 34 | 35 | activity.fetchData( function() { 36 | activity.persistentData( function() { 37 | if (!(activity.persistentData( 'experienced' ))) { 38 | TinCan.experience(activity); 39 | activity.persistentData( 'experienced', true ); 40 | } 41 | }); 42 | 43 | ProgressBar.monitorActivity( activity ); 44 | 45 | $(".problem-environment", activity).problemEnvironment(); 46 | $(".multiple-choice", activity).multipleChoice(); 47 | $(".select-all", activity).selectAll(); 48 | $(".word-choice", activity).wordChoice(); 49 | $(".hint", activity).hint(); 50 | $(".foldable", activity).foldable(); 51 | $(".free-response", activity).freeResponse(); 52 | $(".javascript-code", activity).coding(); 53 | 54 | $(".shuffle", activity).shuffle(); 55 | $(".feedback", activity).feedback(); 56 | $(".validator", activity).validator(); 57 | $(".inline-javascript", activity).javascript(); 58 | $('.youtube-player', activity).youtube(); 59 | 60 | connectInteractives(); 61 | 62 | $('.activity-card').activityCard(); 63 | }); 64 | }; 65 | 66 | $.fn.extend({ 67 | activity: function() { 68 | return this.each( createActivity ); 69 | }, 70 | 71 | recordCompletion: function(proportionComplete) { 72 | var hash = $(this).activityHash(); 73 | 74 | if (hash != undefined) { 75 | var repositoryName = $(this).repositoryName(); 76 | var activityPath = $(this).activityPath(); 77 | 78 | database.setCompletion( repositoryName, activityPath, proportionComplete ); 79 | } 80 | 81 | return; 82 | } 83 | }); 84 | 85 | -------------------------------------------------------------------------------- /blog/cpc.markdown: -------------------------------------------------------------------------------- 1 | {{{ 2 | "title" : "MOOCulus at the Coursera Partners Conference", 3 | "tags" : [ "calculus one" ], 4 | "category" : "calculus one", 5 | "date" : "5-3-2013", 6 | "author" : "Bart Snapp" 7 | }}} 8 | 9 | 10 | 11 | I've just returned from the Coursera Partners conference. This was a 12 | chance for people who are teaching massive open online courses (MOOCs) 13 | and those who are interested in these things to get together and 14 | share/learn/collaborate. I got to meet up with my old teacher, 15 | friend, and competitor Rob Ghrist. 16 | 17 | 18 | 19 | 20 | You see, Jim Fowler, Steve Gubkin, a few others, and I at OSU have been 21 | running a Calculus One MOOC that we call "MOOCulus." Rob Ghrist is 22 | doing something similar at the University of Pennsylvania, though his 23 | MOOC doesn't have a funny little name. Ok, I suppose it 24 | does—since he is basically teaching out of his beautiful book, 25 | titled "Funny Little Calculus Text." 26 | 27 | Despite the fact that we are now technically in direct competition, 28 | since we are both teaching calculus MOOCs, I don’t think we really 29 | view ourselves as competitors—rather we see each other as vanguards, 30 | cheering at each other’s successes. After talking to the people at the 31 | conference, I think most (if not all) of us honestly see the MOOC 32 | phenomenon as something of a “force of nature” and rather than fight 33 | the current of these murky waters, we choose to learn to surf the 34 | waves. 35 | 36 | We face many obstacles on our way. One is that we as academics are 37 | unsure what our roles as teachers would be in a post-MOOC society. I'm 38 | an optimist. I see MOOCs as a way to enhance education. The practice, 39 | rote procedures, and tedium of skills one needs can be pushed off on 40 | to the MOOC. At this point the teacher can get to the good stuff such 41 | as critical thinking and creativity—things that belong in all 42 | courses including calculus. 43 | 44 | Another obstacle we face is that of longevity. Technology changes at 45 | an essentially incomprehensible rate. How do you produce courseware 46 | that will last 5 years? Several solutions were discussed at the 47 | conference, here is what we've adopted: 48 | 49 | 1. Use formats that are "here to stay" 50 | 2. Produce content as efficiently as possible. 51 | 52 | I’ll address these in turn. To start, formats that are "here to stay" 53 | are those such as the Wiki markup, LaTeX, and basic XML. These are all 54 | markup languages, whose content if needed could be converted (and 55 | enhanced!) to a future format without trouble. 56 | 57 | Second, we try to be as efficient as possible when making video 58 | content. Jim Fowler has been a pioneer in this respect, developing the 59 | "AutoCutter" software that reduces time consuming video editing to a 60 | few key strokes. 61 | 62 | One thing is certain to me—MOOCs are the future. I am encouraged that 63 | with courses like MOOCulus OSU is at the forefront of this revolution. -------------------------------------------------------------------------------- /test/routes/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sessionFactory = require('supertest-session'); 4 | var app = require('../../app').app; 5 | var should = require('should'); 6 | var faker = require('faker'); 7 | var assert = require('assert'); 8 | var validator = require('validator'); 9 | 10 | var session = null; 11 | var otherSession = null; 12 | 13 | before(function () { 14 | session = sessionFactory(app); 15 | otherSession = sessionFactory(app); 16 | }); 17 | 18 | describe('the current user', function () { 19 | var user = {}; 20 | 21 | before(function(done){ 22 | session 23 | .get('/users/me') 24 | .set('Accept', 'application/json') 25 | .expect(200, function(err, res) { 26 | user = res.body; 27 | done(); 28 | }); 29 | }); 30 | 31 | it('should include an id', function () { 32 | user.should.have.property('_id'); 33 | }); 34 | 35 | it('is available at /users/:id', function (done) { 36 | session 37 | .get('/users/' + user['_id']) 38 | .expect(200, done); 39 | }); 40 | 41 | // These are really integration tests, so fuzz testing is 42 | // acceptable 43 | 44 | var updatedData = { 45 | email: faker.internet.email(), 46 | birthday: faker.date.past(), 47 | website: faker.internet.url(), 48 | biography: faker.lorem.text(), 49 | displayName: faker.name.firstName() 50 | }; 51 | 52 | it('can be updated at /users/:id', function (done) { 53 | session 54 | .post('/users/' + user['_id']) 55 | .send(updatedData) 56 | .expect(200, done); 57 | }); 58 | 59 | it('can not be updated at by another person', function (done) { 60 | otherSession 61 | .post('/users/' + user['_id']) 62 | .send(updatedData) 63 | .expect(403, done); 64 | }); 65 | 66 | it('can be retrieved a second time', function (done) { 67 | session 68 | .get('/users/me') 69 | .set('Accept', 'application/json') 70 | .expect(200, function(err, res) { 71 | user = res.body; 72 | done(); 73 | }); 74 | }); 75 | 76 | it('should include new email', function () { 77 | user.should.have.property('email'); 78 | validator.normalizeEmail(user.email).should.equal( validator.normalizeEmail(updatedData.email) ); 79 | }); 80 | 81 | it('should include new birthday', function () { 82 | user.should.have.property('birthday'); 83 | assert.deepEqual( validator.toDate(user.birthday), validator.toDate(updatedData.birthday) ); 84 | }); 85 | 86 | it('should include new website', function () { 87 | user.should.have.property('website'); 88 | user.website.should.equal( updatedData.website ); 89 | }); 90 | 91 | it('should include new biography', function () { 92 | user.should.have.property('biography'); 93 | user.biography.should.equal( updatedData.biography ); 94 | }); 95 | 96 | it('should include new display name', function () { 97 | user.should.have.property('displayName'); 98 | user.displayName.should.equal( updatedData.displayName ); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /views/layouts/footer/index.pug: -------------------------------------------------------------------------------- 1 | footer.bg-inverse.text-white 2 | .container 3 | .row 4 | if (!atColoradoState) 5 | .col-md-3.hidden-sm-down 6 | h5 7 | a(href="/mooculus/") Courses 8 | ul 9 | a(href="/mooculus/calculus1") Calculus One 10 | a(href="/mooculus/calculus2") Calculus Two 11 | a(href="/mooculus/calculus3") Calculus Three 12 | .col-md-3 13 | h5.hidden-sm-down 14 | a(href="/about/overview") About 15 | ul 16 | a(href="/about/faq") FAQ 17 | a(href="/about/team") Development Team 18 | a(href="/about/workshop") Workshop 19 | a(href='/about/contact') Contact Us 20 | i.fa.fa-envelope 21 | .col-md-3.hidden-sm-down 22 | h5 Social 23 | ul 24 | a(href='http://www.facebook.com/sharer.php?u=' + absoluteUrl) 25 | | Facebook 26 | i.fa.fa-facebook 27 | a(href='http://twitter.com/intent/tweet?status=' + absoluteUrl + ' @XimeraProject') 28 | | Twitter 29 | i.fa.fa-twitter 30 | a(href='https://plus.google.com/share?url=' + absoluteUrl) 31 | | Google Plus 32 | i.fa.fa-google-plus 33 | a(href='https://github.com/kisonecat/ximera') 34 | | GitHub 35 | i.fa.fa-github-alt 36 | 37 | .col-md-3.address.hidden-sm-down 38 | p 39 | | Built at The Ohio State UniversityOSU with support from NSF Grant DUE-1245433, the Shuttleworth Foundation, the Department of Mathematics, and the Affordable Learning ExchangeALX. 40 | .row 41 | .col-md-12.osu 42 | if (atColoradoState) 43 | img.hidden-sm-down(src=versionPath("/public/images/colorado-state/logo.png")) 44 | else 45 | img.hidden-sm-down(src=versionPath("/public/images/osu/osu-web-footer-wordmark-rev.png")) 46 | p 47 | | © 2013–#{new Date().getFullYear()}, The Ohio State University Ximera team 48 | p.hidden-xs-down 49 | | 100 Math Tower, 231 West 18th Avenue, Columbus OH, 43210–1174 50 | p.hidden-xs-down 51 | | Phone: (773) 809–5659 | 52 | a(href='/about/contact') Contact 53 | p 54 | | If you have trouble accessing this page and need to request an alternate format, contact ximera@math.osu.edu. 55 | -------------------------------------------------------------------------------- /blog/bigdata.markdown: -------------------------------------------------------------------------------- 1 | {{{ 2 | "title" : "Why Data?", 3 | "tags" : [ "calculus one" ], 4 | "category" : "calculus one", 5 | "date" : "4-16-2013", 6 | "author" : "Jim Fowler" 7 | }}} 8 | 9 | Data turned medicine from anecdotes into science; data has the same 10 | effect on education. But gathering data is much too expensive! If 11 | only there were some way to produce activity logs for each student, 12 | without the awkward overhead of moving students into [Bentham's 13 | Panopticon](http://en.wikipedia.org/wiki/Panopticon). 14 | 15 | But there is a way! Instead of an in-person course, use a massive 16 | open online course (a MOOC). Once the student experience is online, 17 | the student experience can be logged, so MOOCs provide tons of free 18 | data on exactly how students are using the educational resources we 19 | provide. **Online isn't the point: data is the point.** 20 | 21 | 22 | 23 | Big data pays off right away. Because the courses are massive 24 | (Calculus One had 35k enrollments), there's enough data to see trends. 25 | A wrong answer caused by a misconception in one percent of my students 26 | might go unnoticed when I only teach a couple hundred students, but 27 | when a few hundred MOOC students submit the same wrong answer, we take 28 | notice, and we can program our quizzes to address the misconception 29 | for future students. 30 | 31 | Big data also pays off in the long run. MOOCs are real-time (this is 32 | not the Postal Service) and interactive (this is not watching TV), yet 33 | much of that real-time interactivity arises from (or, at least, is 34 | stored in) a fixed form, namely posts in the forums, the lecture 35 | videos we made, the textbook we wrote, and the exercises (which are 36 | really just computer programs) we designed. A fixed form makes 37 | iterative development possible. 38 | 39 | How so? When I teach an in-person course for the second time, each 40 | lecture is a new reading from improved notes; when a MOOC runs a 41 | second time, we can re-use the pieces that worked and fix the parts 42 | that didn't. Data tells us precisely where improvement is needed. 43 | 44 | Unfortunately, I often hear that it'll be so much easier to run 45 | Calculus One for the second time, now that I have the videos recorded; 46 | and yes, yes, the second run will be easier in significant ways, but 47 | serious iterative development is seriously expensive enough that I 48 | doubt MOOCs will save money---what MOOCs will save is education. 49 | We'll know exactly which paragraphs students are turning to in the 50 | textbook when they're struggling with a particular homework problem; 51 | we'll know exactly when students pause the lecture; we'll know exactly 52 | which instructors communicate most effectively with students to 53 | improve outcomes. In ten years time, with that feedback mechanism in 54 | place, the most effective learning experiences will be found online. 55 | -------------------------------------------------------------------------------- /views/modals/guppymath.pug: -------------------------------------------------------------------------------- 1 | .modal.fade#guppymathModal(tabindex="-1",role="dialog",aria-labelledby="guppyModal") 2 | .modal-dialog(role="document") 3 | .modal-content 4 | .modal-header 5 | h5(class="modal-title",id="guppyModal") Mathematical Expression Editor 6 | button.close(type="button",data-dismiss="modal",aria-label="Close") 7 | span(aria-hidden="true") × 8 | .modal-body 9 | .btn-toolbar.mb-2(role="math palette",aria-label="Palette of mathematical symbols") 10 | .btn-group.mr-2(role="group",aria-label="Operations") 11 | button.btn.btn-secondary#guppy-plus(type="button") + 12 | button.btn.btn-secondary#guppy-minus(type="button") – 13 | button.btn.btn-secondary#guppy-times(type="button") × 14 | button.btn.btn-secondary#guppy-slash(type="button") ÷ 15 | button.btn.btn-secondary#guppy-exp(type="button") xⁿ 16 | .btn-group.mr-2(role="group",aria-label="Radiacls") 17 | button.btn.btn-secondary#guppy-sqrt(type="button") √ 18 | button.btn.btn-secondary#guppy-root(type="button") ⁿ√ 19 | .btn-group.mr-2(role="group",aria-label="Greek letters") 20 | button.btn.btn-secondary#guppy-pi(type="button") π 21 | button.btn.btn-secondary#guppy-theta(type="button") θ 22 | button.btn.btn-secondary#guppy-phi(type="button") φ 23 | button.btn.btn-secondary#guppy-rho(type="button") ρ 24 | .btn-group.mr-2(role="group",aria-label="Delimiters") 25 | button.btn.btn-secondary#guppy-paren(type="button") ( ) 26 | button.btn.btn-secondary#guppy-abs(type="button") | | 27 | .btn-toolbar.mb-2(role="math palette",aria-label="Palette of mathematical symbols") 28 | .btn-group.mr-2(role="group",aria-label="trig functions") 29 | button.btn.btn-secondary#guppy-sin(type="button") sin 30 | button.btn.btn-secondary#guppy-cos(type="button") cos 31 | button.btn.btn-secondary#guppy-tan(type="button") tan 32 | .btn-group.mr-2(role="group",aria-label="inverse trig functions") 33 | button.btn.btn-secondary#guppy-arcsin(type="button") arcsin 34 | button.btn.btn-secondary#guppy-arccos(type="button") arccos 35 | button.btn.btn-secondary#guppy-arctan(type="button") arctan 36 | .btn-group.mr-2(role="group",aria-label="logarithms and exponentials") 37 | button.btn.btn-secondary#guppy-etothe(type="button") eˣ 38 | button.btn.btn-secondary#guppy-ln(type="button") ln 39 | button.btn.btn-secondary#guppy-log(type="button") log 40 | div#guppy(style="width:100%;height:100pt;") 41 | div#guppy-error 42 | .modal-footer 43 | button.btn.btn-danger(type="button",data-dismiss="modal") 44 | |  Cancel 45 | button.btn.btn-primary#guppy-save-button(type="button") 46 | |  OK 47 | -------------------------------------------------------------------------------- /public/javascripts/math-palette.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | 3 | // https://github.com/daniel3735928559/guppy 4 | var Guppy = require('guppy-dev/src/guppy.js'); 5 | 6 | var Expression = require('math-expressions'); 7 | var guppyDiv = undefined; 8 | var callback = undefined; 9 | 10 | $(function() { 11 | if ($("#guppy").length > 0) { 12 | Guppy.init({"path":"/lib/guppy", 13 | "symbols":"/public/json/symbols.json" 14 | }); 15 | 16 | guppyDiv = new Guppy("guppy", { 17 | settings: { 18 | "buttons": [] 19 | }, 20 | "events":{ 21 | 'done': function(event) { 22 | var input = guppyDiv.engine.get_content('latex'); 23 | try { 24 | var output = Expression.fromLatex( input 25 | .replace(/\\dfrac/g,'\\frac') 26 | .replace(/\\cdot/g, ' ') 27 | ).toString(); 28 | $('#guppymathModal').modal('hide'); 29 | 30 | callback( null, output ); 31 | } catch (err) { 32 | $('#guppy-error').text(err); 33 | } 34 | } 35 | } 36 | }); 37 | 38 | function symbolizer( id, sym ) { 39 | $('#guppy-' + id).mousedown( function(event) { 40 | guppyDiv.engine.insert_symbol(sym); 41 | event.stopImmediatePropagation(); 42 | document.getElementById("guppy").focus(); 43 | } ); 44 | } 45 | 46 | function stringizer( id, str ) { 47 | $('#guppy-' + id).mousedown( function(event) { 48 | guppyDiv.engine.insert_string(str); 49 | event.stopImmediatePropagation(); 50 | document.getElementById("guppy").focus(); 51 | } ); 52 | } 53 | 54 | symbolizer( 'pi', 'pi' ); 55 | symbolizer( 'theta', 'theta' ); 56 | symbolizer( 'phi', 'phi' ); 57 | symbolizer( 'rho', 'rho' ); 58 | 59 | symbolizer( 'sqrt', 'sqrt' ); 60 | symbolizer( 'root', 'root' ); 61 | stringizer( 'times', '*' ); 62 | stringizer( 'plus', '+' ); 63 | stringizer( 'minus', '-' ); 64 | symbolizer( 'slash', 'slash' ); 65 | symbolizer( 'paren', 'paren' ); 66 | symbolizer( 'abs', 'abs' ); 67 | symbolizer( 'exp', 'exp' ); 68 | 69 | stringizer( 'sin', 'sin' ); 70 | stringizer( 'cos', 'cos' ); 71 | stringizer( 'tan', 'tan' ); 72 | stringizer( 'log', 'log' ); 73 | stringizer( 'ln', 'ln' ); 74 | stringizer( 'arcsin', 'arcsin' ); 75 | stringizer( 'arccos', 'arccos' ); 76 | stringizer( 'arctan', 'arctan' ); 77 | 78 | $('#guppy-etothe').mousedown( function(event) { 79 | guppyDiv.engine.insert_string('e'); 80 | guppyDiv.engine.insert_symbol('exp'); 81 | event.stopImmediatePropagation(); 82 | document.getElementById("guppy").focus(); 83 | } ); 84 | 85 | } 86 | 87 | $('#guppy-save-button').click( function() { 88 | guppyDiv.engine.done(); 89 | }); 90 | }); 91 | 92 | 93 | module.exports.launch = function( text, f ) { 94 | callback = f; 95 | 96 | try { 97 | if (text.match(/^ *$/)) { 98 | guppyDiv.engine.set_content(''); 99 | } else { 100 | var expression = Expression.fromText( text ); 101 | guppyDiv.engine.set_content(expression.toXML()); 102 | } 103 | } catch (err) { 104 | guppyDiv.engine.set_content(''); 105 | //$('#guppy-error').text(err); 106 | } 107 | 108 | $('#guppymathModal').modal('show'); 109 | guppyDiv.activate(); 110 | }; 111 | -------------------------------------------------------------------------------- /public/javascripts/pencil.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | 4 | $( function() { 5 | // Only activities get the pencil 6 | if ($('main.activity').length == 0) 7 | return; 8 | 9 | var pencilDiv; 10 | if ($("#pencil").length > 0) { 11 | pencilDiv = $("#pencil").first(); 12 | } else { 13 | pencilDiv = $(''); 14 | $('div.container-fluid').append( pencilDiv ); 15 | } 16 | 17 | var drawn = {}; 18 | var maxCounter = 0; 19 | 20 | pencilDiv.fetchData( function() { 21 | pencilDiv.persistentData( function(event) { 22 | // When erase, let's actually clear things 23 | if (Object.keys( event.data ).length == 0) { 24 | drawn = {}; 25 | pencilDiv.empty(); 26 | } 27 | 28 | Object.keys( event.data ).forEach( function(key) { 29 | if (parseInt(key) > maxCounter) 30 | maxCounter = parseInt(key); 31 | 32 | if (drawn[key] !== true) { 33 | var stroke = event.data[key]; 34 | drawn[key] = true; 35 | drawLine( stroke.x1, stroke.y1, stroke.x2, stroke.y2 ); 36 | } 37 | }); 38 | }); 39 | }); 40 | 41 | var parent = $('div.container-fluid'); 42 | 43 | pencilDiv.css( 'position', 'absolute' ); 44 | pencilDiv.css( 'top', '0' ); 45 | pencilDiv.css( 'left', '0' ); 46 | pencilDiv.css( 'width', '100%' ); 47 | pencilDiv.css( 'height', '100%' ); 48 | pencilDiv.css( 'overflow', 'visible' ); 49 | pencilDiv.css( 'z-index', '1029' ); 50 | pencilDiv.css( 'pointer-events', 'none' ); 51 | 52 | var drawing = false; 53 | var lastX = undefined; 54 | var lastY = undefined; 55 | 56 | function drawLine( ax, ay, bx, by ) { 57 | var newLine = document.createElementNS('http://www.w3.org/2000/svg','line'); 58 | newLine.setAttribute('id','line2'); 59 | newLine.setAttribute('x1',ax); 60 | newLine.setAttribute('y1',ay); 61 | newLine.setAttribute('x2',bx); 62 | newLine.setAttribute('y2',by); 63 | newLine.setAttribute('stroke','black'); 64 | $("#pencil").append(newLine); 65 | } 66 | 67 | parent.on( "touchstart", function(e) { 68 | if (e.touches[0].touchType !== "stylus") return; 69 | 70 | var touch = e.touches[0]; 71 | drawing = true; 72 | 73 | lastX = touch.pageX - parent.offset().left; 74 | lastY = touch.pageY - parent.offset().top; 75 | 76 | e.preventDefault(); 77 | }); 78 | 79 | parent.on( "touchend", function(e) { 80 | if (drawing == false) return; 81 | 82 | drawing = false; 83 | e.preventDefault(); 84 | }); 85 | 86 | parent.on( "touchmove", function(e) { 87 | if (drawing == false) return; 88 | 89 | var touch = e.touches[0]; 90 | 91 | var bx = touch.pageX - parent.offset().left; 92 | var by = touch.pageY - parent.offset().top; 93 | var ax = lastX; 94 | var ay = lastY; 95 | drawLine( ax, ay, bx, by ); 96 | 97 | maxCounter = maxCounter + 1; 98 | var uuid = maxCounter.toString(); 99 | drawn[uuid] = true; 100 | pencilDiv.persistentData( uuid, { 'x1': ax, 'y1': ay, 'x2': bx, 'y2': by } ); 101 | 102 | lastX = touch.pageX - parent.offset().left; 103 | lastY = touch.pageY - parent.offset().top; 104 | 105 | e.preventDefault(); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /routes/poetry.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | var prepositions = ["aboard", "about", "above", "across", "after", "against", "along", "alongside", "amid", "around", "as", "at", "before", "behind", "below", "beneath", "beside", "besides", "between", "beyond", "by", "despite", "down", "during", "except", "from", "into", "less", "like", "near", "nearer", "of", "off", "on", "onto", "opposite", "outside", "over", "past", "through", "throughout", "to", "toward", "towards", "under", "underneath", "unlike", "until", "up", "upon", "upside", "versus", "via", "with", "within", "without", "according to","adjacent to","ahead of","apart from","as for","as of","as per","as regards","aside from","back to","because of","close to","due to","except for","far from","inside of","instead of","left of","near to","next to","opposite of","opposite to","out from","out of","outside of","owing to","prior to","pursuant to","rather than","regardless of","right of","subsequent to","such as","thanks to","up to",'as far as','as opposed to','as soon as','as well as']; 4 | 5 | var adjectives = ["quick", "gray", "red", "orange", "green", "blue", "purple", "magenta", "maroon", "navy", "silver", "gold", "lime", "teal", "violet", "bright", "tall", "short", "dark", "cloudy", "summer", "winter", "spring", "fall", "autumn", "windy", "noisy", "loud", "quiet", "heavy", "light", "strong", "powerful", "wonderful", "amazing", "super", "sour", "bitter", "beautiful", "good", "bad", "great", "important", "useful", "free", "fine", "sad", "proud", "lonely", "frowning","comfortable", "happy", "clever", "interesting", "famous", "exciting", "funny", "kind", "polite", "fair", "careful", "rainy", "humid", "arid", "frigid", "foggy", "windy", "stormy", "breezy", "windless", "calm", "still"]; 6 | 7 | var nouns = ['mountain','tree','lake','water','river','ocean','sea','gulf','bay','town','city','village','house','bird','fish','cat','flower','butterfly','owl','book','hummingbird','eyes','building','home','raft','number','expression','equation','manifold','group','ring','set','prime','square','hexagon','cube','quadrilateral','rectangle','rhombus','parallelogram','trapezoid','tetrahedron','octahedron','circle','sphere','dodecahedron','icosahedron','disk','line','angle','chord','arc','approximation','function','formula','calculation','matrix','solution','theorem','fact','lemma','castle']; 8 | 9 | function poeticName(text) { 10 | var sha = crypto 11 | .createHash('sha256') 12 | .update(text) 13 | .digest('hex'); 14 | 15 | var buffer = new Buffer(sha, "hex"); 16 | 17 | var n = buffer.readUInt32LE(7); 18 | var preposition = prepositions[n % prepositions.length]; 19 | 20 | var n = buffer.readUInt32LE(11); 21 | var adjective = adjectives[n % adjectives.length]; 22 | 23 | var article = 'the'; 24 | if (buffer.readUInt8(6) % 2 == 0) { 25 | if (adjective.substr(0,1).match(/[aeiou]/)) 26 | article = 'an'; 27 | else 28 | article = 'a'; 29 | } 30 | 31 | var n = buffer.readUInt32LE(15); 32 | var noun = nouns[n % nouns.length]; 33 | 34 | var phrase = [preposition, article, adjective, noun].join(' '); 35 | function capitalizeFirstLetter(string) { 36 | return string.charAt(0).toUpperCase() + string.slice(1); 37 | } 38 | 39 | return capitalizeFirstLetter(phrase.toLowerCase()); 40 | } 41 | 42 | module.exports.poeticName = poeticName; 43 | -------------------------------------------------------------------------------- /public/javascripts/free-response.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('underscore'); 3 | var database = require('./database'); 4 | 5 | window.Markdown = require('pagedown-converter'); 6 | var Converter = Markdown.Converter; 7 | var Sanitizer = require('pagedown-sanitizer').getSanitizingConverter; 8 | var editor = require('pagedown-editor'); 9 | 10 | var TinCan = require('./tincan'); 11 | 12 | var createFreeResponse = function() { 13 | var element = $(this); 14 | 15 | var wmdName = "-" + $(this).attr('id'); 16 | 17 | var formHtml = '
' + 18 | '
' + 19 | '