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:
commit
8fa01db612
|
@ -69,3 +69,4 @@ app/coffeescripts/plugins/
|
|||
app/views/jst/plugins/
|
||||
app/stylesheets/plugins/
|
||||
spec/coffeescripts/plugins/
|
||||
branch_tools.rb
|
||||
|
|
23
.jshintrc
23
.jshintrc
|
@ -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
12
Gemfile
|
@ -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|
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}"
|
|
@ -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
|
||||
|
|
|
@ -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')
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,2 +1,2 @@
|
|||
require ['full_files', 'uploadify']
|
||||
require ['full_files', 'use!uploadify']
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
require ['grade_summary', 'compiled/grade_calculator']
|
||||
require ['grade_summary']
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -1 +0,0 @@
|
|||
require ['topic']
|
|
@ -1,2 +0,0 @@
|
|||
require ['topics']
|
||||
|
|
@ -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) =>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
define [
|
||||
'Backbone'
|
||||
'compiled/models/AssignmentGroup'
|
||||
], (Backbone, AssignmentGroup) ->
|
||||
|
||||
class AssignmentGroupCollection extends Backbone.Collection
|
||||
|
||||
model: AssignmentGroup
|
|
@ -0,0 +1,9 @@
|
|||
define [
|
||||
'Backbone'
|
||||
'compiled/models/DiscussionEntry'
|
||||
], (Backbone, DiscussionEntry) ->
|
||||
|
||||
class DiscussionEntryCollection extends Backbone.Collection
|
||||
|
||||
model: DiscussionEntry
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
define [
|
||||
'compiled/collections/PaginatedCollection'
|
||||
'compiled/models/DiscussionTopic'
|
||||
], (PaginatedCollection, DiscussionTopic) ->
|
||||
|
||||
class DiscussionTopicsCollection extends PaginatedCollection
|
||||
|
||||
model: DiscussionTopic
|
|
@ -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')
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
define [
|
||||
'Backbone'
|
||||
'compiled/models/Entry'
|
||||
], (Backbone, Entry) ->
|
||||
|
||||
##
|
||||
# Collection for Entries
|
||||
class EntryCollection extends Backbone.Collection
|
||||
|
||||
model: Entry
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
define [
|
||||
'Backbone'
|
||||
'compiled/models/ExternalFeed'
|
||||
'compiled/str/splitAssetString'
|
||||
], (Backbone, ExternalFeed, splitAssetString) ->
|
||||
|
||||
class ExternalFeedCollection extends Backbone.Collection
|
||||
|
||||
model: ExternalFeed
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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', () ->
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
##
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
define ->
|
||||
preventDefault = (fn) ->
|
||||
(event) ->
|
||||
event.preventDefault()
|
||||
event?.preventDefault()
|
||||
fn.apply(this, arguments)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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...
|
||||
$
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
define [
|
||||
'Backbone'
|
||||
], (Backbone) ->
|
||||
|
||||
class AssignmentGroup extends Backbone.Model
|
||||
resourceName: 'assignment_groups'
|
|
@ -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
|
||||
|
|
@ -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')
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
define [
|
||||
'Backbone'
|
||||
], (Backbone) ->
|
||||
|
||||
class ExternalFeed extends Backbone.Model
|
||||
resourceName: 'external_feeds'
|
|
@ -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) =>
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -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) =>
|
||||
|
|
|
@ -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®istration_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: [
|
||||
|
|
|
@ -71,7 +71,7 @@ define [
|
|||
item.list = @list
|
||||
item
|
||||
|
||||
if data.length
|
||||
if data.length?
|
||||
(modelize(item) for item in data)
|
||||
else
|
||||
modelize(data)
|
||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
define [
|
||||
'Backbone',
|
||||
'Backbone'
|
||||
'i18n!dashboard'
|
||||
'underscore'
|
||||
'jst/quickStartBar/QuickStartBarView'
|
||||
'formToJSON'
|
||||
'jquery.toJSON'
|
||||
], ({View, Model}, I18n, _, template) ->
|
||||
|
||||
capitalize = (str) ->
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 assignment’s 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])
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue