Merge branch 'master' into dev/learning_outcome_refactor

Conflicts:
	public/javascripts/aligned_outcomes.js
	spec/apis/v1/collections_spec.rb
	vendor/plugins/moodle2cc/Gemfile

Change-Id: I2ea31263e4367dd456d12d2d53a297f3c25c9a5b
This commit is contained in:
Jacob Fugal 2012-08-22 10:42:21 -06:00
commit 8fa01db612
884 changed files with 20857 additions and 23108 deletions

1
.gitignore vendored
View File

@ -69,3 +69,4 @@ app/coffeescripts/plugins/
app/views/jst/plugins/
app/stylesheets/plugins/
spec/coffeescripts/plugins/
branch_tools.rb

View File

@ -1,11 +1,20 @@
{
"predef": [
"INST", "define", "require", "ENV"
],
"browser": true,
"onevar": false,
"devel": true,
"jquery": true,
"asi": true,
"plusplus": false,
"curly": true,
"forin": true,
"strict": false,
"undef": true
"asi": true, // This option suppresses warnings about missing semicolons.
"plusplus": false, // This option prohibits the use of unary increment and decrement operators
"curly": false, // This option requires you to always put curly braces around blocks in loops and conditionals
"forin": false, // This option requires all for in loops to filter object's items.
"undef": true, // This option prohibits the use of explicitly undeclared variables. This option is very useful for spotting leaking and mistyped variables.
"unused": true, // This option warns when you define and never use your variables. It is very useful for general code cleanup, especially when used in addition to undef.
"camelcase": true, //This option allows you to force all variable names to use either camelCase style or UPPER_CASE with underscores.
"eqeqeq":true, // This options prohibits the use of == and != in favor of === and !==. The former try to coerce values before comparing them which can lead to some unexpected results
"indent": 2, // This option enforces specific tab width for your code.
"latedef": true, // This option prohibits the use of a variable before it was defined
"immed": true, // This option prohibits the use of immediate function invocations without wrapping them in parentheses. Wrapping parentheses assists readers of your code in understanding that the expression is the result of a function, and not the function itself.
"boss": true // This option suppresses warnings about the use of assignments in cases where comparisons are expected. More often than not, code like if (a = 10) {} is a typo.
}

12
Gemfile
View File

@ -8,6 +8,7 @@ gem 'authlogic', '2.1.3'
# use custom gem until pull request at https://github.com/marcel/aws-s3/pull/41
# is merged into mainline. gem built from https://github.com/lukfugl/aws-s3
gem "aws-s3-instructure", "~> 0.6.2.1319643167", :require => 'aws/s3'
gem 'barby', '0.5.0'
gem 'bcrypt-ruby', '3.0.1'
gem 'builder', '2.1.2'
gem 'daemons', '1.1.0'
@ -46,9 +47,11 @@ end
gem 'rdiscount', '1.6.8'
gem 'require_relative', '1.0.1'
gem 'ritex', '1.0.1'
gem 'rotp', '1.4.1'
gem 'rqrcode', '0.4.2'
gem 'rscribd', '1.2.0'
gem 'ruby-net-ldap', '0.0.4', :require => 'net/ldap'
gem 'ruby-saml-mod', '0.1.15'
gem 'net-ldap', '0.3.1', :require => 'net/ldap'
gem 'ruby-saml-mod', '0.1.17'
gem 'rubycas-client', '2.2.1'
gem 'rubyzip', '0.9.4', :require => 'zip/zip'
gem 'sanitize', '2.0.3'
@ -110,7 +113,6 @@ group :i18n_tools do
gem 'ruby_parser', '2.0.6'
gem 'sexp_processor', '3.0.5'
gem 'ya2yaml', '0.30'
gem 'uglifier'
end
group :redis do
@ -122,6 +124,10 @@ group :embedly do
gem 'embedly', '1.5.5'
end
group :statsd do
gem 'statsd-ruby', '1.0.0', :require => 'statsd'
end
# Non-standard Canvas extension to Bundler behavior -- load the Gemfiles from
# plugins.
Dir[File.join(File.dirname(__FILE__),'vendor/plugins/*/Gemfile')].each do |g|

View File

@ -2,6 +2,7 @@ define [
'i18n!submission_details_dialog'
'jquery'
'jst/SubmissionDetailsDialog'
'compiled/gradebook2/Turnitin'
'jst/_submission_detail' # a partial needed by the SubmissionDetailsDialog template
'jst/_turnitinScore' # a partial needed by the submission_detail partial
'jquery.ajaxJSON'
@ -10,7 +11,7 @@ define [
'jqueryui/dialog'
'jquery.instructure_misc_plugins'
'vendor/jquery.scrollTo'
], (I18n, $, submissionDetailsDialog) ->
], (I18n, $, submissionDetailsDialog, {extractDataFor}) ->
class SubmissionDetailsDialog
constructor: (@assignment, @student, @options) ->
@ -52,20 +53,14 @@ define [
@submission.moreThanOneSubmission = @submission.submission_history.length > 1
@submission.loading = false
for submission in @submission.submission_history
submission["submission_type_is#{submission.submission_type}"] = true
submission.submissionWasLate = @assignment.due_at && new Date(@assignment.due_at) > new Date(submission.submitted_at)
for comment in submission.submission_comments || []
comment.url = "#{@options.context_url}/users/#{comment.author_id}"
urlPrefix = "#{location.protocol}//#{location.host}"
comment.image_url = "#{urlPrefix}/images/users/#{comment.author_id}?fallback=#{encodeURIComponent(urlPrefix+'/images/messages/avatar-50.png')}"
submission.turnitin = extractDataFor(submission, "submission_#{submission.id}", @options.context_url)
for attachment in submission.attachments || []
if turnitinDataForThisAttachment = submission.turnitin_data?["attachment_#{attachment.id}"]
if turnitinDataForThisAttachment["similarity_score"]
attachment.turnitin_data = turnitinDataForThisAttachment
attachment.turnitin_data.state = "#{turnitinDataForThisAttachment.state || 'no'}_score"
attachment.turnitin_data.score = "#{turnitinDataForThisAttachment.similarity_score}%"
attachment.turnitin_data.reportUrl = "#{@options.context_url}/assignments/#{@assignment.id}/submissions/#{@student.id}/turnitin/attachment_#{attachment.id}"
attachment.turnitin_data.tooltip = I18n.t('turnitin.tooltip.score', 'Turnitin Similarity Score - See detailed report')
attachment.turnitin = extractDataFor(submission, "attachment_#{attachment.id}", @options.context_url)
@dialog.html(submissionDetailsDialog(@submission))
@dialog.find('select').trigger('change')
@scrollCommentsToBottom()

View File

@ -1,26 +1,33 @@
# copied from: https://gist.github.com/1998897
define [
'use!vendor/backbone'
'underscore'
], (Backbone, _) ->
'jquery'
], (Backbone, _, $) ->
Backbone.syncWithoutMultipart = Backbone.sync
Backbone.syncWithMultipart = (method, model, options) ->
# Create a hidden iframe
iframeId = 'file_upload_iframe_' + (new Date()).getTime()
iframeId = _.uniqueId 'file_upload_iframe_'
$iframe = $("<iframe id='#{iframeId}' name='#{iframeId}' ></iframe>").hide()
dfd = new $.Deferred()
# Create a hidden form
httpMethod = {create: 'POST', update: 'PUT', delete: 'DELETE', read: 'GET'}[method]
toForm = (object, nested) ->
inputs = _.map object, (attr, key) ->
key = "#{nested}[#{key}]" if nested
if _.isElement(attr)
# leave a copy in the original form, since we're moving it
$orig = $(attr)
$orig.after($orig.clone(true))
attr
else if not _.isEmpty(attr) and (_.isArray(attr) or typeof attr is 'object')
else if !_.isEmpty(attr) and (_.isArray(attr) or typeof attr is 'object')
toForm(attr, key)
else if not "#{key}".match(/^_/) and attr? and typeof attr isnt 'object' and typeof attr isnt 'function'
else if !"#{key}".match(/^_/) and attr? and typeof attr isnt 'object' and typeof attr isnt 'function'
$el = $ "<input/>",
name: key
value: attr
@ -32,7 +39,9 @@ define [
<input type='hidden' name='authenticity_token' value='#{ENV.AUTHENTICITY_TOKEN}' />
</form>
""").hide()
$form.prepend(el for el in toForm(model) when el)
_.each toForm(model.attributes), (el) ->
$form.prepend(el) if el
$(document.body).prepend($iframe, $form)
@ -46,8 +55,10 @@ define [
if iframeBody.className is "error"
options.error?(response)
dfd.reject(response)
else
options.success?(response)
dfd.resolve(response)
$iframe.remove()
$form.remove()
@ -60,6 +71,7 @@ define [
$iframe[0].onload = callback
$form[0].submit()
dfd
Backbone.sync = (method, model, options) ->
if options?.multipart

View File

@ -0,0 +1,24 @@
define [
'use!vendor/backbone'
'compiled/str/splitAssetString'
], (Backbone, splitAssetString) ->
##
# In the spirit of convention over configuration, if the base API route of your collection
# follows canvas's default routing pattern of:
# /api/v1/<context_type>/<context_id>/<plural_form_of_resource_name>
# then you can just define a `resourceName` property on your model or collection and fall back on this default
# 'url' function. This will look for a @contextCode on your collection and fall back to
# ENV.context_asset_string.
#
# Feel free to still specicically set a url for your collection if you need to do any disambiguation
#
# So, for example say you are on /courses/1 and you do new DiscussionTopicsCollection().fetch()
# it will go to /api/v1/courses/1/discussion_topics (since ENV.context_asset_string will be already set)
Backbone.Collection::url = ->
assetString = @contextAssetString || ENV.context_asset_string
resourceName = @resourceName || @model::resourceName
throw new Error "Must define a `resourceName` property on collection or model prototype to use defaultUrl" unless resourceName
[contextType, contextId] = splitAssetString assetString
"/api/v1/#{contextType}/#{contextId}/#{resourceName}"

View File

@ -28,9 +28,8 @@
define [
'jquery'
'compiled/fn/preventDefault'
'compiled/jquery/fixDialogButtons'
], ($, preventDefault) ->
], ($) ->
updateTextToState = (newStateOfRegion) ->
return ->
@ -76,13 +75,19 @@ define [
$allElementsControllingRegion.each updateTextToState( if showRegion then 'Shown' else 'Hidden' )
$(document).delegate '.element_toggler[aria-controls]', 'click', preventDefault ->
$(document).on 'click change', '.element_toggler[aria-controls]', (event) ->
$this = $(this)
if $this.is('input[type="checkbox"]')
return if event.type is 'click'
force = $this.prop('checked')
event.preventDefault() if event.type is 'click'
# allow .links inside .user_content to be elementTogglers, but only for other elements inside of
# that .user_content area
$parent = $this.closest('.user_content')
$parent = $(document.body) unless $parent.length
$region = $parent.find("##{$this.attr('aria-controls')}")
toggleRegion($region) if $region.length
toggleRegion($region, force) if $region.length

View File

@ -0,0 +1,83 @@
##
# add the [data-tooltip] attribute and title="<tooltip contents>" to anything you want to give a tooltip:
#
# usage: (see Styleguide)
# <a data-tooltip title="pops up on top center">default</a>
# <a data-tooltip="top" title="same as default">top</a>
# <a data-tooltip="right" title="should be right center">right</a>
# <a data-tooltip="bottom" title="should be bottom center">bottom</a>
# <a data-tooltip="left" title="should be left center">left</a>
# <a data-tooltip='{"track":true}' title="this toolstip will stay connected to mouse as it moves around">
# tooltip that tracks mouse
# </a>
# <button data-tooltip title="any type of element can have a tooltip" class="btn">
# button with tooltip
# </button>
define [
'underscore'
'jquery'
'jqueryui/tooltip'
], (_, $) ->
CARET_SIZE = 5
# you can provide a 'using' option to jqueryUI position (which gets called by jqueryui Tooltip to
# position it on the screen), it will be passed the position cordinates and a feedback object which,
# among other things, tells you where it positioned it relative to the target. we use it to add some
# css classes that handle putting the pointer triangle (aka: caret) back to the trigger.
using = ( position, feedback ) ->
$( this )
.css( position )
.removeClass( "left right top bottom center middle vertical horizontal" )
.addClass([
# one of: "left", "right", "center"
feedback.horizontal
# one of "top", "bottom", "middle"
feedback.vertical
# if tooltip was positioned mostly above/below trigger then: "vertical"
# else since the tooltip was positioned more to the left or right: "horizontal"
feedback.important
].join(' '))
positions =
right:
my: "left center"
at: "right+#{CARET_SIZE} center"
collision: 'none none'
left:
my: "right center"
at: "left-#{CARET_SIZE} center"
collision: 'none none'
top:
my: "center bottom"
at: "center top-#{CARET_SIZE}"
collision: 'none none'
bottom:
my: "center top"
at: "center bottom+#{CARET_SIZE}"
collision: 'none none'
$('body').on 'mouseover', '[data-tooltip]', (event) ->
$this = $(this)
opts = $this.data('tooltip')
# allow specifying position by simply doing <a data-tooltip="left">
# and allow shorthand top|bottom|left|right positions like <a data-tooltip='{"position":"left"}'>
if opts of positions
opts = position: opts
opts ||= {}
opts.position ||= 'top'
if opts.position of positions
opts.position = positions[opts.position]
opts.position.using ||= using
$this
.removeAttr('data-tooltip')
.tooltip(opts)
.tooltip('open')

View File

@ -13,7 +13,6 @@ require [
'reminders'
'jquery.instructure_forms'
'instructure'
'fixed_warning'
'ajax_errors'
'page_views'
'compiled/license_help'
@ -23,11 +22,11 @@ require [
'compiled/behaviors/upvote-item'
'compiled/behaviors/repin-item'
'compiled/behaviors/follow'
'compiled/behaviors/tooltip'
# other stuff several bundles use
'media_comments'
'order'
'jqueryui/effects/core'
'jqueryui/effects/drop'
'jqueryui/progressbar'
'jqueryui/tabs'

View File

@ -24,4 +24,4 @@ require [
$("#course_details_tabs").bind 'tabsshow', (e,ui) ->
if ui.tab.hash == '#tab-users' and not window.app?.usersTab
loadUsersTab()
loadUsersTab()

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2011 Instructure, Inc.
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
@ -16,15 +16,20 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/../views_helper')
require [
'jquery',
'compiled/collections/UserCollection'
'compiled/views/RecentStudents/RecentStudentCollectionView',
], ($, UserCollection, RecentStudentCollectionView) ->
describe "/shared/_topic" do
it "should render" do
course_with_student
view_context
render :partial => "shared/topic"
response.should_not be_nil
end
end
$ ->
$('#reports-tabs').tabs().show()
recentStudentCollection = new UserCollection
recentStudentCollection.url = ENV.RECENT_STUDENTS_URL
recentStudentCollection.fetch()
window.app = studentsTab: {}
window.app.studentsTab = new RecentStudentCollectionView
el: '#tab-students .item_list'
collection: recentStudentCollection

View File

@ -0,0 +1,22 @@
require [
'jquery'
'compiled/models/DiscussionTopic'
'compiled/models/Announcement'
'compiled/views/DiscussionTopics/EditView'
'compiled/collections/AssignmentGroupCollection'
'compiled/str/splitAssetString'
], ($, DiscussionTopic, Announcement, EditView, AssignmentGroupCollection, splitAssetString) ->
is_announcement = ENV.DISCUSSION_TOPIC.ATTRIBUTES?.is_announcement
model = new (if is_announcement then Announcement else DiscussionTopic)(ENV.DISCUSSION_TOPIC.ATTRIBUTES)
model.urlRoot = ENV.DISCUSSION_TOPIC.URL_ROOT
view = new EditView(model: model)
[contextType, contextId] = splitAssetString ENV.context_asset_string
if contextType is 'courses' && !is_announcement
(view.assignmentGroupCollection = new AssignmentGroupCollection).contextAssetString = ENV.context_asset_string
$ -> view.render().$el.appendTo('#content')
view

View File

@ -0,0 +1,25 @@
require [
'compiled/collections/DiscussionTopicsCollection'
'compiled/collections/AnnouncementsCollection'
'compiled/collections/ExternalFeedCollection'
'compiled/views/DiscussionTopics/IndexView'
'compiled/views/ExternalFeeds/IndexView'
], (DiscussionTopicsCollection, AnnouncementsCollection, ExternalFeedCollection, IndexView, ExternalFeedsIndexView) ->
if ENV.is_showing_announcements
collection = new AnnouncementsCollection
externalFeeds = new ExternalFeedCollection
externalFeeds.fetch()
new ExternalFeedsIndexView
permissions: ENV.permissions
collection: externalFeeds
else
collection = new DiscussionTopicsCollection
collection.fetch()
new IndexView
collection: collection
permissions: ENV.permissions
atom_feed_url: ENV.atom_feed_url

View File

@ -1,2 +1,2 @@
require ['full_files', 'uploadify']
require ['full_files', 'use!uploadify']

View File

@ -1 +1 @@
require ['grade_summary', 'compiled/grade_calculator']
require ['grade_summary']

View File

@ -2,6 +2,7 @@ require [
'INST'
'ENV'
'compiled/notifications/NotificationPreferences'
'compiled/profile/confirmEmail'
], (INST, ENV, NotificationPreferences) ->
ENV.NOTIFICATION_PREFERENCES_OPTIONS.touch = INST.browser.touch
new NotificationPreferences(ENV.NOTIFICATION_PREFERENCES_OPTIONS)

View File

@ -0,0 +1,21 @@
require [
'jquery',
'jquery.fancyplaceholder'
], ($) ->
$(".field-with-fancyplaceholder input").fancyPlaceholder()
$("#login_form").find(":text:first").select()
$select_phone_form = $("#select_phone_form")
$new_phone_form = $("#new_phone_form")
$phone_select = $select_phone_form.find("select")
$phone_select.change (event) ->
if $phone_select.val() == '{{id}}'
$select_phone_form.hide()
$new_phone_form.show()
$("#back_to_choose_number_link").click (event) ->
$new_phone_form.hide()
$select_phone_form.show()
$phone_select.find("option:first").attr("selected", "selected")
event.preventDefault()

View File

@ -3,6 +3,7 @@ require [
'jquery'
'str/htmlEscape'
'compiled/tinymce'
'compiled/jquery/validate'
'tinymce.editor_box'
], ({View}, $, htmlEscape) ->
@ -12,6 +13,7 @@ require [
events:
'click [data-event]': 'handleDeclarativeClick'
'submit #edit_profile_form': 'validateForm'
handleDeclarativeClick: (event) ->
event.preventDefault()
@ -66,9 +68,11 @@ require [
$row.find('input:first').focus()
removeLinkRow: (event, $el) ->
console.log('hi')
$el.parents('tr').remove()
validateForm: (event) ->
unless $('#edit_profile_form').validate()
event.preventDefault()
new ProfileShow ENV.PROFILE

View File

@ -0,0 +1,72 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require [
'jquery'
'underscore'
'compiled/collections/EnrollmentCollection'
'compiled/collections/SectionCollection'
'compiled/views/courses/RosterView'
], ($, _, EnrollmentCollection, SectionCollection, RosterView) ->
rosterPage =
init: ->
@loadEnvironment()
@cacheElements()
@createCollections()
# Get the course ID and create the enrollments API url.
#
# @api public
# @return nothing
loadEnvironment: ->
@course = ENV.context_asset_string.split('_')[1]
@url = "/api/v1/courses/#{@course}/enrollments"
# Store DOM elements used.
#
# @api public
# @return nothing
cacheElements: ->
@$studentList = $('.student_roster .user_list')
@$teacherList = $('.teacher_roster .user_list')
# Create the view and collection objects needed for the page.
#
# @api public
# @return nothing
createCollections: ->
@sections = new SectionCollection(ENV.SECTIONS)
students = new EnrollmentCollection
teachers = new EnrollmentCollection
_.each [students, teachers], (c) =>
c.url = @url
c.sections = @sections
@studentView = new RosterView
el: @$studentList
collection: students
requestOptions: type: ['StudentEnrollment']
@teacherView = new RosterView
el: @$teacherList
collection: teachers
requestOptions: type: ['TeacherEnrollment', 'TaEnrollment']
# Start loading the page.
rosterPage.init()

View File

@ -1 +0,0 @@
require ['topic']

View File

@ -1,2 +0,0 @@
require ['topics']

View File

@ -122,7 +122,6 @@ define [
@colorizeContexts()
@scheduler = new Scheduler(".scheduler-wrapper", this)
$('html').addClass('calendar-loaded')
# Pre-load the appointment group list, for the badge
@dataSource.getAppointmentGroups false, (data) =>

View File

@ -128,12 +128,9 @@ define [
openHelpDialog: (e) =>
e.preventDefault()
$("#options_help_dialog").dialog('close').dialog(
autoOpen: false
$("#options_help_dialog").dialog
title: I18n.t('affect_reservations', "How will this affect reservations?")
width: 400
).dialog('open')
saveWithoutPublishingClick: (jsEvent) =>
jsEvent.preventDefault()

View File

@ -67,7 +67,15 @@ define [
visibleContexts = new VisibleContextManager(contexts, selectedContexts, $holder)
$holder.find('.settings').kyleMenu(buttonOpts: {icons: { primary:'ui-icon-cog-with-droparrow', secondary: null}})
$holder.find('.settings').kyleMenu
buttonOpts:
icons:
primary:'ui-icon-cog-with-droparrow'
secondary: null
popupOpts:
position:
offset: '-25px 10px'
within: '#right-side'
$holder.delegate '.context_list_context', 'click', (event) ->
# dont toggle if thy were clicking the .settings button

View File

@ -0,0 +1,19 @@
define [
'compiled/collections/DiscussionTopicsCollection'
'compiled/models/Announcement'
'compiled/str/splitAssetString'
], (DiscussionTopicsCollection, Announcement, splitAssetString) ->
class AnnouncementsCollection extends DiscussionTopicsCollection
# this sets it up so it uses /api/v1/<context_type>/<context_id>/discussion_topics as base url
resourceName: 'discussion_topics'
# this is wonky, and admittitedly not the right way to do this, but it is a workaround
# to append the query string '?only_announcements=true' to the index action (which tells
# discussionTopicsController#index to show announcements instead of discussion topics)
# but remove it for create/show/update/delete
_stringToAppendToURL: '?only_announcements=true'
url: -> super + @_stringToAppendToURL
model: Announcement

View File

@ -0,0 +1,8 @@
define [
'Backbone'
'compiled/models/AssignmentGroup'
], (Backbone, AssignmentGroup) ->
class AssignmentGroupCollection extends Backbone.Collection
model: AssignmentGroup

View File

@ -0,0 +1,9 @@
define [
'Backbone'
'compiled/models/DiscussionEntry'
], (Backbone, DiscussionEntry) ->
class DiscussionEntryCollection extends Backbone.Collection
model: DiscussionEntry

View File

@ -0,0 +1,8 @@
define [
'compiled/collections/PaginatedCollection'
'compiled/models/DiscussionTopic'
], (PaginatedCollection, DiscussionTopic) ->
class DiscussionTopicsCollection extends PaginatedCollection
model: DiscussionTopic

View File

@ -0,0 +1,70 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
define [
'underscore'
'compiled/collections/PaginatedCollection'
'compiled/models/Enrollment'
], (_, PaginatedCollection, Enrollment) ->
# A collection for managing responses from EnrollmentsApiController.
# Extends PaginatedCollection to allow for paging of returned results.
class EnrollmentCollection extends PaginatedCollection
model: Enrollment
# Format returned responses by flattening the enrollment/user objects
# returned and adding a section name if given sections.
#
# @param response {Object} - A parsed JSON object from the server.
#
# @api private
# @return a formatted JSON response
parse: (response) ->
_.map(response, @flattenEnrollment)
super
# Add the returned user elements to the parent enrollment object to
# make templating easier (e.g. remove all {{#with}} calls in Handlebars.
#
# @param enrollment {Object} - An enrollment object w/ a user sub-object.
#
# @api private
# @return a formatted enrollment JSON object
flattenEnrollment: (enrollment) =>
id = enrollment.user.id
delete enrollment.user.id
enrollment[key] = value for key, value of enrollment.user
enrollment.user.id = id
@storeSection(enrollment) if @sections?
enrollment
# If the collection has been assigned a SectionCollection as @sections,
# use the course_section_id to find the section name and add it as
# course_section_name to the enrollment.
#
# NOTE: This function side-effects the passed enrollment to add the
# given column. It doesn't return anything.
#
# @param enrollment {Object} - An enrollment object.
#
# @api private
# @return nothing
storeSection: (enrollment) ->
section = @sections.find((section) -> section.get('id') == enrollment.course_section_id)
enrollment.course_section_name = section.get('name')

View File

@ -1,11 +0,0 @@
define [
'Backbone'
'compiled/models/Entry'
], (Backbone, Entry) ->
##
# Collection for Entries
class EntryCollection extends Backbone.Collection
model: Entry

View File

@ -0,0 +1,9 @@
define [
'Backbone'
'compiled/models/ExternalFeed'
'compiled/str/splitAssetString'
], (Backbone, ExternalFeed, splitAssetString) ->
class ExternalFeedCollection extends Backbone.Collection
model: ExternalFeed

View File

@ -7,9 +7,7 @@ define [
fetchNextPage: (options={}) ->
throw new Error "can't fetch next page when @nextPageUrl is undefined" unless @nextPageUrl?
options = _.extend {}, options,
add: true
url: @nextPageUrl
options = _.extend({}, {add: true, url: @nextPageUrl}, options)
@fetchingNextPage = true
@trigger 'beforeFetchNextPage'
@ -48,7 +46,6 @@ define [
# useful for dispaying 'nothingToShow' messages
@atLeastOnePageFetched = true
parse: (response, xhr) ->
@_parsePageLinks(xhr)
super
super

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2011 Instructure, Inc.
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
@ -16,13 +16,13 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module DiscussionTopicsHelper
# the delayed posting and not seeing reply options on a
# discussion don't make sense in a group setting because
# all the users are "admins" of the group so they can
# see the topics anyway.
def show_course_only_options?(context)
context.is_a? Course
end
end
define [
'Backbone'
'compiled/models/Section'
], ({Collection}, Section) ->
# A collection for working with course sections returned from
# CoursesController#sections.
class SectionCollection extends Collection
model: Section

View File

@ -1,9 +1,26 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
define [
'Backbone'
'compiled/collections/PaginatedCollection'
'compiled/models/User'
], (Backbone, PaginatedCollection, User) ->
], (PaginatedCollection, User) ->
class UserCollection extends PaginatedCollection
model: User
model: User

View File

@ -536,7 +536,7 @@ define [
$(this).closest('li').data('id')
$preview.find('> li').last().addClass('last')
@$forwardForm.css('max-height', ($(window).height() - 300) + 'px')
.dialog('close').dialog
.dialog
position: 'center'
height: 'auto'
width: 510
@ -565,7 +565,7 @@ define [
return unless @conversations.active()?
@$addForm
.attr('action', @conversations.actionUrlFor($(e.currentTarget)))
.dialog('close').dialog
.dialog
width: 420
title: I18n.t('title.add_recipients', 'Add Recipients')
buttons: [

View File

@ -65,14 +65,14 @@ define [
$key = $(this).closest(".key")
key = $key.data('key')
$form = buildForm(key, $key)
$("#edit_dialog").empty().append($form).dialog('close').dialog('open')
$("#edit_dialog").empty().append($form).dialog('open')
)
$(".add_key").click((event) ->
event.preventDefault()
$form = buildForm()
$("#edit_dialog").empty().append($form).dialog('close').dialog('open')
$("#edit_dialog").empty().append($form).dialog('open')
)
$("#edit_dialog").html(developer_key_form({})).dialog('close').dialog({
$("#edit_dialog").html(developer_key_form({})).dialog({
autoOpen: false,
width: 350
}).on('click', '.cancel', () ->

View File

@ -1,3 +1,8 @@
#################
# DONT USE THIS, USE DiscussionTopic.coffee, the only place that should use this is
# the discusison show page. (it was made before we tried standardizing and re-using models)
#######################
define [
'Backbone'
'compiled/util/BackoffPoller'

View File

@ -1,8 +1,10 @@
define [
'i18n!discussions'
'jquery'
'Backbone'
'underscore'
'compiled/discussions/Topic'
'compiled/models/DiscussionTopic'
'compiled/discussions/EntriesView'
'compiled/discussions/EntryView'
'jst/discussions/_reply_form'
@ -11,7 +13,7 @@ define [
'compiled/util/wikiSidebarWithMultipleEditors'
'jquery.instructure_misc_helpers' #scrollSidebar
], (I18n, Backbone, _, Topic, EntriesView, EntryView, replyTemplate, Reply, assignmentRubricDialog) ->
], (I18n, $, Backbone, _, Topic, DiscussionTopic, EntriesView, EntryView, replyTemplate, Reply, assignmentRubricDialog) ->
##
# View that considers the enter ERB template, not just the JS
@ -48,6 +50,14 @@ define [
assignmentRubricDialog.initTriggers()
@disableNextUnread()
# this is weird but Topic.coffee was not set up to talk to the API for CRUD
$('.discussion_locked_toggler').click ->
locked = $(this).data('mark-locked')
topic = new DiscussionTopic(id: ENV.DISCUSSION.TOPIC.ID)
# get rid of the /view on /api/vl/courses/x/discusison_topics/x/view
topic.url = ENV.DISCUSSION.ROOT_URL.replace /\/view/m, ''
topic.save({locked: locked}).done -> window.location.reload()
@$el.toggleClass 'side_comment_discussion', !ENV.DISCUSSION.THREADED
##

View File

@ -5,5 +5,5 @@
define ->
preventDefault = (fn) ->
(event) ->
event.preventDefault()
event?.preventDefault()
fn.apply(this, arguments)

View File

@ -1,7 +1,6 @@
define [
'INST'
'jquery'
], (INST, $) ->
'underscore'
], (_) ->
class GradeCalculator
# each submission needs fields: score, points_possible, assignment_id, assignment_group_id
@ -14,9 +13,7 @@ define [
# if weighting_scheme is "percent", group weights are used, otherwise no weighting is applied
@calculate: (submissions, groups, weighting_scheme) ->
result = {}
# NOTE: purposely using $.map because it can handle array or object, old gradebook sends array
# new gradebook sends object, needs jquery >1.6's version of $.map, since it can handle both
result.group_sums = $.map groups, (group) =>
result.group_sums = _(groups).map (group) =>
group: group
current: @create_group_sum(group, submissions, true)
'final': @create_group_sum(group, submissions, false)
@ -24,57 +21,133 @@ define [
result['final'] = @calculate_total(result.group_sums, false, weighting_scheme)
result
@create_group_sum: (group, submissions, ignore_ungraded) ->
sum = { submissions: [], score: 0, possible: 0, submission_count: 0 }
for assignment in group.assignments
data = { score: 0, possible: 0, percent: 0, drop: false, submitted: false }
@create_group_sum: (group, submissions, ignoreUngraded) ->
arrayToObj = (arr, property) ->
obj = {}
for e in arr
obj[e[property]] = e
obj
submission = null
for s in submissions when s.assignment_id == assignment.id
submission = s
break
gradeableAssignments = _(group.assignments).filter (a) ->
not _.isEqual(a.submission_types, ['not_graded'])
assignments = arrayToObj gradeableAssignments, "id"
submission ?= { score: null }
submission.assignment_group_id = group.id
submission.points_possible ?= assignment.points_possible
data.submission = submission
sum.submissions.push data
unless ignore_ungraded and (!submission.score? || submission.score == '')
data.score = @parse submission.score
data.possible = @parse assignment.points_possible
data.percent = @parse(data.score / data.possible)
data.submitted = (submission.score? and submission.score != '')
sum.submission_count += 1 if data.submitted
# filter out submissions from other assignment groups
submissions = _(submissions).filter (s) -> assignments[s.assignment_id]?
# sort the submissions by assigned score
sum.submissions.sort (a,b) -> a.percent - b.percent
rules = $.extend({ drop_lowest: 0, drop_highest: 0, never_drop: [] }, group.rules)
# fill in any missing submissions
unless ignoreUngraded
submissionAssignmentIds = _(submissions).map (s) ->
s.assignment_id.toString()
missingSubmissions = _.difference(_.keys(assignments),
submissionAssignmentIds)
dummySubmissions = _(missingSubmissions).map (assignmentId) ->
s = assignment_id: assignmentId, score: null
submissions.push dummySubmissions...
dropped = 0
submissionsByAssignment = arrayToObj submissions, "assignment_id"
# drop the lowest and highest assignments
for lowOrHigh in ['low', 'high']
for data in sum.submissions
if !data.drop and rules["drop_#{lowOrHigh}est"] > 0 and $.inArray(data.assignment_id, rules.never_drop) == -1 and data.possible > 0 and data.submitted
data.drop = true
# TODO: do I want to do this, it actually modifies the passed in submission object but it
# it seems like the best way to tell it it should be dropped.
data.submission?.drop = true
rules["drop_#{lowOrHigh}est"] -= 1
dropped += 1
submissionData = submissions.map (s) =>
sub =
total: @parse assignments[s.assignment_id].points_possible
score: @parse s.score
submitted: s.score? and s.score != ''
submission: s
relevantSubmissionData = if ignoreUngraded
_(submissionData).filter (s) -> s.submitted
else
submissionData
# if everything was dropped, un-drop the highest single submission
if dropped > 0 and dropped == sum.submission_count
sum.submissions[sum.submissions.length - 1].drop = false
# see TODO above
sum.submissions[sum.submissions.length - 1].submission?.drop = false
dropped -= 1
kept = @dropAssignments relevantSubmissionData, group.rules
sum.submission_count -= dropped
[score, possible] = _.reduce kept
, ([scoreSum, totalSum], {score,total}) =>
[scoreSum + @parse(score), totalSum + total]
, [0, 0]
sum.score += s.score for s in sum.submissions when !s.drop
sum.possible += s.possible for s in sum.submissions when !s.drop
sum
ret =
possible: possible
score: score
# TODO: figure out what submission_count is actually counting
submission_count: (_(submissionData).filter (s) -> s.submitted).length
submissions: _(submissionData).map (s) =>
submissionRet =
drop: s.drop
percent: @parse(s.score / s.total)
possible: s.total
score: @parse(s.score)
submission: s.submission
submitted: s.submitted
# I'm not going to pretend that this code is understandable.
#
# The naive approach to dropping the lowest grades (calculate the
# grades for each combination of assignments and choose the set which
# results in the best overall score) is obviously too slow.
#
# This approach is based on the algorithm described in "Dropping Lowest
# Grades" by Daniel Kane and Jonathan Kane. Please see that paper for
# a full explanation of the math.
# (http://web.mit.edu/dankane/www/droplowest.pdf)
@dropAssignments: (submissions, rules) ->
rules or= {}
dropLowest = rules.drop_lowest || 0
dropHighest = rules.drop_highest || 0
neverDropIds = rules.never_drop || []
return submissions unless dropLowest or dropHighest
if neverDropIds.length > 0
cantDrop = _(submissions).filter (s) ->
_.indexOf(neverDropIds, parseInt s.submission.assignment_id) >= 0
submissions = _.difference submissions, cantDrop
else
cantDrop = []
return cantDrop if submissions.length == 0
dropLowest = submissions.length - 1 if dropLowest >= submissions.length
dropHighest = 0 if dropLowest + dropHighest >= submissions.length
totals = (s.total for s in submissions)
maxTotal = Math.max(totals...)
grades = (s.score / s.total for s in submissions).sort (a,b) -> a - b
qLow = grades[0]
qHigh = grades[grades.length - 1]
qMid = (qLow + qHigh) / 2
keepHighest = submissions.length - dropLowest
bigF = (q, submissions) ->
ratedScores = _(submissions).map (s) ->
ratedScore = s.score - q * s.total
[ratedScore, s]
rankedScores = ratedScores.sort (a, b) -> b[0] - a[0]
keptScores = rankedScores[0...keepHighest]
qKept = _.reduce keptScores
, (sum, [ratedScore, s]) ->
sum + ratedScore
, 0
keptSubmissions = (s for [ratedScore, s] in keptScores)
[qKept, keptSubmissions]
[x, kept] = bigF(qMid, submissions)
threshold = 1 /(2 * keepHighest * Math.pow(maxTotal, 2))
until qHigh - qLow < threshold
if x < 0
qHigh = qMid
else
qLow = qMid
qMid = (qLow + qHigh) / 2
[x, kept] = bigF(qMid, submissions)
if dropHighest
kept.splice 0, dropHighest
kept.push cantDrop...
dropped = _.difference(submissions, kept)
# SIDE EFFECT: The gradebooks require this behavior
s.drop = true for s in dropped
kept
@calculate_total: (group_sums, ignore_ungraded, weighting_scheme) ->
data_idx = if ignore_ungraded then 'current' else 'final'
@ -113,10 +186,7 @@ define [
@letter_grade: (grading_scheme, score) ->
score = 0 if score < 0
letters = $.grep grading_scheme, (row, i) ->
letters = _(grading_scheme).filter (row, i) ->
score >= row[1] * 100 || i == (grading_scheme.length - 1)
letter = letters[0]
letter[0]
window.INST.GradeCalculator = GradeCalculator

View File

@ -11,4 +11,5 @@ define ['i18n!gradebook2'], (I18n) ->
submission_tooltip_online_text_entry: I18n.t('titles.text', "Text Entry Submission"),
submission_tooltip_pending_review: I18n.t('titles.quiz_review', "This quiz needs review"),
submission_tooltip_media_comment: I18n.t('titles.media', "Media Comment Submission"),
submission_tooltip_quiz: I18n.t('title.quiz', "Quiz Submission")
submission_tooltip_quiz: I18n.t('title.quiz', "Quiz Submission")
submission_tooltip_turnitin: I18n.t('title.turnitin', 'Has Turnitin score')

View File

@ -68,7 +68,7 @@ define [
@spinner = new Spinner()
$(@spinner.spin().el).css(
opacity: 0.5
top: '50%'
top: '55px'
left: '50%'
).addClass('use-css-transitions-for-show-hide').appendTo('#main')
@ -210,14 +210,15 @@ define [
@multiGrid.invalidate()
getSubmissionsChunks: =>
allStudents = (s for k, s of @students)
loop
students = @rows[@chunk_start...(@chunk_start+@options.chunk_size)]
students = allStudents[@chunk_start...(@chunk_start+@options.chunk_size)]
unless students.length
@allSubmissionsLoaded = true
break
params =
student_ids: (student.id for student in students)
response_fields: ['user_id', 'url', 'score', 'grade', 'submission_type', 'submitted_at', 'assignment_id', 'grade_matches_current_submission']
response_fields: ['id', 'user_id', 'url', 'score', 'grade', 'submission_type', 'submitted_at', 'assignment_id', 'grade_matches_current_submission', 'attachments']
$.ajaxJSON(@options.submissions_url, "GET", params, @gotSubmissionsChunk)
@chunk_start += @options.chunk_size
@ -240,13 +241,27 @@ define [
# It is different from gotSubmissionsChunk in that gotSubmissionsChunk expects an array of students
# where each student has an array of submissions. This one just expects an array of submissions,
# they are not grouped by student.
updateSubmissionsFromExternal: (submissions) =>
updateSubmissionsFromExternal: (submissions, submissionCell) =>
activeCell = @gradeGrid.getActiveCell()
editing = $(@gradeGrid.getActiveCellNode()).hasClass('editable')
columns = @gradeGrid.getColumns()
for submission in submissions
student = @students[submission.user_id]
idToMatch = "assignment_#{submission.assignment_id}"
cell = index for column, index in columns when column.id is idToMatch
thisCellIsActive = activeCell? and
editing and
activeCell.row is student.row and
activeCell.cell is cell
@updateSubmission(submission)
@multiGrid.invalidateRow(student.row)
@calculateStudentGrade(student)
@multiGrid.render()
@gradeGrid.updateCell student.row, cell unless thisCellIsActive
@updateRowTotals student.row
updateRowTotals: (rowIndex) ->
columns = @gradeGrid.getColumns()
for column, columnIndex in columns
@gradeGrid.updateCell rowIndex, columnIndex if column.type isnt 'assignment'
cellFormatter: (row, col, submission) =>
if !@rows[row].loaded
@ -287,12 +302,13 @@ define [
if student.loaded
finalOrCurrent = if @include_ungraded_assignments then 'final' else 'current'
submissionsAsArray = (value for key, value of student when key.match /^assignment_(?!group)/)
result = INST.GradeCalculator.calculate(submissionsAsArray, @assignmentGroups, @options.group_weighting_scheme)
result = GradeCalculator.calculate(submissionsAsArray, @assignmentGroups, @options.group_weighting_scheme)
for group in result.group_sums
student["assignment_group_#{group.group.id}"] = group[finalOrCurrent]
for submissionData in group[finalOrCurrent].submissions
submissionData.submission.drop = submissionData.drop
student["total_grade"] = result[finalOrCurrent]
highlightColumn: (columnIndexOrEvent) =>
if isNaN(columnIndexOrEvent)
# then assume that columnIndexOrEvent is an event, so figure out which column
@ -414,7 +430,6 @@ define [
@gradeGrid.getEditorLock().commitCurrentEdit()
onGridInit: () ->
@fixColumnReordering()
tooltipTexts = {}
$(@spinner.el).remove()
$('#gradebook_wrapper').show()
@ -459,7 +474,7 @@ define [
id: id
checked: @sectionToShow is id
$sectionToShowMenu = $(sectionToShowMenuTemplate(sections: sections, scrolling: sections.length > 15))
$sectionToShowMenu = $(sectionToShowMenuTemplate(sections: sections))
(updateSectionBeingShownText = =>
$('#section_being_shown').html(if @sectionToShow then @sections[@sectionToShow].name else allSectionsText)
)()
@ -480,8 +495,8 @@ define [
@buildRows()
# don't show the "show attendance" link in the dropdown if there's no attendance assignments
unless (_.detect @gradeGrid.getColumns(), (col) -> col.object?.submission_types == "attendance")
$settingsMenu.find('#show_attendance').hide()
unless (_.detect @assignments, (a) -> (''+a.submission_types) == "attendance")
$settingsMenu.find('#show_attendance').closest('li').hide()
@$columnArrangementTogglers = $('#gradebook-toolbar [data-arrange-columns-by]').bind 'click', (event) =>
event.preventDefault()
@ -603,7 +618,7 @@ define [
@aggregateColumns = for id, group of @assignmentGroups
html = "#{group.name}"
if group.group_weight?
percentage = I18n.toPercentage(group.group_weight, precision: 0)
percentage = I18n.toPercentage(group.group_weight, precision: 2)
html += """
<div class='assignment-points-possible'>
#{I18n.t 'percent_of_grade', "%{percentage} of grade", percentage: percentage}

View File

@ -2,8 +2,9 @@ define [
'compiled/gradebook2/GRADEBOOK_TRANSLATIONS'
'jquery'
'underscore'
'compiled/gradebook2/Turnitin'
'jquery.ajaxJSON'
], (GRADEBOOK_TRANSLATIONS, $, _) ->
], (GRADEBOOK_TRANSLATIONS, $, _, {extractData}) ->
class SubmissionCell
@ -54,18 +55,22 @@ define [
cellWrapper: (innerContents, options = {}) ->
opts = $.extend({}, {
innerContents: '',
classes: '',
editable: true
}, options)
opts.submission ||= @opts.item[@opts.column.field]
opts.assignment ||= @opts.column.object
specialClasses = SubmissionCell.classesBasedOnSubmission(opts.submission, opts.assignment)
tooltipText = $.map(specialClasses, (c)-> GRADEBOOK_TRANSLATIONS["submission_tooltip_#{c}"]).join ', '
opts.classes += ' no_grade_yet ' unless opts.submission.grade
innerContents ?= if opts.submission?.submission_type then '<span class="submission_type_icon" />' else '-'
if turnitin = extractData(opts.submission)
specialClasses.push('turnitin')
innerContents += "<span class='gradebook-cell-turnitin #{turnitin.state}-score' />"
tooltipText = $.map(specialClasses, (c)-> GRADEBOOK_TRANSLATIONS["submission_tooltip_#{c}"]).join ', '
"""
#{ if tooltipText then '<div class="gradebook-tooltip">'+ tooltipText + '</div>' else ''}
<div class="gradebook-cell #{ if opts.editable then 'gradebook-cell-editable focus' else ''} #{opts.classes} #{specialClasses.join(' ')}">
@ -79,7 +84,7 @@ define [
classes.push('resubmitted') if submission.grade_matches_current_submission == false
if assignment.due_at && submission.submitted_at
classes.push('late') if submission.submission_type isnt 'online_quiz' && (submission.submitted_at.timestamp > assignment.due_at.timestamp)
classes.push('late') if submission.submission_type is 'online_quiz' && ((submission.submitted_at.timestamp - assignment.due_at.timestamp) > 30)
classes.push('late') if submission.submission_type is 'online_quiz' && ((submission.submitted_at.timestamp - assignment.due_at.timestamp) > 60)
classes.push('dropped') if submission.drop
classes.push('ungraded') if ''+assignment.submission_types is "not_graded"
classes.push('muted') if assignment.muted

View File

@ -0,0 +1,37 @@
define [
'i18n!turnitin'
'underscore'
'compiled/util/invert'
], (I18n, {max}, invert) ->
Turnitin =
extractData: (submission) ->
return unless submission?.turnitin_data
data = items: []
if submission.attachments and submission.submission_type is 'online_upload'
for attachment in submission.attachments
attachment = attachment.attachment ? attachment
if turnitin = submission.turnitin_data?['attachment_' + attachment.id]
data.items.push turnitin
else if submission.submission_type is "online_text_entry"
if turnitin = submission.turnitin_data?['submission_' + submission.id]
data.items.push turnitin
return unless data.items.length
stateList = ['no', 'none', 'acceptable', 'warning', 'problem', 'failure']
stateMap = invert(stateList, parseInt)
states = (stateMap[item.state or 'no'] for item in data.items)
data.state = stateList[max(states)]
data
extractDataFor: (submission, key, urlPrefix) ->
data = submission.turnitin_data
return {} unless data and data[key] and data[key].similarity_score?
data = data[key]
data.state = "#{data.state || 'no'}_score"
data.score = "#{data.similarity_score}%"
data.reportUrl = "#{urlPrefix}/assignments/#{submission.assignment_id}/submissions/#{submission.user_id}/turnitin/#{key}"
data.tooltip = I18n.t('tooltip.score', 'Turnitin Similarity Score - See detailed report')
data

View File

@ -32,10 +32,21 @@ define [
friendlyDatetime : (datetime, {hash: {pubdate}}) ->
return unless datetime?
# if datetime is already a date convert it back into an ISO string to parseFromISO,
# TODO: be smarter about this
datetime = $.dateToISO8601UTC(datetime) if _.isDate datetime
parsed = $.parseFromISO(datetime)
new Handlebars.SafeString "<time title='#{parsed.datetime_formatted}' datetime='#{parsed.datetime.toISOString()}' #{'pubdate' if pubdate}>#{$.friendlyDatetime(parsed.datetime)}</time>"
# expects: a Date object
formattedDate : (datetime, format, {hash: {pubdate}}) ->
return unless datetime?
new Handlebars.SafeString "<time title='#{datetime}' datetime='#{datetime.toISOString()}' #{'pubdate' if pubdate}>#{datetime.toString(format)}</time>"
datetimeFormatted : (isoString) ->
return '' unless isoString
isoString = $.parseFromISO(isoString) unless isoString.datetime
isoString.datetime_formatted
@ -98,19 +109,32 @@ define [
previousArg = arg
fn(this)
# runs block if all arguments are true-ish
# runs block if *ALL* arguments are truthy
# usage:
# {{#ifAll arg1 arg2 arg3 arg}}
# everything was true-ish
# everything was truthy
# {{else}}
# something was false-y
# {{/ifEqual}}
# something was falsey
# {{/ifAll}}
ifAll: ->
[args..., {fn, inverse}] = arguments
for arg in args
return inverse(this) unless arg
fn(this)
# runs block if *ANY* arguments are truthy
# usage:
# {{#ifAny arg1 arg2 arg3 arg}}
# something was truthy
# {{else}}
# all were falsy
# {{/ifAny}}
ifAny: ->
[args..., {fn, inverse}] = arguments
for arg in args
return fn(this) if arg
inverse(this)
eachWithIndex: (context, options) ->
fn = options.fn
inverse = options.inverse
@ -168,6 +192,46 @@ define [
dateSelect: (name, options) ->
new Handlebars.SafeString dateSelect(name, options.hash).html()
##
# usage:
# if 'this' is {human: true}
# and you do: {{checkbox "human"}}
# you'll get: <input type="checkbox"
# value="1"
# id="human"
# checked="true"
# name="human" >
# you can pass custom attributes and use nested properties:
# if 'this' is {likes: {tacos: true}}
# and you do: {{checkbox "likes.tacos" class="foo bar"}}
# you'll get: <input type="checkbox"
# value="1"
# id="likes_tacos"
# checked="true"
# name="likes[tacos]"
# class="foo bar" >
checkbox : (propertyName, {hash}) ->
splitPropertyName = propertyName.split(/\./)
snakeCase = splitPropertyName.join('_')
bracketNotation = splitPropertyName[0] + _.chain(splitPropertyName)
.rest()
.map((prop) -> "[#{prop}]")
.value()
.join('')
inputProps = _.extend
type: 'checkbox'
value: 1
id: snakeCase
name: bracketNotation
, hash
unless inputProps.checked
value = _.reduce splitPropertyName, ((memo, key) -> memo[key]), this
inputProps.checked = true if value
attributes = _.map inputProps, (val, key) -> "#{htmlEscape key}=\"#{htmlEscape val}\""
new Handlebars.SafeString "<input #{attributes.join ' '}>"
}
return Handlebars

View File

@ -1,9 +1,9 @@
define [
'jquery'
'jquery.ui.menu.inputmenu'
'vendor/jquery.ui.popup-1.9'
'vendor/jquery.ui.button-1.9'
], ($, _inputmenu, _popup, _button) ->
'jqueryui/button'
'jqueryui/popup'
], ($) ->
class KyleMenu
constructor: (trigger, options) ->

View File

@ -29,12 +29,10 @@ define [
$node.appendTo($holder).
css('z-index', 1).
show('drop', direction: "up", 'fast').
slideDown('fast', -> $(this).css('z-index', 2)).
show('drop', direction: "up", 'fast', -> $(this).css('z-index', 2)).
delay(timeout || 7000).
animate({'z-index': 1}, 0).
fadeOut('slow', -> $(this).slideUp('fast', -> $(this).remove()))
#hide('drop', { direction: "up" }, 'fast', -> $(this).remove())
# Pops up a small notification box at the top of the screen.
$.flashMessage = (content, timeout = 3000) ->

View File

@ -0,0 +1,16 @@
define ['jquery'], ($) ->
# like $.when, except it transforms rejects into resolves. useful when you
# don't care if some items succeed or not, but you want to wait until
# everything completes before you resolve (or reject) ... the default $.when
# behavior is to reject as soon as the first dependency rejects.
$.whenAll = (dfds...) ->
dfds = for d in dfds
do ->
dfd = $.Deferred()
$.when(d).always (args...) ->
dfd.resolve args...
dfd.promise()
$.when dfds...
$

View File

@ -18,6 +18,6 @@ define ['jquery', 'jqueryui/dialog' ], ($) ->
text: $button.text()
"data-text-while-loading": $button.data("textWhileLoading")
click: -> $button.click()
class: -> $button.attr('class')
class: $button.attr('class')
}
$dialog.dialog "option", "buttons", buttons

View File

@ -0,0 +1,42 @@
define [
'jquery'
'underscore'
], ($, _) ->
rselectTextarea = /^(?:select|textarea)/i
rcheckboxOrRadio = /checkbox|radio/i
rCRLF = /\r?\n/g
rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week|checkbox|radio|file)$/i
isInput = (el) ->
el.name && !el.disabled && rselectTextarea.test(el.nodeName) or rinput.test(el.type)
getValue = (el) ->
resultFor = (val) ->
name: el.name
el: el
value: if _.isString(val) then val.replace( rCRLF, "\r\n" ) else val
$input = $(el)
val = if rcheckboxOrRadio.test(el.type)
el.checked
else if el.type == 'file'
el if $input.val()
else if $input.hasClass 'datetime_field_enabled'
$input.data('date')
else if $input.data('rich_text')
$input.editorBox('get_code', false)
else
$input.val()
if _.isArray val
_.map val, resultFor
else
resultFor val
$.fn.serializeForm = ->
_.chain(this[0].elements)
.toArray()
.filter(isInput)
.map(getValue)
.value()

View File

@ -0,0 +1,41 @@
##
# Validates a form, returns true or false, stores errors on element data.
#
# Markup supported:
#
# - Required
# <input type="text" name="whatev" required>
#
# ex:
# if $form.validates()
# doStuff()
# else
# errors = $form.data 'errors'
define [
'jquery'
'underscore'
'i18n!validate'
], ($, _, I18n) ->
$.fn.validate = ->
errors = {}
this.find('[required]').each ->
$input = $ this
name = $input.attr 'name'
value = $input.val()
if value is ''
(errors[name] ?= []).push
name: name
type: 'required'
message: I18n.t 'is_required', 'This field is required'
hasErrors = _.size(errors) > 0
if hasErrors
this.data 'errors', errors
false
else
this.data 'errors', undefined
true

View File

@ -1,6 +1,15 @@
define ['compiled/models/Discussion'], (Discussion) ->
define [
'compiled/models/DiscussionTopic'
'underscore'
], (DiscussionTopic, _) ->
class Announcement extends Discussion
class Announcement extends DiscussionTopic
# this is wonky, and admittitedly not the right way to do this, but it is a workaround
# to append the query string '?only_announcements=true' to the index action (which tells
# discussionTopicsController#index to show announcements instead of discussion topics)
# but remove it for create/show/update/delete
urlRoot: -> _.result(@collection, 'url').replace(@collection._stringToAppendToURL, '')
defaults:
is_announcement: true

View File

@ -0,0 +1,6 @@
define [
'Backbone'
], (Backbone) ->
class AssignmentGroup extends Backbone.Model
resourceName: 'assignment_groups'

View File

@ -1,23 +0,0 @@
define ['Backbone'], ({Model}) ->
class Discussion extends Model
url: ->
[z, contextType, contextId] = @contextCode.match /^(group|course)_(\d+)$/
"/api/v1/#{contextType}s/#{contextId}/discussion_topics"
defaults:
title: 'No title'
message: 'No message'
discussion_type: 'side_comment'
delayed_post_at: null
podcast_enabled: false
podcast_has_student_posts: false
require_initial_post: false
assignment: null
###
due_at: null
points_possible: null
###
is_announcement: false

View File

@ -6,12 +6,12 @@ define [
UNKOWN_AUTHOR =
avatar_image_url: null
display_name: 'Unknown Author'
display_name: I18n.t 'unknown_author', 'Unknown Author'
id: null
##
# Model representing an entry in discussion topic
class Entry extends Backbone.Model
class DiscussionEntry extends Backbone.Model
author: ->
@findParticipant @get('user_id')

View File

@ -0,0 +1,65 @@
define [
'Backbone'
'jquery'
'underscore'
'compiled/collections/ParticipantCollection'
'compiled/collections/DiscussionEntriesCollection'
], (Backbone, $, _, ParticipantCollection, DiscussionEntriesCollection) ->
class DiscussionTopic extends Backbone.Model
resourceName: 'discussion_topics'
defaults:
discussion_type: 'side_comment'
delayed_post_at: null
podcast_enabled: false
podcast_has_student_posts: false
require_initial_post: false
assignment: null
is_announcement: false
dateAttributes: [
'last_reply_at'
'posted_at'
'delayed_post_at'
]
initialize: ->
@participants = new ParticipantCollection
@entries = new DiscussionEntriesCollection
@entries.url = => "#{_.result this, 'url'}/entries"
@entries.participants = @participants
##
# this is for getting the topic 'full view' from the api
# see: http://<canvas>/doc/api/discussion_topics.html#method.discussion_topics_api.view
fetchEntries: ->
baseUrl = _.result this, 'url'
$.get "#{baseUrl}/view", ({unread_entries, participants, view: entries}) =>
@unreadEntries = unread_entries
@participants.reset participants
# TODO: handle nested replies and 'new_entries' here
@entries.reset(entries)
summary: ->
$('<div/>').html(@get('message')).text() || ''
# TODO: this would belong in Backbone.model, but I dont know of others are going to need it much
# or want to commit to this api so I am just putting it here for now
updateOneAttribute: (key, value, options = {}) ->
data = {}
data[key] = value
options = _.defaults options,
data: JSON.stringify(data)
contentType: 'application/json'
@save {}, options
positionAfter: (otherId) ->
@updateOneAttribute 'position_after', otherId
collection = @collection
otherIndex = collection.indexOf collection.get(otherId)
collection.remove this, silent: true
collection.models.splice (otherIndex), 0, this
collection.reset collection.models

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2011 Instructure, Inc.
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
@ -16,5 +16,6 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module DiscussionEntriesHelper
end
define ['Backbone'], ({Model}) ->
class Enrollment extends Model

View File

@ -0,0 +1,6 @@
define [
'Backbone'
], (Backbone) ->
class ExternalFeed extends Backbone.Model
resourceName: 'external_feeds'

View File

@ -2,8 +2,8 @@ define [
'Backbone'
'underscore'
'vendor/jquery.ba-tinypubsub'
'compiled/models/Topic'
], (Backbone, _, {subscribe}, Topic) ->
'compiled/models/DiscussionTopic'
], (Backbone, _, {subscribe}, DiscussionTopic) ->
class KollectionItem extends Backbone.Model
@ -23,7 +23,7 @@ define [
"/api/v1/collections/items/#{encodeURIComponent(@id)}"
initialize: ->
@commentTopic = new Topic
@commentTopic = new DiscussionTopic
@commentTopic.url = => "/api/v1/collection_items/#{@id}/discussion_topics/self"
_.each ['upvote', 'deupvote'], (action) =>
subscribe "#{action}Item", (itemId) =>

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2011 Instructure, Inc.
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
@ -16,5 +16,6 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module TopicsHelper
end
define ['Backbone'], ({Model}) ->
class Section extends Model

View File

@ -1,28 +0,0 @@
define [
'Backbone'
'underscore'
'compiled/collections/ParticipantCollection'
'compiled/collections/EntriesCollection'
], (Backbone, _, ParticipantCollection, EntriesCollection) ->
class Topic extends Backbone.Model
initialize: ->
@participants = new ParticipantCollection
@entries = new EntriesCollection
@entries.url = => "#{_.result this, 'url'}/entries"
@entries.participants = @participants
##
# this is for getting the topic 'full view' from the api
# see: http://<canvas>/doc/api/discussion_topics.html#method.discussion_topics_api.view
fetchEntries: ->
baseUrl = _.result this, 'url'
$.get "#{baseUrl}/view", ({unread_entries, participants, view: entries}) =>
@unreadEntries = unread_entries
@participants.reset participants
# TODO: handle nested replies and 'new_entries' here
@entries.reset(entries)

View File

@ -83,10 +83,10 @@ define [
buildPolicyCellsHtml: (category) =>
fragments = for c in @channels
policy = _.find @policies, (p) ->
p.channel_id is c.id and p.category_id is category.id
p.channel_id is c.id and p.category is category.category
frequency = 'never'
frequency = policy['frequency'] if policy
@policyCellHtml(category.id, c.id, frequency)
@policyCellHtml(category, c.id, frequency)
fragments.join ''
hideButtonsExceptCell: ($notCell) =>
@ -154,17 +154,17 @@ define [
@setupEventBindings()
# Generate and return the HTML for an option cell with the with the sepecified value set/reflected.
policyCellHtml: (categoryId, channelId, selectedValue = 'never') =>
policyCellHtml: (category, channelId, selectedValue = 'never') =>
# Reset all buttons to not be active by default. Set their ID to be unique to the data combination.
_.each(@buttonData, (b) ->
b['active'] = false
b['coordinate'] = "cat_#{categoryId}_ch_#{channelId}"
b['coordinate'] = "cat_#{category.id}_ch_#{channelId}"
b['id'] = "#{b['coordinate']}_#{b['code']}"
)
selected = @findButtonDataForCode(selectedValue)
selected['active'] = true
policyCellTemplate(touch: @touch, categoryId: categoryId, channelId: channelId, selected: selected, allButtons: @buttonData)
policyCellTemplate(touch: @touch, category: category.category, channelId: channelId, selected: selected, allButtons: @buttonData)
# Record and display the value for the cell.
saveNewCellValue: ($cell, value) =>
@ -175,10 +175,10 @@ define [
$cell.find('a.change-selection span.ui-icon').attr('class', 'ui-icon '+btnData['image'])
$cell.find('a.change-selection span.img-text').text(btnData['text'])
# Get category and channel values
categoryId = $cell.attr('data-categoryId')
category = $cell.attr('data-category')
channelId = $cell.attr('data-channelId')
# Send value to server
data = {category_id: categoryId, channel_id: channelId, frequency: value}
data = {category: category, channel_id: channelId, frequency: value}
@$notificationSaveStatus.disableWhileLoading $.ajaxJSON(@updateUrl, 'PUT', data, null,
# Error callback
((data) =>

View File

@ -29,13 +29,17 @@ define [
signupDialog($(this).data('template'), $(this).prop('title'))
$form = $node.find('form')
promise = null
$form.formSubmit
disableWhileLoading: true
beforeSubmit: ->
promise = $.Deferred()
$form.disableWhileLoading(promise)
success: (data) =>
# they should now be authenticated (either registered or pre_registered)
window.location = "/?login_success=1&registration_success=1"
formErrors: false
error: (errors) ->
promise.reject()
if _.any(errors.user.birthdate ? [], (e) -> e.type is 'too_young')
$node.find('.registration-dialog').html I18n.t('too_young_error', 'You must be at least %{min_years} years of age to use Canvas without a course join code.', min_years: ENV.USER.MIN_AGE)
$node.dialog buttons: [

View File

@ -71,7 +71,7 @@ define [
item.list = @list
item
if data.length
if data.length?
(modelize(item) for item in data)
else
modelize(data)

View File

@ -0,0 +1,21 @@
# analogous to ruby's Hash#invert, namely it takes an object and inverts
# the keys/values of its properties. takes an optional formatter for the
# key->value translation (otherwise they will just default to strings).
# in the event of duplicates, the last one wins.
#
# examples:
#
# > invert {a: 'A', b: 'B', c: 'C', dup: 'A'}
# => {A: 'dup', B: 'b', C: 'c'}
#
# > invert ['a', 'b', 'c'], parseInt
# => {a: 0, b: 1, c: 2}
#
define ->
invert = (object, formatter) ->
result = {}
for own key, value of object
result[value] = if formatter then formatter(key) else key
result

View File

@ -0,0 +1,81 @@
define [
'i18n!discussion_topics'
'compiled/views/ValidatedFormView'
'underscore'
'jst/DiscussionTopics/EditView'
'wikiSidebar'
'str/htmlEscape'
'jquery'
'compiled/tinymce'
'tinymce.editor_box'
'jquery.instructure_misc_helpers' # $.scrollSidebar
'compiled/jquery.rails_flash_notifications' #flashMessage
], (I18n, ValidatedFormView, _, template, wikiSidebar, htmlEscape, $) ->
class EditView extends ValidatedFormView
template: template
tagName: 'form'
className: 'form-horizontal bootstrap-form'
dontRenableAfterSaveSuccess: true
events: _.extend(@::events,
'click .removeAttachment' : 'removeAttachment'
)
initialize: ->
@model.on 'sync', -> window.location = @get 'html_url'
super
toJSON: ->
_.extend super, @options,
showAssignment: !!@assignmentGroupCollection
render: =>
super
unless wikiSidebar.inited
wikiSidebar.init()
$.scrollSidebar()
wikiSidebar.attachToEditor @$('textarea[name=message]').attr('id', _.uniqueId('discussion-topic-message')).editorBox()
wikiSidebar.show()
if @assignmentGroupCollection
(@assignmentGroupFetchDfd ||= @assignmentGroupCollection.fetch()).done @renderAssignmentGroupOptions
@$(".datetime_field").datetime_field()
this
# I am sad that this code even had to be written, we should abstract away
# handling a 'remoteSelect' for a collection
renderAssignmentGroupOptions: =>
html = @assignmentGroupCollection.map (ag) ->
"<option value='#{ag.id}'>#{htmlEscape ag.get('name')}</option>"
.join('')
@$('[name="assignment[assignment_group_id]"]')
.html(html)
.prop('disabled', false)
.val @model.get('assignment')?.assignment_group_id
getFormData: ->
data = super
data.title ||= I18n.t 'default_discussion_title', 'No Title'
data.delay_posting_at = data.delay_posting && data.delay_posting_at
data.discussion_type = if data.threaded then 'threaded' else 'side_comment'
delete data.assignment unless data.assignment?.set_assignment
# these options get passed to Backbone.sync in ValidatedFormView
@saveOpts = multipart: !!data.attachment
data
removeAttachment: ->
@model.set 'attachments', []
@$el.append '<input type="hidden" name="remove_attachment" >'
@$('.attachmentRow').remove()
@$('[name="attachment"]').show()

View File

@ -0,0 +1,155 @@
define [
'i18n!discussion_topics'
'underscore'
'jst/DiscussionTopics/IndexView'
'compiled/views/PaginatedView'
'compiled/views/DiscussionTopics/SummaryView'
'compiled/collections/AnnouncementsCollection'
], (I18n, _, template, PaginatedView, DiscussionTopicSummaryView, AnnouncementsCollection) ->
class IndexView extends PaginatedView
template: template
el: '#content'
initialize: ->
super
@collection.on 'remove', => @render() unless @collection.length
@collection.on 'reset', @render
@collection.on 'add didFetchNextPage', @renderList
@collection.on 'change:selected', @toggleActionsForSelectedDiscussions
@render()
render: =>
super
@$('#discussionsFilter').buttonset()
@renderList()
@toggleActionsForSelectedDiscussions()
this
renderList: =>
$list = @$('.discussionTopicIndexList').empty()
nothingMatched = not _.any @collection.map @addDiscussionTopicToList
@$('.nothingMatchedFilter').toggle nothingMatched && !@collection.fetchingNextPage
makeSortable = !nothingMatched &&
!@activeFilters.length &&
!@isShowingAnnouncements() &&
@options.permissions.moderate
if makeSortable
$list.sortable
axis: 'y'
cancel: 'a'
containment: $list
cursor: 'ns-resize'
handle: '.discussion-drag-handle'
else if $list.is(':ui-sortable')
$list.sortable('destroy')
addDiscussionTopicToList: (discussionTopic) =>
if @modelMeetsFilterRequirements(discussionTopic)
view = new DiscussionTopicSummaryView
model: discussionTopic
permissions: @options.permissions
@$('.discussionTopicIndexList').append view.render().el
toggleActionsForSelectedDiscussions: =>
selectedTopics = @selectedTopics()
atLeastOneSelected = selectedTopics.length > 0
$actions = @$('#actionsForSelectedDiscussions').toggle atLeastOneSelected
if atLeastOneSelected
checkLock = _.any selectedTopics, (model) -> model.get('locked')
$actions.find('#lock').prop('checked', checkLock).button
text: false
icons:
primary: 'ui-icon-locked'
$actions.find('#delete').button
text: false
icons:
primary: 'ui-icon-trash'
$actions.buttonset()
toggleLockingSelectedTopics: ->
lock = @$('#lock').is(':checked')
_.invoke @selectedTopics(), 'updateOneAttribute', 'locked', lock
destroySelectedTopics: ->
selectedTopics = @selectedTopics()
message = if @isShowingAnnouncements()
I18n.t 'confirm_delete_announcement',
one: 'Are you sure you wan to delete this announcement?'
other: 'Are you sure you want to delete these %{count} announcements?'
,
count: selectedTopics.length
else
I18n.t 'confirm_delete_discussion_topic',
one: 'Are you sure you wan to delete this discussion topic?'
other: 'Are you sure you want to delete these %{count} discussion topics?'
,
count: selectedTopics.length
if confirm message
_(selectedTopics).invoke 'destroy'
@toggleActionsForSelectedDiscussions()
selectedTopics: ->
@collection.filter (model) -> model.selected
events:
'change #onlyUnread, #onlyGraded, #searchTerm' : 'handleFilterChange'
'input #searchTerm' : 'handleFilterChange'
'sortupdate' : 'handleSortUpdate'
'change #lock' : 'toggleLockingSelectedTopics'
'click #delete' : 'destroySelectedTopics'
modelMeetsFilterRequirements: (model) =>
_.all @activeFilters(), (fn, key) =>
fn.call(model, @[key])
handleSortUpdate: (event, ui) =>
id = ui.item.data 'id'
otherId = ui.item.next('.discussion-topic').data 'id'
@collection.get(id).positionAfter otherId
activeFilters: ->
res = {}
res[key] = fn for key, fn of @filters when @[key]
res
handleFilterChange: (event) ->
input = event.target
val = if input.type is "checkbox" then input.checked else input.value
@[input.id] = val
@renderList()
@collection.trigger 'aBogusEventToCauseYouToFetchNextPageIfNeeded'
filters:
onlyGraded: -> @get 'assignment_id'
onlyUnread: -> (@get('read_state') is 'unread') or @get('unread_count')
searchTerm: (term) ->
words = term.match(/\w+/ig)
pattern = "(#{_.uniq(words).join('|')})"
regexp = new RegExp(pattern, "igm")
@get('author')?.display_name?.match(regexp) ||
@get('title').match(regexp) ||
@summary().match(regexp)
isShowingAnnouncements: ->
@collection.constructor == AnnouncementsCollection
toJSON: ->
new_topic_url = @collection.url().replace('/api/v1', '') + '/new'
if @isShowingAnnouncements()
new_topic_url = (new_topic_url + '?is_announcement=true')
# announcements will have a '?only_announcements=true' at the end, remove it
.replace(@collection._stringToAppendToURL, '')
filterProps = _.pick this, _.keys(@filters)
collectionProps = _.pick @collection, ['atLeastOnePageFetched', 'length']
_.extend
new_topic_url: new_topic_url
options: @options
showingAnnouncements: @isShowingAnnouncements()
, filterProps, collectionProps

View File

@ -0,0 +1,54 @@
define [
'i18n!discussion_topics'
'Backbone'
'underscore'
'jst/DiscussionTopics/SummaryView'
'jst/_api_avatar'
], (I18n, Backbone, _, template) ->
class DiscussionTopicSummaryView extends Backbone.View
template: template
attributes: ->
'class': "discussion-topic #{@model.get('read_state')} #{if @model.selected then 'selected' else '' }"
'data-id': @model.id
events:
'change .toggleSelected' : 'toggleSelected'
'click' : 'openOnClick'
initialize: ->
@model.on 'change reset', @render, this
@model.on 'destroy', @remove, this
toJSON: ->
_.extend super,
permissions: @options.permissions
selected: @model.selected
unread_count_tooltip: (I18n.t 'unread_count_tooltip', {
zero: 'No unread replies'
one: '1 unread reply'
other: '%{count} unread replies'
}, count: @model.get('unread_count'))
reply_count_tooltip: (I18n.t 'reply_count_tooltip', {
zero: 'No replies',
one: '1 reply',
other: '%{count} replies'
}, count: @model.get('discussion_subentry_count'))
summary: @model.summary()
render: ->
super
@$el.attr @attributes()
this
toggleSelected: ->
@model.selected = !@model.selected
@model.trigger 'change:selected'
@$el.toggleClass 'selected', @model.selected
openOnClick: (event) ->
window.location = @model.get('html_url') unless $(event.target).closest(':focusable, label').length

View File

@ -0,0 +1,36 @@
define [
'Backbone'
'underscore'
'jst/ExternalFeeds/IndexView'
'compiled/fn/preventDefault'
'jquery'
'jquery.toJSON'
], (Backbone, _, template, preventDefault, $) ->
class IndexView extends Backbone.View
template: template
el: '#right-side'
events:
'submit #add_external_feed_form' : 'submit'
'click [data-delete-feed-id]' : 'deleteFeed'
initialize: ->
super
@collection.on 'all', @render, this
@render()
render: ->
if @collection.length || @options.permissions.create
$('body').addClass('with-right-side')
super
deleteFeed: preventDefault (event) ->
id = @$(event.target).data('deleteFeedId')
@collection.get(id).destroy()
submit: preventDefault (event) ->
data = @$('#add_external_feed_form').toJSON()
@$el.disableWhileLoading @collection.create data

View File

@ -43,7 +43,7 @@ define [
containerScrollHeight - $container.scrollTop() - $container.height()
startPaginationListener: ->
$(@paginationScrollContainer).on "scroll.pagination#{@cid} resize.pagination#{@cid}", $.proxy @fetchNextPageIfNeeded, this
$(@paginationScrollContainer).on "scroll.pagination#{@cid}, resize.pagination#{@cid}", $.proxy @fetchNextPageIfNeeded, this
@fetchNextPageIfNeeded()
stopPaginationListener: ->
@ -54,5 +54,5 @@ define [
unless @collection.nextPageUrl
@stopPaginationListener() if @collection.length
return
if @distanceToBottom() < @distanceTillFetchNextPage
if $(@paginationScrollContainer).is(':visible') and @distanceToBottom() < @distanceTillFetchNextPage
@collection.fetchNextPage @fetchOptions

View File

@ -1,7 +1,7 @@
define [
'compiled/views/QuickStartBar/BaseItemView'
'underscore'
'compiled/models/Discussion'
'compiled/models/DiscussionTopic'
'jst/quickStartBar/discussion'
'jquery.instructure_date_and_time'
'vendor/jquery.placeholder'

View File

@ -1,9 +1,9 @@
define [
'Backbone',
'Backbone'
'i18n!dashboard'
'underscore'
'jst/quickStartBar/QuickStartBarView'
'formToJSON'
'jquery.toJSON'
], ({View, Model}, I18n, _, template) ->
capitalize = (str) ->

View File

@ -0,0 +1,22 @@
define [
'jquery'
'underscore'
'compiled/views/PaginatedView'
'compiled/views/RecentStudents/RecentStudentView'
], ($, _, PaginatedView, RecentStudentView) ->
class RecentStudentCollectionView extends PaginatedView
initialize: (options) ->
@collection.on 'add', @renderUser
@collection.on 'reset', @render
@paginationScrollContainer = @$el
super
render: =>
ret = super
@collection.each (user) => @renderUser user
ret
renderUser: (user) =>
@$el.append (new RecentStudentView model: user).render().el

View File

@ -0,0 +1,24 @@
define [
'i18n!course_statistics'
'jquery'
'underscore'
'Backbone'
'jst/recentStudent'
], (I18n, $, _, Backbone, RecentStudentTemplate) ->
class RecentStudentView extends Backbone.View
tagName: 'li'
template: RecentStudentTemplate
toJSON: ->
data = @model.toJSON()
if data.last_login?
date = $.fudgeDateForProfileTimezone(new Date(data.last_login), false)
data.last_login = I18n.t '#time.event', '%{date} at %{time}',
date: I18n.l('#date.formats.short', date)
time: I18n.l('#time.formats.tiny', date)
else
data.last_login = I18n.t 'unknown', 'unknown'
data

View File

@ -1,9 +1,11 @@
define [
'Backbone'
'formToJSON'
'jquery'
'compiled/fn/preventDefault'
'jquery.toJSON'
'jquery.disableWhileLoading'
'jquery.instructure_forms'
], ({View, Model}, formToJSON) ->
], (Backbone, $, preventDefault) ->
##
# Sets model data from a form, saves it, and displays errors returned in a
@ -18,33 +20,45 @@ define [
#
# @event success
# @signature `(response, status, jqXHR)`
class ValidatedFormView extends View
class ValidatedFormView extends Backbone.View
tagName: 'form'
className: 'validated-form-view'
events: {'submit'}
events:
submit: 'submit'
##
# When the form submits, the model's attributes are set from the form
# and saved to the server. Make sure to pass in `model` to the options on
# initialize
model: Model.extend()
model: Backbone.Model.extend()
##
# Sets the model data from the form and saves it. Called when the form
# submits, or can be called programatically.
# set @saveOpts in your vew to to pass opts to Backbone.sync (like multipart: true if you have
# a file attachment). if you want the form not to be re-enabled after save success (because you
# are navigating to a new page, set dontRenableAfterSaveSuccess to true on your view)
#
# @api public
# @returns jqXHR
submit: (event) ->
event.preventDefault() if event
submit: preventDefault ->
data = @getFormData()
dfd = @model.save(data).then @onSaveSuccess, @onSaveFail
@$el.disableWhileLoading dfd
disablingDfd = new $.Deferred()
saveDfd = @model
.save(data, @saveOpts)
.then(@onSaveSuccess, @onSaveFail)
.fail -> disablingDfd.reject()
unless @dontRenableAfterSaveSuccess
saveDfd.done -> disablingDfd.resolve()
@$el.disableWhileLoading disablingDfd
@trigger 'submit'
dfd
saveDfd
##
# Converts the form to an object. Override this if the form's input names
@ -117,5 +131,8 @@ define [
findField: (field) ->
selector = @fieldSelectors?[field] or "[name=#{field}]"
@$ selector
$el = @$(selector)
if $el.data('rich_text')
$el = $el.next('.mceEditor').find(".mceIframeContainer")
$el

View File

@ -34,7 +34,7 @@ define [
selector:
baseData:
type: 'section'
context: "course_#{ENV.COURSE_ID}"
context: "course_#{ENV.COURSE_ID}_sections"
exclude: _.map(@model.get('enrollments'), (e) -> "section_#{e.course_section_id}")
preparer: (postData, data, parent) ->
row.noExpand = true for row in data

View File

@ -4,6 +4,7 @@ define [
'underscore'
'compiled/views/DialogBaseView'
'jst/courses/settings/LinkToStudentsView'
'compiled/jquery.whenAll'
'jquery.disableWhileLoading'
], (I18n, $, _, DialogBaseView, linkToStudentsViewTemplate) ->
@ -31,8 +32,9 @@ define [
selector:
baseData:
type: 'user'
context: "course_#{ENV.COURSE_ID}"
context: "course_#{ENV.COURSE_ID}_students"
exclude: [@model.get('id')]
skip_visibility_checks: true
preparer: (postData, data, parent) ->
row.noExpand = true for row in data
browser:
@ -49,7 +51,9 @@ define [
text: user.name
data: user
$.when(dfds...).done -> dfd.resolve()
# if a dfd fails (e.g. observee was removed from course), we still want
# the observer dialog to render (possibly with other observees)
$.whenAll(dfds...).always -> dfd.resolve(data)
this
getUserData: (id) ->

View File

@ -10,7 +10,9 @@ define [
'jst/courses/settings/UserView'
'compiled/str/underscore'
'str/htmlEscape'
'compiled/jquery.whenAll'
'compiled/jquery.kylemenu'
'compiled/jquery.rails_flash_notifications'
], (I18n, $, _, Backbone, EditSectionsView, InvitationsView, LinkToStudentsView, User, userViewTemplate, toUnderscore, h) ->
editSectionsDialog = null
@ -45,10 +47,16 @@ define [
dfds = []
data = $.extend @model.toJSON(),
url: "#{ENV.COURSE_ROOT_URL}/users/#{@model.get('id')}"
permissions: ENV.PERMISSIONS
isObserver: @model.hasEnrollmentType('ObserverEnrollment')
isDesigner: @model.hasEnrollmentType('DesignerEnrollment')
isPending: @model.pending()
for en in data.enrollments
data.canRemove =
if _.any(['TeacherEnrollment', 'DesignerEnrollment', 'TaEnrollment'], (et) => @model.hasEnrollmentType et)
ENV.PERMISSIONS.manage_admin_users
else
ENV.PERMISSIONS.manage_students
for en in data.enrollments when ! data.isDesigner
en.pending = @model.pending()
en.typeClass = toUnderscore en.type
section = ENV.CONTEXTS['sections'][en.course_section_id]
@ -66,7 +74,10 @@ define [
section = ENV.CONTEXTS['sections'][en.course_section_id]
ob.sectionTitle += h(I18n.t('#support.array.words_connector') + section.name) if section
data.enrollments.push ob
$.when(dfds...).done -> dfd.resolve data
# if a dfd fails (e.g. observee was removed from course), we still want
# the observer to render (possibly with other observees)
$.whenAll(dfds...).then ->
dfd.resolve(data)
dfd.promise()
reload: =>
@ -94,14 +105,16 @@ define [
linkToStudentsDialog.render().show()
removeFromCourse: (e) ->
return unless confirm I18n.t('links.unenroll_user_course', 'Remove User from Course')
return unless confirm I18n.t('delete_confirm', 'Are you sure you want to remove this user?')
@$el.hide()
success = =>
for e in @model.get('enrollments')
e_type = e.typeClass.split('_')[0]
c.innerText = parseInt(c.innerText) - 1 for c in $(".#{e_type}_count")
$.flashMessage I18n.t('flash.removed', 'User successfully removed.')
failure = =>
@$el.show()
$.flashError I18n.t('flash.removeError', 'Unable to remove the user. Please try again later.')
deferreds = _.map @model.get('enrollments'), (e) ->
$.ajaxJSON "#{ENV.COURSE_ROOT_URL}/unenroll/#{e.id}", 'DELETE'
$.when(deferreds...).then success, failure

View File

@ -0,0 +1,112 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
define [
'jquery'
'underscore'
'compiled/views/PaginatedView'
'jst/courses/RosterUser'
], ($, _, PaginatedView, rosterUser) ->
# This view displays a paginated collection of users inside of a course.
#
# @examples
#
# view = RosterView.new
# el: $('...')
# collection: EnrollmentCollection.new(...')
#
# view.collection.on('reset', view.render)
class RosterView extends PaginatedView
# Default options to be passed to the server on each request for new
# collection records.
fetchOptions:
include: ['avatar_url']
per_page: 50
# Create and configure a new RosterView.
#
# @param el {jQuery} - The parent element (should have overflow: hidden and
# a height for infinite scroll).
# @param collection {EnrollmentCollection} - The collection to retrieve
# results from.
# @param options {Object} - Configuration options.
# - requestOptions: options to be passed w/ every server call.
#
# @examples
#
# view = new RosterView
# el: $(...)
# collection: new EnrollmentCollection
# url: ...
# sections: ENV.SECTIONS
# requestOptions:
# type: ['StudentEnrollment']
# include: ['avatar_url']
# per_page: 25
#
# @api public
# @return a RosterView.
initialize: (options) ->
@fetchOptions =
data: _.extend({}, @fetchOptions, options.requestOptions)
add: false
@collection.on('reset', @render, this)
@paginationScrollContainer = @$el
@$el.disableWhileLoading(@collection.fetch(@fetchOptions))
super(fetchOptions: @fetchOptions)
# Append newly fetched records to the roster list.
#
# @api private
# @return nothing.
render: ->
users = @combinedSectionEnrollments(@collection)
enrollments = _.map(users, @renderUser)
@$el.append(enrollments.join(''))
super
# Create the HTML for a given user record.
#
# @param enrollment - An enrollment model.
#
# @api private
# @return nothing.
renderUser: (enrollment) ->
rosterUser(enrollment.toJSON())
# Take users in multiple sections and combine their section names
# into an array to be displayed in a list.
#
# @param collection {EnrollmentCollection} - Enrollments to format.
#
# @api private
# @return an array of user models.
combinedSectionEnrollments: (collection) ->
users = collection.groupBy (enrollment) -> enrollment.get('user_id')
enrollments = _.reduce users, (list, enrollments, key) ->
enrollment = enrollments[0]
names = _.map(enrollments, (e) -> e.get('course_section_name'))
# do it this way instead of calling .set(...) so that we don't fire an
# extra page load from PaginatedView.
enrollment.attributes.course_section_name = _.uniq(names)
list.push(enrollment)
list
, []
enrollments

View File

@ -24,6 +24,7 @@ class AccountAuthorizationConfigsController < ApplicationController
@account_configs = @account.account_authorization_configs.to_a
while @account_configs.length < 2
@account_configs << @account.account_authorization_configs.new
@account_configs.last.auth_over_tls = :start_tls
end
@saml_identifiers = Onelogin::Saml::NameIdentifiers::ALL_IDENTIFIERS
@saml_login_attributes = AccountAuthorizationConfig.saml_login_attributes
@ -102,10 +103,11 @@ class AccountAuthorizationConfigsController < ApplicationController
#
# The LDAP server's TCP port. (default: 389)
#
# - auth_over_tls [Optional, Boolean]
# - auth_over_tls [Optional]
#
# Whether to use simple TLS encryption. Only simple TLS encryption is
# supported at this time. (default: false)
# Whether to use TLS. Can be '', 'simple_tls', or 'start_tls'. For backwards
# compatibility, booleans are also accepted, with true meaning simple_tls.
# If not provided, it will default to start_tls.
#
# - auth_base [Optional]
#
@ -117,6 +119,11 @@ class AccountAuthorizationConfigsController < ApplicationController
# LDAP search filter. Use !{{login}} as a placeholder for the username
# supplied by the user. For example: "(sAMAccountName=!{{login}})".
#
# - identifier_format [Optional]
#
# The LDAP attribute to use to look up the Canvas login. Omit to use
# the username supplied by the user.
#
# - auth_username
#
# Username
@ -312,7 +319,7 @@ class AccountAuthorizationConfigsController < ApplicationController
when 'ldap'
[ :auth_type, :auth_host, :auth_port, :auth_over_tls, :auth_base,
:auth_filter, :auth_username, :auth_password, :change_password_url,
:login_handle_name ]
:identifier_format, :login_handle_name ]
when 'saml'
[ :auth_type, :log_in_url, :log_out_url, :change_password_url, :requested_authn_context,
:certificate_fingerprint, :identifier_format, :login_handle_name, :login_attribute ]
@ -322,6 +329,12 @@ class AccountAuthorizationConfigsController < ApplicationController
end
def filter_data(data)
data ? data.slice(*recognized_params(data[:auth_type])) : {}
data ||= {}
data = data.slice(*recognized_params(data[:auth_type]))
if data[:auth_type] == 'ldap'
data[:auth_over_tls] = 'start_tls' unless data.has_key?(:auth_over_tls)
data[:auth_over_tls] = AccountAuthorizationConfig.auth_over_tls_setting(data[:auth_over_tls])
end
data
end
end

View File

@ -62,16 +62,18 @@ class AccountReportsController < ApplicationController
# @example_response
#
# [
# {"id":"student_assignment_outcome_map_csv",
# "title":"Student Competency",
# "parameters":null
# {
# "report":"student_assignment_outcome_map_csv",
# "title":"Student Competency",
# "parameters":null
# },
# {"report":"grade_export_csv",
# "title":"Grade Export",
# "parameters":{
# "term":{
# "description":"The canvas id of the term to get grades from",
# "required":true
# {
# "report":"grade_export_csv",
# "title":"Grade Export",
# "parameters":{
# "term":{
# "description":"The canvas id of the term to get grades from",
# "required":true
# }
# }
# }

View File

@ -100,7 +100,6 @@ class AccountsController < ApplicationController
enable_user_notes = params[:account].delete :enable_user_notes
allow_sis_import = params[:account].delete :allow_sis_import
global_includes = !!params[:account][:settings].try(:delete, :global_includes)
params[:account].delete :default_user_storage_quota_mb unless @account.root_account? && !@account.site_admin?
if params[:account][:services]
params[:account][:services].slice(*Account.services_exposed_to_ui_hash.keys).each do |key, value|
@ -111,10 +110,21 @@ class AccountsController < ApplicationController
if Account.site_admin.grants_right?(@current_user, :manage_site_settings)
@account.enable_user_notes = enable_user_notes if enable_user_notes
@account.allow_sis_import = allow_sis_import if allow_sis_import && @account.root_account?
if params[:account][:settings]
@account.settings[:admins_can_change_passwords] = !!params[:account][:settings][:admins_can_change_passwords]
@account.settings[:global_includes] = global_includes
@account.settings[:enable_eportfolios] = !!params[:account][:settings][:enable_eportfolios] unless @account.site_admin?
if @account.site_admin? && params[:account][:settings]
# these shouldn't get set for the site admin account
params[:account][:settings].delete(:enable_alerts)
params[:account][:settings].delete(:enable_eportfolios)
end
else
# must have :manage_site_settings to update these
[ :admins_can_change_passwords,
:enable_alerts,
:enable_eportfolios,
:enable_profiles,
:enable_scheduler,
:global_includes,
].each do |key|
params[:account][:settings].try(:delete, key)
end
end
if sis_id = params[:account].delete(:sis_source_id)
@ -229,7 +239,7 @@ class AccountsController < ApplicationController
end
end
if @account.grants_right?(@current_user, nil, :read_roster)
@recently_logged_users = @account.all_users.recently_logged_in[0,25]
@recently_logged_users = @account.all_users.recently_logged_in
end
@counts_report = @account.report_snapshots.detailed.last.try(:data)
end

View File

@ -17,32 +17,39 @@
#
class AnnouncementsController < ApplicationController
include Api::V1::DiscussionTopics
before_filter :require_context, :except => :public_feed
before_filter { |c| c.active_tab = "announcements" }
before_filter { |c| c.active_tab = "announcements" }
def index
add_crumb(t(:announcements_crumb, "Announcements"))
if authorized_action(@context, @current_user, :read)
return if @context.class.const_defined?('TAB_ANNOUNCEMENTS') && !tab_enabled?(@context.class::TAB_ANNOUNCEMENTS)
@announcements = @context.active_announcements.paginate(:page => params[:page], :order => 'posted_at DESC').reject{|a| a.locked_for?(@current_user, :check_policies => true) }
log_asset_access("announcements:#{@context.asset_string}", "announcements", "other")
respond_to do |format|
format.html { render }
format.json { render :json => @announcements.to_json(:methods => [:user_name, :discussion_subentry_count], :permissions => {:user => @current_user, :session => session }) }
format.html do
add_crumb(t(:announcements_crumb, "Announcements"))
can_create = @context.announcements.new.grants_right?(@current_user, session, :create)
js_env :permissions => {
:create => can_create,
:moderate => can_create
}
js_env :is_showing_announcements => true
js_env :atom_feed_url => feeds_announcements_format_path((@context_enrollment || @context).feed_code, :atom)
end
end
end
end
def show
redirect_to named_context_url(@context, :context_discussion_topic_url, params[:id])
end
def public_feed
return unless get_feed_context
announcements = @context.announcements.active.find(:all, :order => 'posted_at DESC', :limit => 15).reject{|a| a.locked_for?(@current_user, :check_policies => true) }
respond_to do |format|
format.atom {
format.atom {
feed = Atom::Feed.new do |f|
f.title = t(:feed_name, "%{course} Announcements Feed", :course => @context.name)
f.links << Atom::Link.new(:href => polymorphic_url([@context, :announcements]), :rel => 'self')
@ -52,9 +59,9 @@ class AnnouncementsController < ApplicationController
announcements.each do |e|
feed.entries << e.to_atom
end
render :text => feed.to_xml
render :text => feed.to_xml
}
format.rss {
format.rss {
@announcements = announcements
require 'rss/2.0'
rss = RSS::Rss.new("2.0")
@ -76,30 +83,5 @@ class AnnouncementsController < ApplicationController
}
end
end
def create_external_feed
if authorized_action(@context.announcements.new, @current_user, :create)
params[:external_feed].delete(:add_header_match)
@feed = @context.external_feeds.build(params[:external_feed])
@feed.feed_purpose = "announcements"
@feed.user = @current_user
@feed.feed_type = "rss/atom"
if @feed.save
render :json => @feed.to_json
else
render :json => @feed.errors.to_json, :response => :bad_request
end
end
end
def destroy_external_feed
if authorized_action(@context.announcements.new, @current_user, :create)
@feed = @context.external_feeds.find(params[:id])
if @feed.destroy
render :json => @feed.to_json
else
render :json => @feed.errors.to_json, :response => :bad_request
end
end
end
end

View File

@ -33,6 +33,7 @@ class ApplicationController < ActionController::Base
include AuthenticationMethods
protect_from_forgery
before_filter :load_account, :load_user
before_filter :check_pending_otp
before_filter :set_user_id_header
before_filter :set_time_zone
before_filter :clear_cached_contexts
@ -153,6 +154,13 @@ class ApplicationController < ActionController::Base
@domain_root_account
end
def check_pending_otp
if session[:pending_otp] && !(params[:action] == 'otp_login' && request.method == :post)
reset_session
redirect_to login_url
end
end
# used to generate context-specific urls without having to
# check which type of context it is everywhere
def named_context_url(context, name, *opts)
@ -194,8 +202,18 @@ class ApplicationController < ActionController::Base
end
true
end
# checks the authorization policy for the given object using
def require_password_session
if session[:used_remember_me_token]
flash[:warning] = t "#application.warnings.please_log_in", "For security purposes, please enter your password to continue"
store_location
redirect_to login_url
return false
end
true
end
# checks the authorization policy for the given object using
# the vendor/plugins/adheres_to_policy plugin. If authorized,
# returns true, otherwise renders unauthorized messages and returns
# false. To be used as follows:
@ -304,7 +322,7 @@ class ApplicationController < ActionController::Base
unless @context
if params[:course_id]
@context = api_request? ?
api_find(Course, params[:course_id]) : Course.find(params[:course_id])
api_find(Course.active, params[:course_id]) : Course.active.find(params[:course_id])
params[:context_id] = params[:course_id]
params[:context_type] = "Course"
if @context && session[:enrollment_uuid_course_id] == @context.id
@ -1291,80 +1309,15 @@ class ApplicationController < ActionController::Base
add_crumb t('#crumbs.site_admin', "Site Admin"), url_for(Account.site_admin)
end
def can_add_notes_to?(course)
course.enable_user_notes && course.grants_right?(@current_user, nil, :manage_user_notes)
end
##
# Loads all the contexts the user belongs to into instance variable @contexts
# Used for TokenInput.coffee instances
def load_all_contexts
@contexts = Rails.cache.fetch(['all_conversation_contexts', @current_user].cache_key, :expires_in => 10.minutes) do
contexts = {:courses => {}, :groups => {}, :sections => {}}
term_for_course = lambda do |course|
course.enrollment_term.default_term? ? nil : course.enrollment_term.name
end
@current_user.concluded_courses.each do |course|
contexts[:courses][course.id] = {
:id => course.id,
:url => course_url(course),
:name => course.name,
:type => :course,
:term => term_for_course.call(course),
:state => course.recently_ended? ? :recently_active : :inactive,
:can_add_notes => can_add_notes_to?(course)
}
end
@current_user.courses.each do |course|
contexts[:courses][course.id] = {
:id => course.id,
:url => course_url(course),
:name => course.name,
:type => :course,
:term => term_for_course.call(course),
:state => :active,
:can_add_notes => can_add_notes_to?(course)
}
end
section_ids = @current_user.enrollment_visibility[:section_user_counts].keys
CourseSection.find(:all, :conditions => {:id => section_ids}).each do |section|
contexts[:sections][section.id] = {
:id => section.id,
:name => section.name,
:type => :section,
:term => contexts[:courses][section.course_id][:term],
:state => contexts[:courses][section.course_id][:state],
:parent => {:course => section.course_id},
:context_name => contexts[:courses][section.course_id][:name]
}
end if section_ids.present?
@current_user.messageable_groups.each do |group|
contexts[:groups][group.id] = {
:id => group.id,
:name => group.name,
:type => :group,
:state => group.active? ? :active : :inactive,
:parent => group.context_type == 'Course' ? {:course => group.context.id} : nil,
:context_name => group.context.name,
:category => group.category
}
end
contexts
end
end
def flash_notices
@notices ||= begin
notices = []
if error = flash.delete(:error)
notices << {:type => 'error', :content => error}
end
if warning = flash.delete(:warning)
notices << {:type => 'warning', :content => warning}
end
if notice = (flash[:html_notice] ? flash.delete(:html_notice).html_safe : flash.delete(:notice))
notices << {:type => 'success', :content => notice}
end
@ -1406,15 +1359,13 @@ class ApplicationController < ActionController::Base
data
end
FILTERED_PARAMETERS = [:password, :access_token, :api_key, :client_secret]
filter_parameter_logging *FILTERED_PARAMETERS
filter_parameter_logging *Canvas::LoggingFilter.filtered_parameters
# filter out sensitive parameters in the query string as well when logging
# the rails "Completed in XXms" line.
# this is fixed in Rails 3.x
def complete_request_uri
@@filtered_parameters_regex ||= %r{([?&](?:#{FILTERED_PARAMETERS.join('|')}))=[^&]+}
uri = request.request_uri.gsub(@@filtered_parameters_regex, '\1=[FILTERED]')
uri = Canvas::LoggingFilter.filter_uri(request.request_uri)
"#{request.protocol}#{request.host}#{uri}"
end
end

View File

@ -84,6 +84,7 @@ class AssignmentGroupsController < ApplicationController
end
hash
end
hashes.each { |group| group['group_weight'] = nil } unless @context.apply_group_weights?
render :json => hashes.to_json
}
end

View File

@ -19,6 +19,122 @@
# @API Assignments
#
# API for accessing assignment information.
#
# @object Assignment
# {
# // the ID of the assignment
# id: 4,
#
# // the name of the assignment
# name: "some assignment",
#
# // the assignment description, in an HTML fragment
# description: '<p>Do the following:</p>...',
#
# // the due date
# due_at: '2012-07-01T23:59:00-06:00',
#
# // the ID of the course the assignment belongs to
# course_id: 123,
#
# // the URL to the assignment's web page
# html_url: 'http://canvas.example.com/courses/123/assignments/4'
#
# // the ID of the assignment's group
# assignment_group_id: 2,
#
# // the ID of the assignments group set (if this is a group assignment)
# group_category_id: 1
#
# // if the requesting user has grading rights, the number of submissions that need grading.
# needs_grading_count: 17,
#
# // the sorting order of the assignment in the group
# position: 1,
#
# // the URL to the Canvas web UI page for the assignment
# html_url: "https://...",
#
# // whether the assignment is muted
# muted: false,
#
# // (Optional) explanation of lock status
# lock_explanation: "This assignment is locked until September 1 at 12:00am",
#
# // (Optional) whether anonymous submissions are accepted (applies only to quiz assignments)
# anonymous_submissions: false,
#
# // (Optional) list of file extensions allowed for submissions
# allowed_extensions: ["doc","xls"],
#
# // (Optional) the DiscussionTopic associated with the assignment, if applicable
# discussion_topic: { ... },
#
# // the maximum points possible for the assignment
# points_possible: 12,
#
# // the types of submissions allowed for this assignment
# // list containing one or more of the following:
# // "online_text_entry", "online_url", "online_upload", "media_recording"
# submission_types: ["online_text_entry"]
#
# // (Optional) the type of grading the assignment receives;
# // one of 'pass_fail', 'percent', 'letter_grade', 'points'
# grading_type: "points",
#
# // if true, the rubric is directly tied to grading the assignment.
# // Otherwise, it is only advisory.
# use_rubric_for_grading: true,
#
# // an object describing the basic attributes of the rubric, including the point total
# rubric_settings: {
# points_possible: 12
# },
#
# // a list of scoring criteria and ratings for each
# rubric: [
# {
# "points": 10,
# "id": "crit1",
# "description": "Criterion 1",
# "ratings": [
# {
# "points": 10,
# "id": "rat1",
# "description": "Full marks"
# },
# {
# "points": 7,
# "id": "rat2",
# "description": "Partial answer"
# },
# {
# "points": 0,
# "id": "rat3",
# "description": "No marks"
# }
# ]
# },
# {
# "points": 2,
# "id": "crit2",
# "description": "Criterion 2",
# "ratings": [
# {
# "points": 2,
# "id": "rat1",
# "description": "Pass"
# },
# {
# "points": 0,
# "id": "rat2",
# "description": "Fail"
# }
# ]
# }
# ]
# }
#
class AssignmentsApiController < ApplicationController
before_filter :require_context
@ -26,91 +142,7 @@ class AssignmentsApiController < ApplicationController
# @API List assignments
# Returns the list of assignments for the current context.
#
# @response_field id The unique identifier for the assignment.
# @response_field assignment_group_id The unique identifier of the assignment's group.
# @response_field name The name of the assignment.
# @response_field needs_grading_count [Integer] If the requesting user has grading rights, the number of submissions that need grading.
# @response_field position [Integer] The sorting order of this assignment in
# the group.
# @response_field points_possible The maximum possible points for the
# assignment.
# @response_field grading_type [Optional, "pass_fail"|"percent"|"letter_grade"|"points"]
# The type of grade the assignment receives.
# @response_field use_rubric_for_grading [Boolean] If true, the rubric is
# directly tied to grading the assignment. Otherwise, it is only advisory.
# @response_field rubric [Rubric]
# A list of rows and ratings for each row. TODO: need more discussion of the
# rubric data format and usage for grading.
# @response_field rubric_settings
# An object describing the basic attributes of the rubric, including the point total.
# @response_field group_category_id [Integer] The unique identifier of the assignment's group set (if this is a group assignment)
# @response_field html_url The URL to the Canvas web UI page for the assignment.
#
# @example_response
# [
# {
# "id": 4,
# "assignment_group_id": 2,
# "name": "some assignment",
# "points_possible": 12,
# "grading_type": "points",
# "due_at": "2011-05-26T23:59:00-06:00",
# "submission_types" : [
# "online_upload",
# "online_text_entry",
# "online_url",
# "media_recording"
# ],
# "use_rubric_for_grading": true,
# "html_url": "https://...",
# "rubric_settings": {
# "points_possible": 12
# }
# "rubric": [
# {
# "ratings": [
# {
# "points": 10,
# "id": "rat1",
# "description": "A"
# },
# {
# "points": 7,
# "id": "rat2",
# "description": "B"
# },
# {
# "points": 0,
# "id": "rat3",
# "description": "F"
# }
# ],
# "points": 10,
# "id": "crit1",
# "description": "Crit1"
# },
# {
# "ratings": [
# {
# "points": 2,
# "id": "rat1",
# "description": "Pass"
# },
# {
# "points": 0,
# "id": "rat2",
# "description": "Fail"
# }
# ],
# "points": 2,
# "id": "crit2",
# "description": "Crit2"
# }
# ],
# "group_category_id: 1
# }
# ]
# @returns [Assignment]
def index
if authorized_action(@context, @current_user, :read)
@assignments = @context.active_assignments.find(:all,
@ -124,6 +156,9 @@ class AssignmentsApiController < ApplicationController
end
end
# @API Get a single assignment
# Returns the assignment with the given id.
# @returns Assignment
def show
if authorized_action(@context, @current_user, :read)
@assignment = @context.active_assignments.find(params[:id],
@ -146,6 +181,7 @@ class AssignmentsApiController < ApplicationController
# @argument assignment[due_at] [Timestamp] The day/time the assignment is due. Accepts
# times in ISO 8601 format, e.g. 2011-10-21T18:48Z.
# @argument assignment[description] [String] The assignment's description, supports HTML.
# @returns Assignment
def create
@assignment = create_api_assignment(@context, params[:assignment])
@ -163,6 +199,7 @@ class AssignmentsApiController < ApplicationController
# @API Edit an assignment
# Modify an existing assignment. See the documentation for assignment
# creation.
# @returns Assignment
def update
@assignment = @context.assignments.find(params[:id])

View File

@ -337,6 +337,7 @@ class AssignmentsController < ApplicationController
# curl https://<canvas>/api/v1/courses/<course_id>/assignments/<assignment_id> \
# -X DELETE \
# -H 'Authorization: Bearer <token>'
# @returns Assignment
def destroy
@assignment = @context.assignments.active.find(params[:id])
if authorized_action(@assignment, @current_user, :delete)

View File

@ -190,11 +190,13 @@ class CalendarsController < ApplicationController
get_all_pertinent_contexts
@events = []
@contexts.each do |context|
@assignments = context.assignments.active.find(:all) if context.respond_to?("assignments")
@events.concat context.calendar_events.active.find(:all)
@events.concat @assignments || []
@events = @events.sort_by{ |e| [(e.start_at || Time.now), e.title] }
ActiveRecord::Base::ConnectionSpecification.with_environment(:slave) do
@contexts.each do |context|
@assignments = context.assignments.active.find(:all) if context.respond_to?("assignments")
@events.concat context.calendar_events.active.find(:all)
@events.concat @assignments || []
@events = @events.sort_by{ |e| [(e.start_at || Time.now), e.title] }
end
end
@contexts.each do |context|
log_asset_access("calendar_feed:#{context.asset_string}", "calendar", 'other')

View File

@ -243,12 +243,9 @@ class CommunicationChannelsController < ApplicationController
@pseudonym.attributes = params[:pseudonym]
@pseudonym.communication_channel = cc
# validate the e-mail address
@pseudonym.require_email_unique_id = true
# ensure the password gets validated
# ensure the password gets validated, but don't require confirmation
@pseudonym.require_password = true
# don't require confirmation
@pseudonym.password_confirmation = params[:pseudonym][:password] if params[:pseudonym]
@pseudonym.password_confirmation = @pseudonym.password = params[:pseudonym][:password] if params[:pseudonym]
return unless @pseudonym.valid?

View File

@ -286,36 +286,27 @@ class ContextController < ApplicationController
format.json { render :json => {:marked_as_read => true}.to_json }
end
end
def roster
if authorized_action(@context, @current_user, [:read_roster, :manage_students, :manage_admin_users])
log_asset_access("roster:#{@context.asset_string}", "roster", "other")
if @context.is_a?(Course)
@enrollments_hash = Hash.new{ |hash,key| hash[key] = [] }
@context.enrollments.sort_by{|e| [e.state_sortable, e.rank_sortable] }.each{ |e| @enrollments_hash[e.user_id] << e }
@students = @context.
students_visible_to(@current_user).
scoped(:conditions => "enrollments.type != 'StudentViewEnrollment'").
order_by_sortable_name.uniq
@teachers = @context.instructors.order_by_sortable_name.uniq
user_ids = @students.map(&:id) + @teachers.map(&:id)
if @context.visibility_limited_to_course_sections?(@current_user)
user_ids = @students.map(&:id) + [@current_user.id]
end
@primary_users = {t('roster.students', 'Students') => @students}
@secondary_users = {t('roster.teachers', 'Teachers & TAs') => @teachers}
elsif @context.is_a?(Group)
@users = @context.participating_users.order_by_sortable_name.uniq
@primary_users = {t('roster.group_members', 'Group Members') => @users}
if @context.context && @context.context.is_a?(Course)
@secondary_users = {t('roster.teachers', 'Teachers & TAs') => @context.context.instructors.order_by_sortable_name.uniq}
end
return unless authorized_action(@context, @current_user, [:read_roster, :manage_students, :manage_admin_users])
log_asset_access("roster:#{@context.asset_string}", 'roster', 'other')
if @context.is_a?(Course)
sections = @context.course_sections(:select => 'id, name')
js_env :SECTIONS => sections.map { |s| { :id => s.id, :name => s.name } }
elsif @context.is_a?(Group)
@users = @context.participating_users.order_by_sortable_name.uniq
@primary_users = { t('roster.group_members', 'Group Members') => @users }
if course = @context.context.try(:is_a?, Course)
@secondary_users = { t('roster.teachers', 'Teachers & TAs') => course.instructors.order_by_sortable_name.uniq }
end
@secondary_users ||= {}
@groups = @context.groups.active rescue []
end
@secondary_users ||= {}
@groups = @context.groups.active rescue []
end
def prior_users
if authorized_action(@context, @current_user, [:manage_students, :manage_admin_users, :read_prior_roster])
@prior_memberships = @context.enrollments.not_fake.scoped(:conditions => {:workflow_state => 'completed'}, :include => :user).to_a.once_per(&:user_id).sort_by{|e| [e.rank_sortable(true), e.user.sortable_name.downcase] }
@ -380,12 +371,17 @@ class ContextController < ApplicationController
@messages = @entries
@messages = @messages.select{|m| m.grants_right?(@current_user, session, :read) }.sort_by{|e| e.created_at }.reverse
@user_data = profile_data(
@user.profile,
@current_user,
session,
['links', 'user_services']
)
if @domain_root_account.enable_profiles?
@user_data = profile_data(
@user.profile,
@current_user,
session,
['links', 'user_services']
)
render :action => :new_roster_user
return false
end
true
end
end

View File

@ -244,14 +244,19 @@ class ContextModulesController < ApplicationController
order = params[:order].split(",")
tags = @context.context_module_tags.active.find_all_by_id(order).compact
affected_module_ids = tags.map(&:context_module_id).uniq.compact
affected_items = []
items = order.map{|id| tags.detect{|t| t.id == id.to_i } }.compact.uniq
items.each_index do |idx|
item = items[idx]
items.each_with_index do |item, idx|
item.position = idx
item.context_module_id = @module.id
item.save
if item.changed?
item.skip_touch = true
item.save
affected_items << item
end
end
ContextModule.update_all({:updated_at => Time.now.utc}, {:id => affected_module_ids})
ContentTag.touch_context_modules(affected_module_ids)
ContentTag.update_could_be_locked(affected_items)
@context.touch
@module.reload
respond_to do |format|

View File

@ -27,7 +27,6 @@ class ConversationsController < ApplicationController
before_filter :require_user, :except => [:public_feed]
before_filter :reject_student_view_student
before_filter :set_avatar_size
before_filter :get_conversation, :only => [:show, :update, :destroy, :add_recipients, :remove_messages]
before_filter :load_all_contexts, :except => [:public_feed]
before_filter :infer_scope, :only => [:index, :show, :create, :update, :add_recipients, :add_message, :remove_messages]
@ -525,6 +524,11 @@ class ConversationsController < ApplicationController
end
end
# @API Find recipients
#
# Deprecated, see the Find recipients endpoint in the Search API
def find_recipients; end
def public_feed
return unless get_feed_context(:only => [:user])
@current_user = @context
@ -718,7 +722,7 @@ class ConversationsController < ApplicationController
messages.map{ |message|
result = message.as_json
result['media_comment'] = media_comment_json(result['media_comment']) if result['media_comment']
result['attachments'] = result['attachments'].map{ |attachment| attachment_json(attachment) }
result['attachments'] = result['attachments'].map{ |attachment| attachment_json(attachment, @current_user) }
result['forwarded_messages'] = jsonify_messages(result['forwarded_messages'])
result['submission'] = submission_json(message.submission, message.submission.assignment, @current_user, session, nil, ['assignment', 'submission_comments']) if message.submission
result

View File

@ -19,8 +19,56 @@
require 'set'
# @API Courses
#
# API for accessing course information.
#
# @object Course
# {
# // the unique identifier for the course
# id: 370663,
#
# // the SIS identifier for the course, if defined
# sis_course_id: null,
#
# // the full name of the course
# name: "InstructureCon 2012",
#
# // the course code
# course_code: "INSTCON12",
#
# // the account associated with the course
# account_id: 81259,
#
# // the start date for the course, if applicable
# start_at: "2012-06-01T00:00:00-06:00",
#
# // the end date for the course, if applicable
# end_at: null,
#
# // A list of enrollments linking the current user to the course.
# // for student enrollments, grading information may be included
# // if include[]=total_scores
# enrollments: [
# {
# type: student,
# computed_final_score: 41.5,
# computed_current_score: 90,
# computed_final_grade: 'A-'
# }
# ],
#
# // course calendar
# calendar: {
# ics: "https:\/\/canvas.instructure.com\/feeds\/calendars\/course_abcdef.ics"
# }
#
# // optional: user-generated HTML for the course syllabus
# syllabus_body: "<p>syllabus html goes here<\/p>",
#
# // optional: the number of submissions needing grading
# // returned only if the current user has grading rights
# // and include[]=needs_grading_count
# needs_grading_count: '17'
# }
class CoursesController < ApplicationController
include SearchHelper
@ -56,20 +104,7 @@ class CoursesController < ApplicationController
# calculated_final_score (if available). This argument is ignored if the
# course is configured to hide final grades.
#
# @response_field id The unique identifier for the course.
# @response_field name The name of the course.
# @response_field course_code The course code.
# @response_field enrollments A list of enrollments linking the current user
# to the course.
# @response_field sis_course_id The SIS id of the course, if defined.
#
# @response_field needs_grading_count Number of submissions needing grading
# for all the course assignments. Only returned if
# include[]=needs_grading_count
#
# @example_response
# [ { 'id': 1, 'name': 'first course', 'course_code': 'first', 'enrollments': [{'type': 'student', 'computed_current_score': 84.8, 'computed_final_score': 62.9, 'computed_final_grade': 'D-'}], 'calendar': { 'ics': '..url..' } },
# { 'id': 2, 'name': 'second course', 'course_code': 'second', 'enrollments': [{'type': 'teacher'}], 'calendar': { 'ics': '..url..' } } ]
# @returns [Course]
def index
respond_to do |format|
format.html {
@ -119,6 +154,7 @@ class CoursesController < ApplicationController
# @argument course[sis_course_id] [String] [optional] The unique SIS identifier.
# @argument offer [Boolean] [optional] If this option is set to true, the course will be available to students immediately.
#
# @returns Course
def create
@account = Account.find(params[:account_id])
if authorized_action(@account, @current_user, :manage_courses)
@ -247,8 +283,7 @@ class CoursesController < ApplicationController
end
# @API List users
# Returns the list of users in this course. And optionally the user's enrollments
# in the course.
# Returns the list of users in this course. And optionally the user's enrollments in the course.
#
# @argument enrollment_type [optional, "teacher"|"student"|"ta"|"observer"|"designer"]
# When set, only return users where the user is enrolled as this type.
@ -269,10 +304,22 @@ class CoursesController < ApplicationController
# See the API documentation for enrollment data returned; however, the user data is not included.
#
# @example_response
# [ { 'id': 1, 'name': 'first user', 'sis_user_id': null, 'sis_login_id': null,
# 'enrollments': [ ... ] },
# { 'id': 2, 'name': 'second user', 'sis_user_id': 'from-sis', 'sis_login_id': 'login-from-sis',
# 'enrollments': [ ... ] }]
# [
# {
# 'id': 1,
# 'name': 'first user',
# 'sis_user_id': null,
# 'sis_login_id': null,
# 'enrollments': [ ... ],
# },
# {
# 'id': 2,
# 'name': 'second user',
# 'sis_user_id': 'from-sis',
# 'sis_login_id': 'login-from-sis',
# 'enrollments': [ ... ],
# }
# ]
def users
get_context
if authorized_action(@context, @current_user, :read_roster)
@ -296,6 +343,40 @@ class CoursesController < ApplicationController
end
end
# @API List recently logged in students
#
# Returns the list of users in this course, including a 'last_login' field
# which contains a timestamp of the last time that user logged into canvas.
# The querying user must have the 'View usage reports' permission.
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/<course_id>/recent_users
#
# @example_response
# [
# {
# 'id': 1,
# 'name': 'first user',
# 'sis_user_id': null,
# 'sis_login_id': null,
# 'last_login': <timestamp>,
# },
# ...
# ]
def recent_students
get_context
if authorized_action(@context, @current_user, :read_reports)
scope = User.for_course_with_last_login(@context, @context.root_account_id, 'StudentEnrollment')
scope = scope.scoped(:order => 'login_info_exists, last_login DESC')
users = Api.paginate(scope, self, api_v1_course_recent_students_url)
if user_json_is_admin?
User.send(:preload_associations, users, :pseudonyms)
end
render :json => users.map { |u| user_json(u, @current_user, session, ['last_login']) }
end
end
# @API
# Return information on a single user.
#
@ -314,7 +395,7 @@ class CoursesController < ApplicationController
User.send(:preload_associations, users, :not_ended_enrollments,
:conditions => ['enrollments.course_id = ?', @context.id])
end
user = users.first
user = users.first or raise ActiveRecord::RecordNotFound
enrollments = user.not_ended_enrollments if includes.include?('enrollments')
render :json => user_json(user, @current_user, session, includes, @context, enrollments)
end
@ -382,14 +463,14 @@ class CoursesController < ApplicationController
@student_ids = @context.students.map &:id
@range_start = Date.parse("Jan 1 2000")
@range_end = Date.tomorrow
query = "SELECT COUNT(id), SUM(size) FROM attachments WHERE context_id=%s AND context_type='Course' AND root_attachment_id IS NULL AND file_state != 'deleted'"
row = Attachment.connection.select_rows(query % [@context.id]).first
@file_count, @files_size = [row[0].to_i, row[1].to_i]
query = "SELECT COUNT(id), SUM(max_size) FROM media_objects WHERE context_id=%s AND context_type='Course' AND attachment_id IS NULL AND workflow_state != 'deleted'"
row = MediaObject.connection.select_rows(query % [@context.id]).first
@media_file_count, @media_files_size = [row[0].to_i, row[1].to_i]
if params[:range] && params[:date]
date = Date.parse(params[:date]) rescue nil
date ||= Time.zone.today
@ -406,10 +487,11 @@ class CoursesController < ApplicationController
end
end
@recently_logged_students = @context.students.recently_logged_in
respond_to do |format|
format.html
format.json{ render :json => @categories.to_json }
format.html do
js_env(:RECENT_STUDENTS_URL => api_v1_course_recent_students_url(@context))
end
format.json { render :json => @categories.to_json }
end
end
end
@ -417,7 +499,7 @@ class CoursesController < ApplicationController
def settings
get_context
if authorized_action(@context, @current_user, :read_as_admin)
load_all_contexts
load_all_contexts(@context)
users_scope = @context.users_visible_to(@current_user)
enrollment_counts = users_scope.count(:distinct => true, :group => 'enrollments.type', :select => 'users.id')
@user_counts = {
@ -436,8 +518,8 @@ class CoursesController < ApplicationController
:CONTEXTS => @contexts,
:USER_PARAMS => {:include => ['email', 'enrollments', 'locked']},
:PERMISSIONS => {
:manage_students => (@context.grants_right?(@current_user, session, :manage_students) ||
@context.grants_right?(@current_user, session, :manage_admin_users)),
:manage_students => @context.grants_right?(@current_user, session, :manage_students),
:manage_admin_users => @context.grants_right?(@current_user, session, :manage_admin_users),
:manage_account_settings => @context.account.grants_right?(@current_user, session, :manage_account_settings),
})
@ -710,9 +792,11 @@ class CoursesController < ApplicationController
#
# Accepts the same include[] parameters as the list action, and returns a
# single course with the same fields as that action.
#
# @returns Course
def show
if api_request?
@context = api_find(Course, params[:id])
@context = api_find(Course.active, params[:id])
if authorized_action(@context, @current_user, :read)
enrollments = @context.current_enrollments.all(:conditions => { :user_id => @current_user.id })
includes = Set.new(Array(params[:include]))
@ -721,14 +805,10 @@ class CoursesController < ApplicationController
return
end
@context = Course.find(params[:id])
@context = Course.active.find(params[:id])
if request.xhr?
if authorized_action(@context, @current_user, [:read, :read_as_admin])
if is_authorized_action?(@context, @current_user, [:manage_students, :manage_admin_users])
render :json => @context.to_json(:include => {:current_enrollments => {:methods => :email}})
else
render :json => @context.to_json
end
render :json => @context.to_json
end
return
end
@ -1027,6 +1107,7 @@ class CoursesController < ApplicationController
def update
@course = api_find(Course, params[:id])
if authorized_action(@course, @current_user, :update)
params[:course] ||= {}
root_account_id = params[:course].delete :root_account_id
if root_account_id && Account.site_admin.grants_right?(@current_user, session, :manage_courses)
@course.root_account = Account.root_accounts.find(root_account_id)
@ -1066,9 +1147,9 @@ class CoursesController < ApplicationController
end
end
end
params[:course][:event] = :offer if params[:offer].present?
@course.process_event(params[:course].delete(:event)) if params[:course][:event] && @course.grants_right?(@current_user, session, :change_course_state)
params[:course][:conclude_at] = params[:course].delete(:end_at) if api_request?
params[:course][:event] = :offer if params[:offer].present?
@course.process_event(params[:course].delete(:event)) if params[:course][:event] && @course.grants_right?(@current_user, session, :change_course_state)
params[:course][:conclude_at] = params[:course].delete(:end_at) if api_request? && params[:course].has_key?(:end_at)
respond_to do |format|
@default_wiki_editing_roles_was = @course.default_wiki_editing_roles
if @course.update_attributes(params[:course])

View File

@ -60,6 +60,9 @@
# // The datetime to publish the topic (if not right away).
# "delayed_post_at":null,
#
# // whether or not this is locked for students to see.
# "locked":false,
#
# // The username of the topic creator.
# "user_name":"User Name",
#
@ -112,49 +115,52 @@ class DiscussionTopicsController < ApplicationController
# curl https://<canvas>/api/v1/courses/<course_id>/discussion_topics \
# -H 'Authorization: Bearer <token>'
def index
@context.assert_assignment_group rescue nil
@all_topics = @context.discussion_topics.active
@all_topics = @all_topics.only_discussion_topics if params[:include_announcements] != "1"
@topics = Api.paginate(@all_topics, self, topic_pagination_path)
@topics.reject! { |a| a.locked_for?(@current_user, :check_policies => true) }
@topics.each { |t| t.current_user = @current_user }
if authorized_action(@context.discussion_topics.new, @current_user, :read)
return child_topic if params[:root_discussion_topic_id] && @context.respond_to?(:context) && @context.context && @context.context.discussion_topics.find(params[:root_discussion_topic_id])
log_asset_access("topics:#{@context.asset_string}", "topics", 'other')
respond_to do |format|
format.html
format.html do
js_env :permissions => {
:create => @context.discussion_topics.new.grants_right?(@current_user, session, :create),
:moderate => @context.grants_right?(@current_user, session, :moderate_forum)
}
end
format.json do
# you can pass ?only_announcements=true to get announcements instead of discussions TODO: document
@topics = Api.paginate(@context.send( params[:only_announcements] ? :announcements : :discussion_topics).active, self, topic_pagination_path)
@topics.reject! { |a| a.locked_for?(@current_user, :check_policies => true) }
@topics.each { |t| t.current_user = @current_user }
if api_request?
render :json => discussion_topics_api_json(@topics, @context, @current_user, session)
else
render :json => @topics.to_json(:methods => [:user_name, :discussion_subentry_count, :read_state, :unread_count, :threaded], :permissions => {:user => @current_user, :session => session }, :include => [:assignment,:attachment])
end
end
end
end
end
def reorder
if authorized_action(@context, @current_user, :moderate_forum)
@topics = @context.discussion_topics
@topics.first.update_order(params[:order].split(",").map{|id| id.to_i}.reverse) unless @topics.empty?
flash[:notice] = t :reordered_topics_notice, "Topics successfully reordered"
redirect_to named_context_url(@context, :context_discussion_topics_url)
end
def new
@topic = @context.send(params[:is_announcement] ? :announcements : :discussion_topics).new
add_crumb t :create_new_crumb, "Create new"
edit
end
def child_topic
extra_params = {:headless => 1} if params[:headless]
@root_topic = @context.context.discussion_topics.find(params[:root_discussion_topic_id])
@topic = @context.discussion_topics.find_or_initialize_by_root_topic_id(params[:root_discussion_topic_id])
@topic.message = @root_topic.message
@topic.title = @root_topic.title
@topic.assignment_id = @root_topic.assignment_id
@topic.user_id = @root_topic.user_id
@topic.save
redirect_to named_context_url(@context, :context_discussion_topic_url, @topic.id, extra_params)
def edit
@topic ||= @context.all_discussion_topics.find(params[:id])
if authorized_action(@topic, @current_user, (@topic.new_record? ? :create : :update))
hash = {
:URL_ROOT => named_context_url(@context, :api_v1_context_discussion_topics_url)
}
unless @topic.new_record?
add_crumb(@topic.title, named_context_url(@context, :context_discussion_topic_url, @topic.id))
add_crumb t :edit_crumb, "Edit"
hash[:ATTRIBUTES] = discussion_topic_api_json(@topic, @context, @current_user, session)
end
(hash[:ATTRIBUTES] ||= {})[:is_announcement] = @topic.is_announcement
js_env :DISCUSSION_TOPIC => hash
render :action => "edit"
end
end
protected :child_topic
def show
parent_id = params[:parent_id]
@ -224,42 +230,6 @@ class DiscussionTopicsController < ApplicationController
end
end
def permissions
if authorized_action(@context, @current_user, :read)
@topic = @context.discussion_topics.find(params[:discussion_topic_id])
@entries = @topic.discussion_entries.active
@entries.each{|e| e.discussion_topic = @topic }
render :json => @entries.to_json(:only => [:id], :permissions => {:user => @current_user, :session => session})
end
end
def generate_assignment(assignment)
if assignment[:set_assignment] && assignment[:set_assignment] != '1'
params[:discussion_topic][:assignment] = nil
if @topic && @topic.assignment
@topic.update_attribute(:assignment_id, nil)
@topic.assignment.saved_by = :discussion_topic
@topic.assignment.destroy
end
return
end
@assignment = @topic.assignment if @topic
@assignment ||= @topic.restore_old_assignment if @topic
@assignment ||= @context.assignments.build
@assignment.submission_types = 'discussion_topic'
@context.assert_assignment_group
@assignment.assignment_group_id = assignment[:assignment_group_id] || @assignment.assignment_group_id || @context.assignment_groups.first.id
@assignment.title = params[:discussion_topic][:title]
@assignment.points_possible = assignment[:points_possible] || @assignment.points_possible
@assignment.due_at = assignment[:due_at] || @assignment.due_at
# if no due_at was given, set it to 11:59 pm in the creator's time zone
@assignment.infer_due_at
@assignment.saved_by = :discussion_topic
@assignment.save
params[:discussion_topic][:assignment] = @assignment
end
protected :generate_assignment
# @API Create a new discussion topic
#
# Create an new discussion topic for the course or group.
@ -275,10 +245,11 @@ class DiscussionTopicsController < ApplicationController
#
# @argument require_initial_post If true then a user may not respond to other replies until that user has made an initial reply. Defaults to false.
#
# @argument assignment To create an assignment discussion, pass the assignment parameters as a sub-object. See the {api:AssignmentsApiController#create Create an Assignment API} for the available parameters. The name parameter will be ignored, as it's taken from the discussion title.
# @argument assignment To create an assignment discussion, pass the assignment parameters as a sub-object. See the {api:AssignmentsApiController#create Create an Assignment API} for the available parameters. The name parameter will be ignored, as it's taken from the discussion title. If you want to make a discussion that was an assignment NOT an assignment, pass set_assignment = false as part of the assignment object
#
# @argument is_announcement If true, this topic is an announcement. It will appear in the announcements section rather than the discussions section. This requires announcment-posting permissions.
#
# @argument position_after By default, discusions are sorted chronologically by creation date, you can pass the id of another topic to have this one show up after the other when they are listed.
# @example_request
# curl https://<canvas>/api/v1/courses/<course_id>/discussion_topics \
# -F title='my topic' \
@ -294,123 +265,18 @@ class DiscussionTopicsController < ApplicationController
# -H 'Authorization: Bearer <token>'
#
def create
if api_request?
delay_posting = '1' if params[:delayed_post_at].present?
params[:podcast_enabled] = true if value_to_boolean(params[:podcast_has_student_posts])
params[:discussion_topic] = params.slice(:title, :message, :discussion_type, :delayed_post_at, :podcast_enabled, :podcast_has_student_posts, :require_initial_post, :is_announcement)
params[:discussion_topic][:assignment] = create_api_assignment(@context, params[:assignment])
else
params[:discussion_topic].delete(:remove_attachment)
delay_posting ||= params[:discussion_topic].delete(:delay_posting)
assignment = params[:discussion_topic].delete(:assignment)
generate_assignment(assignment) if assignment && assignment[:set_assignment]
end
unless @context.grants_right?(@current_user, session, :moderate_forum)
params[:discussion_topic].delete :podcast_enabled
params[:discussion_topic].delete :podcast_has_student_posts
end
if value_to_boolean(params[:discussion_topic].delete(:is_announcement)) && @context.announcements.new.grants_right?(@current_user, session, :create)
@topic = @context.announcements.build(params[:discussion_topic])
else
@topic = @context.discussion_topics.build(params[:discussion_topic])
end
@topic.workflow_state = 'post_delayed' if delay_posting == '1' && @topic.delayed_post_at && @topic.delayed_post_at > Time.now
@topic.delayed_post_at = "" unless @topic.post_delayed?
@topic.user = @current_user
@topic.current_user = @current_user
if authorized_action(@topic, @current_user, :create)
return if params[:attachment] && params[:attachment][:uploaded_data] &&
params[:attachment][:uploaded_data].size > 1.kilobytes &&
@topic.grants_right?(@current_user, session, :attach) &&
quota_exceeded(named_context_url(@context, :context_discussion_topics_url))
respond_to do |format|
@topic.content_being_saved_by(@current_user)
if @topic.save
@topic.insert_at_bottom
log_asset_access(@topic, 'topics', 'topics', 'participate')
if params[:attachment] && params[:attachment][:uploaded_data] && params[:attachment][:uploaded_data].size > 0 && @topic.grants_right?(@current_user, session, :attach)
@attachment = @context.attachments.create(params[:attachment])
@topic.attachment = @attachment
@topic.save
end
format.html do
flash[:notice] = t :topic_created_notice, 'Topic was successfully created.'
redirect_to named_context_url(@context, :context_discussion_topic_url, @topic)
end
format.json do
if api_request?
render :json => discussion_topics_api_json([@topic], @context, @current_user, session).first
else
render :json => @topic.to_json(:include => [:assignment,:attachment], :methods => [:user_name, :read_state, :unread_count], :permissions => {:user => @current_user, :session => session}), :status => :created
end
end
format.text { render :json => @topic.to_json(:include => [:assignment,:attachment], :methods => [:user_name, :read_state, :unread_count], :permissions => {:user => @current_user, :session => session}), :status => :created }
else
format.html { render :action => "new" }
format.json { render :json => @topic.errors.to_json, :status => :bad_request }
format.text { render :json => @topic.errors.to_json, :status => :bad_request }
end
end
end
process_discussion_topic(!!:is_new)
end
# @API Update a topic, accepts the same parameters as create
# @example_request
# curl https://<canvas>/api/v1/courses/<course_id>/discussion_topics/<topic_id> \
# -F title='This will be positioned after Topic #1234' \
# -F position_after=1234 \
# -H 'Authorization: Bearer <token>'
#
def update
params[:discussion_topic].delete(:is_announcement)
remove_attachment = (params[:discussion_topic] || {}).delete :remove_attachment
@topic = @context.all_discussion_topics.find(params[:id])
@topic.attachment_id = nil if remove_attachment == '1'
if authorized_action(@topic, @current_user, :update)
assignment = params[:discussion_topic].delete(:assignment)
generate_assignment(assignment) if assignment && assignment[:set_assignment]
if params[:discussion_topic][:lock]
@topic.workflow_state = (params[:discussion_topic][:lock] == '1') ? 'locked' : 'active'
params[:discussion_topic].delete :lock
end
unless @context.grants_right?(@current_user, session, :moderate_forum)
params[:discussion_topic].delete :podcast_enabled
params[:discussion_topic].delete :podcast_has_student_posts
params[:discussion_topic].delete :event
end
delay_posting = params[:discussion_topic].delete :delay_posting
delayed_post_at = params[:discussion_topic].delete :delayed_post_at
delayed_post_at = Time.zone.parse(delayed_post_at) if delayed_post_at
@topic.workflow_state = (delay_posting == '1' && delayed_post_at > Time.now ? 'post_delayed' : @topic.workflow_state)
@topic.workflow_state = 'active' if @topic.post_delayed? && (!delayed_post_at || delay_posting != '1')
@topic.delayed_post_at = @topic.post_delayed? ? delayed_post_at : nil
@topic.current_user = @current_user
return if params[:attachment] && params[:attachment][:uploaded_data] &&
params[:attachment][:uploaded_data].size > 1.kilobytes &&
@topic.grants_right?(@current_user, session, :attach) &&
quota_exceeded(named_context_url(@context, :context_discussion_topics_url))
@topic.process_event(params[:discussion_topic].delete(:event)) if params[:discussion_topic][:event]
respond_to do |format|
@topic.content_being_saved_by(@current_user)
@topic.editor = @current_user
if @topic.update_attributes(params[:discussion_topic])
@topic.context_module_action(@current_user, :contributed) if !@locked
if params[:attachment] && params[:attachment][:uploaded_data] && params[:attachment][:uploaded_data].size > 0 && @topic.grants_right?(@current_user, session, :attach)
@attachment = @context.attachments.create(params[:attachment])
@topic.attachment = @attachment
@topic.save
end
flash[:notice] = t :topic_updated_notice, 'Topic was successfully updated.'
format.html { redirect_to named_context_url(@context, :context_discussion_topic_url, @topic) }
format.json { render :json => @topic.to_json(:include => [:assignment, :attachment], :methods => [:user_name, :read_state, :unread_count], :permissions => {:user => @current_user, :session => session}), :status => :ok }
format.text { render :json => @topic.to_json(:include => [:assignment, :attachment], :methods => [:user_name, :read_state, :unread_count], :permissions => {:user => @current_user, :session => session}), :status => :ok }
else
format.html { render :action => "edit" }
format.json { render :json => @topic.errors.to_json, :status => :bad_request }
format.text { render :json => @topic.errors.to_json, :status => :bad_request }
end
end
end
process_discussion_topic(!:is_new)
end
# @API Delete a topic
@ -457,4 +323,112 @@ class DiscussionTopicsController < ApplicationController
def public_topic_feed
end
protected
API_ALLOWED_TOPIC_FIELDS = %w(title message discussion_type delayed_post_at podcast_enabled
podcast_has_student_posts require_initial_post is_announcement)
def process_discussion_topic(is_new = false)
discussion_topic_hash = params.slice(*API_ALLOWED_TOPIC_FIELDS)
model_type = value_to_boolean(discussion_topic_hash.delete(:is_announcement)) && @context.announcements.new.grants_right?(@current_user, session, :create) ? :announcements : :discussion_topics
if is_new
@topic = @context.send(model_type).build
else
@topic = @context.send(model_type).active.find(params[:id] || params[:topic_id])
end
if authorized_action(@topic, @current_user, (is_new ? :create : :update))
discussion_topic_hash[:podcast_enabled] = true if value_to_boolean(discussion_topic_hash[:podcast_has_student_posts])
unless @context.grants_right?(@current_user, session, :moderate_forum)
discussion_topic_hash.delete :podcast_enabled
discussion_topic_hash.delete :podcast_has_student_posts
end
@topic.send(is_new ? :user= : :editor=, @current_user)
@topic.current_user = @current_user
@topic.content_being_saved_by(@current_user)
# handle delayed posting
@topic.delayed_post_at = discussion_topic_hash[:delayed_post_at]
@topic.workflow_state = 'post_delayed' if @topic.delayed_post_at && @topic.delayed_post_at > Time.now
@topic.delayed_post_at = "" unless @topic.post_delayed?
#handle locking/unlocking
if params.has_key? :locked
if value_to_boolean(params[:locked])
@topic.lock
else
@topic.unlock
end
end
if @topic.update_attributes(discussion_topic_hash)
log_asset_access(@topic, 'topics', 'topics', 'participate')
# handle sort positioning
if params[:position_after] && @context.grants_right?(@current_user, session, :moderate_forum)
other_topic = @context.discussion_topics.active.find(params[:position_after])
@topic.insert_at(other_topic.position)
end
# handle creating/removing attachment
if @topic.grants_right?(@current_user, session, :attach)
attachment = params[:attachment] &&
params[:attachment].size > 0 &&
params[:attachment]
return if attachment && attachment.size > 1.kilobytes &&
quota_exceeded(named_context_url(@context, :context_discussion_topics_url))
if (params.has_key?(:remove_attachment) || attachment) && @topic.attachment
@topic.attachment.destroy!
end
if attachment
@attachment = @context.attachments.create!(:uploaded_data => attachment)
@topic.attachment = @attachment
@topic.save
end
end
# handle creating/deleting assignment
if params[:assignment] &&
(@assignment = @topic.assignment || @topic.restore_old_assignment || (@topic.assignment = @context.assignments.build)) &&
@assignment.grants_right?(@current_user, session, :update)
if params[:assignment].has_key?(:set_assignment) && !value_to_boolean(params[:assignment][:set_assignment])
if @topic.assignment
@topic.assignment.destroy
@topic.update_attribute(:assignment_id, nil)
end
else
update_api_assignment(@assignment, params[:assignment].merge(@topic.attributes.slice('title')))
@assignment.submission_types = 'discussion_topic'
@assignment.saved_by = :discussion_topic
@topic.assignment = @assignment
@topic.save!
end
end
render :json => discussion_topic_api_json(@topic, @context, @current_user, session)
else
render :json => @topic.errors.to_json, :status => :bad_request
end
end
end
def child_topic
extra_params = {:headless => 1} if params[:headless]
@root_topic = @context.context.discussion_topics.find(params[:root_discussion_topic_id])
@topic = @context.discussion_topics.find_or_initialize_by_root_topic_id(params[:root_discussion_topic_id])
@topic.message = @root_topic.message
@topic.title = @root_topic.title
@topic.assignment_id = @root_topic.assignment_id
@topic.user_id = @root_topic.user_id
@topic.save
redirect_to named_context_url(@context, :context_discussion_topic_url, @topic.id, extra_params)
end
end

View File

@ -245,7 +245,7 @@ class EnrollmentsApiController < ApplicationController
#
# Returns an ActiveRecord scope of enrollments on success, false on failure.
def course_index_enrollments(scope_arguments)
if authorized_action(@context, @current_user, :read_roster)
if authorized_action(@context, @current_user, [:read_roster, :view_all_grades])
scope = @context.enrollments_visible_to(@current_user, :type => :all, :include_priors => true).scoped(scope_arguments)
unless scope_arguments[:conditions].include?(:workflow_state)
scope = scope.scoped(:conditions => ['enrollments.workflow_state NOT IN (?)', ['rejected', 'completed', 'deleted', 'inactive']])
@ -314,7 +314,7 @@ class EnrollmentsApiController < ApplicationController
if state.present?
state.map(&:to_sym).each do |s|
conditions[0] << User::ENROLLMENT_CONDITIONS[s]
conditions[0] << User.enrollment_conditions(s)
end
end

View File

@ -26,7 +26,7 @@ class ErrorsController < ApplicationController
def index
params[:page] = params[:page].to_i > 0 ? params[:page].to_i : 1
@reports = ErrorReport
@reports = ErrorReport.scoped(:include => :user)
@message = params[:message]
if @message.present?

View File

@ -0,0 +1,63 @@
class ExternalFeedsController < ApplicationController
include Api::V1::ExternalFeeds
before_filter :require_context, :except => :public_feed
# @API List external feeds
#
# Returns the list of External Feeds this course or group.
#
# @example_request
# curl https://<canvas>/api/v1/courses/<course_id>/external_feeds \
# -H 'Authorization: Bearer <token>'
#
def index
if authorized_action(@context, @current_user, :read)
render :json => external_feeds_api_json(@context.external_feeds.for('announcements'), @context, @current_user, session)
end
end
# @API Create a new external feed
#
# Create a new external feed for the course or group.
#
# @argument url
# @argument header_match you can only include posts that have a specific phrase in title by passing the phrase here.
# @argument verbosity options are: full, truncate, or link_only
# @example_request
# curl https://<canvas>/api/v1/courses/<course_id>/external_feeds \
# -F title='http://example.com/rss.xml' \
# -F header_match='news flash!' \
# -F verbosity='full' \
# -H 'Authorization: Bearer <token>'
#
def create
if authorized_action(@context.announcements.new, @current_user, :create)
@feed = create_api_external_feed(@context, params, @current_user)
if @feed.save
render :json => external_feed_api_json(@feed, @context, @current_user, session)
else
render :json => @feed.errors.to_json, :response => :bad_request
end
end
end
# @API Delete an external feed
#
# Deletes the external feed.
#
# @example_request
# curl -X DELETE https://<canvas>/api/v1/courses/<course_id>/external_feeds/<feed_id> \
# -H 'Authorization: Bearer <token>'
def destroy
if authorized_action(@context.announcements.new, @current_user, :create)
@feed = @context.external_feeds.find(params[:external_feed_id])
if @feed.destroy
render :json => external_feed_api_json(@feed, @context, @current_user, session)
else
render :json => @feed.errors.to_json, :response => :bad_request
end
end
end
end

View File

@ -36,18 +36,21 @@ class FavoritesController < ApplicationController
# Retrieve the list of favorite courses for the current user. If the user has not chosen
# any favorites, then a selection of currently enrolled courses will be returned.
#
# See the {api:CoursesController#index List courses API} for details on accepted include[] parameters.
#
# @returns [Course]
#
# @example_request
# curl https://<canvas>/api/v1/users/self/favorites/courses \
# -H 'Authorization: Bearer <ACCESS_TOKEN>'
#
# @example_response
# [{"id":1170, "course_code":"HIST 411", "name":"Ancient History", "primary_enrollment":"StudentEnrollment", ...},
# {"id":1337, "course_code":"PHYS 301", "name":"Modern Physics", "primary_enrollment":"StudentEnrollment", ...}]
#
def list_favorite_courses
includes = Set.new(Array(params[:include]))
courses = Api.paginate(@current_user.menu_courses, self, '/api/v1/users/self/favorite/courses')
render :json => courses.map { |course|
course_json(course, @current_user, session, ['primary_enrollment'], nil) }
enrollments = course.current_enrollments.all(:conditions => { :user_id => @current_user.id })
course_json(course, @current_user, session, includes, enrollments)
}
end
# @API Add course to favorites

View File

@ -26,11 +26,19 @@
# "content-type":"text/plain",
# "url":"http://www.example.com/files/569/download?download_frd=1\u0026verifier=c6HdZmxOZa0Fiin2cbvZeI8I5ry7yqD7RChQzb6P",
# "id":569,
# "display_name":"file.txt"
# "display_name":"file.txt",
# "created_at':"2012-07-06T14:58:50Z",
# "updated_at':"2012-07-06T14:58:50Z",
# "unlock_at':null,
# "locked':false,
# "hidden':false,
# "lock_at':null,
# "locked_for_user":false,
# "hidden_for_user":false
# }
class FilesController < ApplicationController
before_filter :require_user, :only => :create_pending
before_filter :require_context, :except => [:public_feed,:full_index,:assessment_question_show,:image_thumbnail,:show_thumbnail,:preflight,:create_pending,:s3_success,:show,:api_create,:api_create_success,:api_show,:api_index,:destroy,:api_update]
before_filter :require_context, :except => [:public_feed,:full_index,:assessment_question_show,:image_thumbnail,:show_thumbnail,:preflight,:create_pending,:s3_success,:show,:api_create,:api_create_success,:api_show,:api_index,:destroy,:api_update,:api_file_status]
before_filter :check_file_access_flags, :only => [:show_relative, :show]
prepend_around_filter :load_pseudonym_from_policy, :only => :create
skip_before_filter :verify_authenticity_token, :only => :api_create
@ -101,18 +109,11 @@ class FilesController < ApplicationController
# @API List files
# Returns the paginated list of files for the folder.
#
# @argument sort_by Either 'alphabetical' (default) or 'position'
#
# @example_request
#
# curl 'https://<canvas>/api/v1/folders/<folder_id>/files' \
# -H 'Authorization: Bearer <token>'
#
# @example_request
#
# curl 'https://<canvas>/api/v1/folders/<folder_id>/files?sort_by=position' \
# -H 'Authorization: Bearer <token>'
#
# @returns [File]
def api_index
folder = Folder.find(params[:id])
@ -129,7 +130,8 @@ class FilesController < ApplicationController
scope = scope.by_display_name
end
@files = Api.paginate(scope, self, api_v1_list_files_url(@folder))
render :json => attachments_json(@files)
can_manage_files = @context.grants_right?(@current_user, session, :manage_files)
render :json => attachments_json(@files, @current_user, {}, :can_manage_files => can_manage_files)
end
end
@ -221,7 +223,7 @@ class FilesController < ApplicationController
@attachment = Attachment.find(params[:id])
raise ActiveRecord::RecordNotFound if @attachment.deleted?
if authorized_action(@attachment,@current_user,:read)
render :json => attachment_json(@attachment)
render :json => attachment_json(@attachment, @current_user)
end
end
@ -306,8 +308,6 @@ class FilesController < ApplicationController
)
options[:methods] = :authenticated_s3_url if service_enabled?(:google_docs_previews) && attachment.authenticated_s3_url
log_asset_access(@attachment, "files", "files")
else
@attachment.scribd_doc = nil
end
end
format.json { render :json => @attachment.to_json(options) }
@ -561,7 +561,18 @@ class FilesController < ApplicationController
@attachment.save!
end
@attachment.handle_duplicates(duplicate_handling)
render :json => attachment_json(@attachment)
render :json => attachment_json(@attachment, @current_user)
end
def api_file_status
@attachment = Attachment.find_by_id_and_uuid!(params[:id], params[:uuid])
if @attachment.file_state == 'available'
render :json => { :upload_status => 'ready', :attachment => attachment_json(@attachment, @current_user) }
elsif @attachment.file_state == 'deleted'
render :json => { :upload_status => 'pending' }
else
render :json => { :upload_status => 'errored', :message => @attachment.upload_error_message }
end
end
def create
@ -723,7 +734,7 @@ class FilesController < ApplicationController
@attachment.attributes = process_attachment_params(params)
if @attachment.save
render :json => attachment_json(@attachment)
render :json => attachment_json(@attachment, @current_user)
else
render :json => @attachment.errors.to_json, :status => :bad_request
end
@ -757,7 +768,7 @@ class FilesController < ApplicationController
redirect_to named_context_url(@context, :context_files_url)
}
if api_request?
format.json { render :json => attachment_json(@attachment) }
format.json { render :json => attachment_json(@attachment, @current_user) }
else
format.json { render :json => @attachment.to_json }
end

View File

@ -23,7 +23,6 @@
# {
# "context_type":"Course",
# "context_id":1401,
# "locked":null,
# "files_count":0,
# "position":3,
# "updated_at":"2012-07-06T14:58:50Z",
@ -37,6 +36,10 @@
# "parent_folder_id":2934,
# "created_at":"2012-07-06T14:58:50Z",
# "unlock_at":null
# "hidden":null
# "hidden_for_user":false,
# "locked":true,
# "locked_for_user":false
# }
class FoldersController < ApplicationController
include Api::V1::Folders
@ -55,18 +58,11 @@ class FoldersController < ApplicationController
# @subtopic Folders
# Returns the paginated list of folders in the folder.
#
# @argument sort_by Either 'alphabetical' (default) or 'position'
#
# @example_request
#
# curl 'https://<canvas>/api/v1/folders/<folder_id>/folders' \
# -H 'Authorization: Bearer <token>'
#
# @example_request
#
# curl 'https://<canvas>/api/v1/folders/<folder_id>/folders?sort_by=position' \
# -H 'Authorization: Bearer <token>'
#
# @returns [Folder]
def api_index
folder = Folder.find(params[:id])
@ -81,7 +77,8 @@ class FoldersController < ApplicationController
scope = scope.by_name
end
@folders = Api.paginate(scope, self, api_v1_list_folders_url(@context))
render :json => folders_json(@folders, @current_user, session)
can_manage_files = folder.context.grants_right?(@current_user, session, :manage_files)
render :json => folders_json(@folders, @current_user, session, :can_manage_files => can_manage_files)
end
end

Some files were not shown because too many files have changed in this diff Show More