Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
problem-builder
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
problem-builder
Commits
f5953e35
Commit
f5953e35
authored
Jul 10, 2015
by
Jonathan Piacenti
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Allow users to share table blocks.
parent
bee4e702
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
615 additions
and
43 deletions
+615
-43
problem_builder/answer.py
+20
-2
problem_builder/migrations/0003_auto__add_share__add_unique_share_shared_by_shared_with_block_id.py
+112
-0
problem_builder/models.py
+17
-0
problem_builder/public/css/mentoring-table.css
+82
-0
problem_builder/public/js/review_blocks.js
+148
-5
problem_builder/table.py
+153
-8
problem_builder/templates/html/mentoring-table-container.html
+54
-0
problem_builder/templates/html/mentoring-table-shared-list.html
+10
-0
problem_builder/templates/html/mentoring-table.html
+19
-28
No files found.
problem_builder/answer.py
View file @
f5953e35
...
@@ -203,7 +203,10 @@ class AnswerBlock(SubmittingXBlockMixin, AnswerMixin, StepMixin, StudioEditableX
...
@@ -203,7 +203,10 @@ class AnswerBlock(SubmittingXBlockMixin, AnswerMixin, StepMixin, StudioEditableX
if
sub_api
:
if
sub_api
:
# Also send to the submissions API:
# Also send to the submissions API:
sub_api
.
create_submission
(
self
.
student_item_key
,
self
.
student_input
)
item_key
=
self
.
student_item_key
# Need to do this by our own ID, since an answer can be referred to multiple times.
item_key
[
'item_id'
]
=
self
.
name
sub_api
.
create_submission
(
item_key
,
self
.
student_input
)
log
.
info
(
u'Answer submitted for`{}`: "{}"'
.
format
(
self
.
name
,
self
.
student_input
))
log
.
info
(
u'Answer submitted for`{}`: "{}"'
.
format
(
self
.
name
,
self
.
student_input
))
return
self
.
get_results
()
return
self
.
get_results
()
...
@@ -283,9 +286,24 @@ class AnswerRecapBlock(AnswerMixin, StudioEditableXBlockMixin, XBlock):
...
@@ -283,9 +286,24 @@ class AnswerRecapBlock(AnswerMixin, StudioEditableXBlockMixin, XBlock):
def
mentoring_view
(
self
,
context
=
None
):
def
mentoring_view
(
self
,
context
=
None
):
""" Render this XBlock within a mentoring block. """
""" Render this XBlock within a mentoring block. """
context
=
context
.
copy
()
if
context
else
{}
context
=
context
.
copy
()
if
context
else
{}
student_submissions_key
=
context
.
get
(
'student_submissions_key'
)
context
[
'title'
]
=
self
.
display_name
context
[
'title'
]
=
self
.
display_name
context
[
'description'
]
=
self
.
description
context
[
'description'
]
=
self
.
description
context
[
'student_input'
]
=
self
.
student_input
if
student_submissions_key
:
location
=
self
.
location
.
replace
(
branch
=
None
,
version
=
None
)
# Standardize the key in case it isn't already
target_key
=
{
'student_id'
:
student_submissions_key
,
'course_id'
:
unicode
(
location
.
course_key
),
'item_id'
:
self
.
name
,
'item_type'
:
u'pb-answer'
,
}
submissions
=
sub_api
.
get_submissions
(
target_key
,
limit
=
1
)
try
:
context
[
'student_input'
]
=
submissions
[
0
][
'answer'
]
except
IndexError
:
context
[
'student_input'
]
=
None
else
:
context
[
'student_input'
]
=
self
.
student_input
html
=
loader
.
render_template
(
'templates/html/answer_read_only.html'
,
context
)
html
=
loader
.
render_template
(
'templates/html/answer_read_only.html'
,
context
)
fragment
=
Fragment
(
html
)
fragment
=
Fragment
(
html
)
...
...
problem_builder/migrations/0003_auto__add_share__add_unique_share_shared_by_shared_with_block_id.py
0 → 100644
View file @
f5953e35
# -*- coding: utf-8 -*-
from
south.utils
import
datetime_utils
as
datetime
from
south.db
import
db
from
south.v2
import
SchemaMigration
from
django.db
import
models
class
Migration
(
SchemaMigration
):
def
forwards
(
self
,
orm
):
# Adding model 'Share'
db
.
create_table
(
'problem_builder_share'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
'shared_by'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
related_name
=
'problem_builder_shared_by'
,
to
=
orm
[
'auth.User'
]
)),
(
'submission_uid'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
32
)),
(
'block_id'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
255
,
db_index
=
True
)),
(
'shared_with'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
related_name
=
'problem_builder_shared_with'
,
to
=
orm
[
'auth.User'
]
)),
(
'notified'
,
self
.
gf
(
'django.db.models.fields.BooleanField'
)(
default
=
False
,
db_index
=
True
)),
))
db
.
send_create_signal
(
'problem_builder'
,
[
'Share'
])
# Adding unique constraint on 'Share', fields ['shared_by', 'shared_with', 'block_id']
db
.
create_unique
(
'problem_builder_share'
,
[
'shared_by_id'
,
'shared_with_id'
,
'block_id'
])
def
backwards
(
self
,
orm
):
# Removing unique constraint on 'Share', fields ['shared_by', 'shared_with', 'block_id']
db
.
delete_unique
(
'problem_builder_share'
,
[
'shared_by_id'
,
'shared_with_id'
,
'block_id'
])
# Deleting model 'Share'
db
.
delete_table
(
'problem_builder_share'
)
models
=
{
'auth.group'
:
{
'Meta'
:
{
'object_name'
:
'Group'
},
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'80'
}),
'permissions'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Permission']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
})
},
'auth.permission'
:
{
'Meta'
:
{
'ordering'
:
"('content_type__app_label', 'content_type__model', 'codename')"
,
'unique_together'
:
"(('content_type', 'codename'),)"
,
'object_name'
:
'Permission'
},
'codename'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'content_type'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['contenttypes.ContentType']"
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'50'
})
},
'auth.user'
:
{
'Meta'
:
{
'object_name'
:
'User'
},
'date_joined'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'email'
:
(
'django.db.models.fields.EmailField'
,
[],
{
'max_length'
:
'75'
,
'blank'
:
'True'
}),
'first_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'30'
,
'blank'
:
'True'
}),
'groups'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Group']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'is_active'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'True'
}),
'is_staff'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'is_superuser'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'last_login'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'last_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'30'
,
'blank'
:
'True'
}),
'password'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'128'
}),
'user_permissions'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Permission']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
}),
'username'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'30'
})
},
'contenttypes.contenttype'
:
{
'Meta'
:
{
'ordering'
:
"('name',)"
,
'unique_together'
:
"(('app_label', 'model'),)"
,
'object_name'
:
'ContentType'
,
'db_table'
:
"'django_content_type'"
},
'app_label'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'model'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
})
},
'problem_builder.answer'
:
{
'Meta'
:
{
'unique_together'
:
"(('student_id', 'course_id', 'name'),)"
,
'object_name'
:
'Answer'
},
'course_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'50'
,
'db_index'
:
'True'
}),
'created_on'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified_on'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now'
:
'True'
,
'blank'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'50'
,
'db_index'
:
'True'
}),
'student_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'32'
,
'db_index'
:
'True'
}),
'student_input'
:
(
'django.db.models.fields.TextField'
,
[],
{
'default'
:
"''"
,
'blank'
:
'True'
})
},
'problem_builder.share'
:
{
'Meta'
:
{
'unique_together'
:
"(('shared_by', 'shared_with', 'block_id'),)"
,
'object_name'
:
'Share'
},
'block_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'notified'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
,
'db_index'
:
'True'
}),
'shared_by'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'problem_builder_shared_by'"
,
'to'
:
"orm['auth.User']"
}),
'shared_with'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'problem_builder_shared_with'"
,
'to'
:
"orm['auth.User']"
}),
'submission_uid'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'32'
})
}
}
complete_apps
=
[
'problem_builder'
]
problem_builder/models.py
View file @
f5953e35
...
@@ -21,6 +21,7 @@
...
@@ -21,6 +21,7 @@
# Imports ###########################################################
# Imports ###########################################################
from
django.db
import
models
from
django.db
import
models
from
django.contrib.auth.models
import
User
# Classes ###########################################################
# Classes ###########################################################
...
@@ -47,3 +48,19 @@ class Answer(models.Model):
...
@@ -47,3 +48,19 @@ class Answer(models.Model):
# Force validation of max_length
# Force validation of max_length
self
.
full_clean
()
self
.
full_clean
()
super
(
Answer
,
self
)
.
save
(
*
args
,
**
kwargs
)
super
(
Answer
,
self
)
.
save
(
*
args
,
**
kwargs
)
class
Share
(
models
.
Model
):
"""
The XBlock User Service does not permit XBlocks instantiated with non-staff users
to query for arbitrary anonymous user IDs. In order to make sharing work, we have
to store them here.
"""
shared_by
=
models
.
ForeignKey
(
User
,
related_name
=
'problem_builder_shared_by'
)
submission_uid
=
models
.
CharField
(
max_length
=
32
)
block_id
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)
shared_with
=
models
.
ForeignKey
(
User
,
related_name
=
'problem_builder_shared_with'
)
notified
=
models
.
BooleanField
(
default
=
False
,
db_index
=
True
)
class
Meta
(
object
):
unique_together
=
((
'shared_by'
,
'shared_with'
,
'block_id'
),)
problem_builder/public/css/mentoring-table.css
View file @
f5953e35
...
@@ -52,3 +52,84 @@
...
@@ -52,3 +52,84 @@
position
:
absolute
;
position
:
absolute
;
width
:
1px
;
width
:
1px
;
}
}
.mentoring-table-container
.share-with-container
{
text-align
:
right
;
}
.share-with-instructions
{
max-width
:
14.5em
;
}
.mentoring-share-panel
{
float
:
right
;
}
.mentoring-share-panel
.mentoring-share-with
{
position
:
absolute
;
right
:
0
;
background-color
:
rgb
(
255
,
255
,
255
);
border
:
1px
solid
rgb
(
221
,
221
,
221
);
padding
:
1em
;
}
.mentoring-share-with
.share-header
{
text-align
:
left
;
}
.mentoring-share-with
.share-action-buttons
{
text-align
:
center
;
padding-top
:
.5em
;
}
.mentoring-share-with
.add-share-username
{
margin-right
:
1em
;
}
.mentoring-share-with
.share-errors
{
color
:
darkred
;
font-size
:
.75em
;
text-align
:
center
;
}
.new-share-container
{
margin-top
:
.5em
;
}
.shared-list
li
{
list-style-type
:
none
;
display
:
block
;
padding
:
.25em
;
margin
:
0
;
}
.shared-list
li
.username
{
display
:
inline-block
;
float
:
left
;
}
.share-panel-container
{
text-align
:
right
;
}
.share-notification
{
border
:
2px
solid
rgb
(
200
,
200
,
200
);
max-width
:
15em
;
padding
:
1em
;
background-color
:
rgb
(
255
,
255
,
255
);
position
:
absolute
;
right
:
0
;
font-size
:
.8em
;
}
.share-notification
.notification-close
{
float
:
right
;
}
.report-download-container
{
float
:
left
;
}
.mentoring
.identification
{
padding-bottom
:
1em
;
}
\ No newline at end of file
problem_builder/public/js/review_blocks.js
View file @
f5953e35
...
@@ -56,10 +56,153 @@ function PBDashboardBlock(runtime, element, initData) {
...
@@ -56,10 +56,153 @@ function PBDashboardBlock(runtime, element, initData) {
function
MentoringTableBlock
(
runtime
,
element
,
initData
)
{
function
MentoringTableBlock
(
runtime
,
element
,
initData
)
{
// Display an excerpt for long answers, with a "more" link to display the full text
// Display an excerpt for long answers, with a "more" link to display the full text
$
(
'.answer-table'
,
element
).
shorten
({
moreText
:
'more'
,
var
$element
=
$
(
element
),
lessText
:
'less'
,
$shareButton
=
$element
.
find
(
'.mentoring-share-button'
),
showChars
:
'500'
$doShareButton
=
$element
.
find
(
'.do-share-button'
),
$shareMenu
=
$element
.
find
(
'.mentoring-share-with'
),
$displayDropdown
=
$element
.
find
(
'.mentoring-display-dropdown'
),
$errorHolder
=
$element
.
find
(
'.share-errors'
),
$deleteShareButton
=
$element
.
find
(
'.remove-share'
),
$newShareContainer
=
$
(
$element
.
find
(
'.new-share-container'
)[
0
]),
$addShareField
=
$
(
$element
.
find
(
'.add-share-field'
)[
0
]),
$notification
=
$
(
$element
.
find
(
'.share-notification'
)),
$closeNotification
=
$
(
$element
.
find
(
'.notification-close'
)),
tableLoadURL
=
runtime
.
handlerUrl
(
element
,
'table_render'
),
deleteShareUrl
=
runtime
.
handlerUrl
(
element
,
'remove_share'
),
sharedListLoadUrl
=
runtime
.
handlerUrl
(
element
,
'get_shared_list'
),
clearNotificationUrl
=
runtime
.
handlerUrl
(
element
,
'clear_notification'
),
shareResultsUrl
=
runtime
.
handlerUrl
(
element
,
'share_results'
);
function
loadTable
(
data
)
{
$element
.
find
(
'.mentoring-table-target'
).
html
(
data
[
'content'
]);
$
(
'.answer-table'
,
element
).
shorten
({
moreText
:
'more'
,
lessText
:
'less'
,
showChars
:
'500'
});
}
function
errorMessage
(
event
)
{
$errorHolder
.
text
(
JSON
.
parse
(
event
.
responseText
)[
'error'
])
}
function
sharedRefresh
(
data
)
{
$element
.
find
(
'.shared-with-container'
).
html
(
data
[
'content'
]);
$deleteShareButton
=
$
(
$deleteShareButton
.
selector
);
$deleteShareButton
.
on
(
'click'
,
deleteShare
);
}
function
postShareRefresh
(
data
)
{
sharedRefresh
(
data
);
$element
.
find
(
".new-share-container"
).
each
(
function
(
index
,
container
)
{
if
(
index
===
0
)
{
$
(
container
).
find
(
'.add-share-username'
).
val
(
''
);
return
;
}
$
(
container
).
remove
()
});
$errorHolder
.
html
(
''
);
}
function
postShare
()
{
$
.
ajax
({
type
:
"POST"
,
url
:
sharedListLoadUrl
,
data
:
JSON
.
stringify
({}),
success
:
postShareRefresh
,
error
:
errorMessage
});
}
function
updateShare
()
{
var
usernames
=
[];
$element
.
find
(
'.add-share-username'
).
each
(
function
(
index
,
username
)
{
usernames
.
push
(
$
(
username
).
val
())
});
$
.
ajax
({
type
:
"POST"
,
url
:
shareResultsUrl
,
data
:
JSON
.
stringify
({
'usernames'
:
usernames
}),
success
:
postShare
,
error
:
errorMessage
});
}
function
menuHider
(
event
)
{
if
(
!
$
(
event
.
target
).
closest
(
$shareMenu
).
length
)
{
// We're clicking outside of the menu, so hide it.
$shareMenu
.
hide
();
$
(
document
).
off
(
'click.mentoring_share_menu_hide'
);
}
}
$shareButton
.
on
(
'click'
,
function
(
event
)
{
if
(
!
$shareMenu
.
is
(
':visible'
)){
event
.
stopPropagation
();
$
(
document
).
on
(
'click.mentoring_share_menu_hide'
,
menuHider
);
$shareMenu
.
show
();
}
});
});
new
ExportBase
(
runtime
,
element
,
initData
)
$doShareButton
.
on
(
'click'
,
updateShare
);
function
postLoad
(
data
)
{
loadTable
(
data
);
new
ExportBase
(
runtime
,
element
,
initData
);
}
$
.
ajax
({
type
:
"POST"
,
url
:
tableLoadURL
,
data
:
JSON
.
stringify
({
'target_username'
:
$displayDropdown
.
val
()}),
success
:
postLoad
});
$
.
ajax
({
type
:
"POST"
,
url
:
sharedListLoadUrl
,
data
:
JSON
.
stringify
({}),
success
:
sharedRefresh
});
$displayDropdown
.
on
(
'change'
,
function
()
{
$
.
ajax
({
type
:
"POST"
,
url
:
tableLoadURL
,
data
:
JSON
.
stringify
({
'target_username'
:
$displayDropdown
.
val
()}),
success
:
loadTable
})
});
function
addShare
()
{
var
container
=
$newShareContainer
.
clone
();
container
.
find
(
'.add-share-username'
).
val
(
''
);
container
.
insertAfter
(
$element
.
find
(
'.new-share-container'
).
last
());
container
.
find
(
'.add-share-field'
).
on
(
'click'
,
addShare
)
}
function
deleteShare
(
event
)
{
$
.
ajax
({
type
:
"POST"
,
url
:
deleteShareUrl
,
data
:
JSON
.
stringify
({
'username'
:
$
(
event
.
target
).
prev
()[
0
].
innerHTML
}),
success
:
function
()
{
$
(
event
.
target
).
parent
().
remove
();
$errorHolder
.
html
(
''
);
},
error
:
errorMessage
});
}
$closeNotification
.
on
(
'click'
,
function
()
{
// Don't need server approval to hide it.
$notification
.
hide
();
$
.
ajax
({
type
:
"POST"
,
url
:
clearNotificationUrl
,
data
:
JSON
.
stringify
({
'usernames'
:
$notification
.
data
(
'shared'
)})
})
});
$addShareField
.
on
(
'click'
,
addShare
);
}
}
problem_builder/table.py
View file @
f5953e35
...
@@ -21,10 +21,12 @@
...
@@ -21,10 +21,12 @@
# Imports ###########################################################
# Imports ###########################################################
import
errno
import
errno
import
json
from
django.contrib.auth.models
import
User
from
xblock.core
import
XBlock
from
xblock.core
import
XBlock
from
xblock.fields
import
Scope
,
String
,
Boolean
from
xblock.exceptions
import
JsonHandlerError
from
xblock.fields
import
Scope
,
String
,
Boolean
,
Dict
from
xblock.fragment
import
Fragment
from
xblock.fragment
import
Fragment
from
xblockutils.resources
import
ResourceLoader
from
xblockutils.resources
import
ResourceLoader
...
@@ -33,6 +35,8 @@ from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContain
...
@@ -33,6 +35,8 @@ from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContain
# Globals ###########################################################
# Globals ###########################################################
from
problem_builder
import
AnswerRecapBlock
from
problem_builder
import
AnswerRecapBlock
from
problem_builder.dashboard
import
ExportMixin
from
problem_builder.dashboard
import
ExportMixin
from
problem_builder.models
import
Share
from
problem_builder.sub_api
import
SubmittingXBlockMixin
loader
=
ResourceLoader
(
__name__
)
loader
=
ResourceLoader
(
__name__
)
...
@@ -45,7 +49,10 @@ def _(text):
...
@@ -45,7 +49,10 @@ def _(text):
@XBlock.wants
(
"user"
)
@XBlock.wants
(
"user"
)
class
MentoringTableBlock
(
StudioEditableXBlockMixin
,
StudioContainerXBlockMixin
,
ExportMixin
,
XBlock
):
@XBlock.wants
(
"submissions"
)
class
MentoringTableBlock
(
StudioEditableXBlockMixin
,
SubmittingXBlockMixin
,
StudioContainerXBlockMixin
,
ExportMixin
,
XBlock
):
"""
"""
Table-type display of information from mentoring blocks
Table-type display of information from mentoring blocks
...
@@ -76,26 +83,164 @@ class MentoringTableBlock(StudioEditableXBlockMixin, StudioContainerXBlockMixin,
...
@@ -76,26 +83,164 @@ class MentoringTableBlock(StudioEditableXBlockMixin, StudioContainerXBlockMixin,
default
=
False
,
default
=
False
,
scope
=
Scope
.
content
scope
=
Scope
.
content
)
)
allow_sharing
=
Boolean
(
display_name
=
_
(
"Allow Sharing"
),
help
=
_
(
"Allow students to share their results with other students."
),
default
=
True
,
scope
=
Scope
.
content
)
has_children
=
True
has_children
=
True
css_path
=
'public/css/mentoring-table.css'
css_path
=
'public/css/mentoring-table.css'
js_path
=
'public/js/review_blocks.js'
js_path
=
'public/js/review_blocks.js'
def
student_view
(
self
,
context
):
@XBlock.json_handler
context
=
context
.
copy
()
if
context
else
{}
def
table_render
(
self
,
data
,
suffix
=
''
):
fragment
=
Fragment
()
context
=
{}
header_values
=
[]
header_values
=
[]
content_values
=
[]
content_values
=
[]
target_username
=
data
.
get
(
'target_username'
)
try
:
if
target_username
and
target_username
!=
self
.
current_user_key
:
share
=
Share
.
objects
.
get
(
shared_by__username
=
target_username
,
shared_with__username
=
self
.
current_user_key
,
block_id
=
self
.
block_id
,
)
context
[
'student_submissions_key'
]
=
share
.
submission_uid
except
Share
.
DoesNotExist
:
raise
JsonHandlerError
(
403
,
_
(
"You are not permitted to view this student's table."
))
for
child_id
in
self
.
children
:
for
child_id
in
self
.
children
:
child
=
self
.
runtime
.
get_block
(
child_id
)
child
=
self
.
runtime
.
get_block
(
child_id
)
# Child should be an instance of MentoringTableColumn
# Child should be an instance of MentoringTableColumn
header_values
.
append
(
child
.
header
)
header_values
.
append
(
child
.
header
)
child_frag
=
child
.
render
(
'mentoring_view'
,
context
)
child_frag
=
child
.
render
(
'mentoring_view'
,
context
)
content_values
.
append
(
child_frag
.
content
)
content_values
.
append
(
child_frag
.
content
)
fragment
.
add_frag_resources
(
child_frag
)
context
[
'header_values'
]
=
header_values
if
any
(
header_values
)
else
None
context
[
'header_values'
]
=
header_values
if
any
(
header_values
)
else
None
context
[
'content_values'
]
=
content_values
context
[
'content_values'
]
=
content_values
html
=
loader
.
render_template
(
'templates/html/mentoring-table.html'
,
context
)
return
{
'content'
:
html
}
@property
def
current_user_key
(
self
):
user
=
self
.
runtime
.
service
(
self
,
'user'
)
.
get_current_user
()
# We may be in the SDK, in which case the username may not really be available.
return
user
.
opt_attrs
.
get
(
'edx-platform.username'
,
'username'
)
@XBlock.json_handler
def
get_shared_list
(
self
,
data
,
suffix
=
''
):
context
=
{
'shared_with'
:
Share
.
objects
.
filter
(
shared_by__username
=
self
.
current_user_key
,
block_id
=
self
.
block_id
,
)
.
values_list
(
'shared_with__username'
,
flat
=
True
)
}
return
{
'content'
:
loader
.
render_template
(
'templates/html/mentoring-table-shared-list.html'
,
context
)
}
@XBlock.json_handler
def
clear_notification
(
self
,
data
,
suffix
=
''
):
"""
Clear out notifications for users who shared with this user on the last page load.
Since more users might share with them while they're viewing the page, only remove the ones
that they had at the time.
"""
usernames
=
data
.
get
(
'usernames'
)
if
not
usernames
:
raise
JsonHandlerError
(
400
,
"No usernames sent."
)
try
:
isinstance
(
usernames
,
list
)
except
ValueError
:
raise
JsonHandlerError
(
400
,
"Usernames must be a list."
)
Share
.
objects
.
filter
(
shared_with__username
=
self
.
current_user_key
,
shared_by__username__in
=
usernames
,
block_id
=
self
.
block_id
,
)
.
update
(
notified
=
True
)
@property
def
block_id
(
self
):
usage_id
=
self
.
scope_ids
.
usage_id
if
isinstance
(
usage_id
,
basestring
):
return
usage_id
try
:
return
unicode
(
usage_id
.
replace
(
branch
=
None
,
version_guid
=
None
))
except
AttributeError
:
pass
return
unicode
(
usage_id
)
@XBlock.json_handler
def
share_results
(
self
,
data
,
suffix
=
''
):
target_usernames
=
data
.
get
(
'usernames'
)
target_usernames
=
[
username
.
strip
()
.
lower
()
for
username
in
target_usernames
if
username
.
strip
()]
current_user
=
User
.
objects
.
get
(
username
=
self
.
current_user_key
)
failed_users
=
[]
if
not
target_usernames
:
raise
JsonHandlerError
(
400
,
_
(
'Usernames not provided.'
))
for
target_username
in
target_usernames
:
try
:
target_user
=
User
.
objects
.
get
(
username
=
target_username
)
except
User
.
DoesNotExist
:
failed_users
.
append
(
target_username
)
continue
if
current_user
==
target_user
:
continue
try
:
Share
.
objects
.
get
(
shared_by
=
current_user
,
shared_with
=
target_user
,
block_id
=
self
.
block_id
)
except
Share
.
DoesNotExist
:
Share
(
shared_by
=
current_user
,
submission_uid
=
self
.
runtime
.
anonymous_student_id
,
shared_with
=
target_user
,
block_id
=
self
.
block_id
,
)
.
save
()
if
failed_users
:
raise
JsonHandlerError
(
400
,
_
(
'Some users could not be shared with. Please check these usernames: {}'
)
.
format
(
','
.
join
(
failed_users
)
)
)
return
{}
@XBlock.json_handler
def
remove_share
(
self
,
data
,
suffix
=
''
):
target_username
=
data
.
get
(
'username'
)
if
not
target_username
:
raise
JsonHandlerError
(
400
,
_
(
'Username not provided.'
))
Share
.
objects
.
filter
(
shared_by__username
=
self
.
current_user_key
,
shared_with__username
=
target_username
,
block_id
=
self
.
block_id
,
)
.
delete
()
return
{
'message'
:
_
(
'Removed successfully.'
)}
def
student_view
(
self
,
context
):
context
=
context
.
copy
()
if
context
else
{}
fragment
=
Fragment
()
for
child_id
in
self
.
children
:
child
=
self
.
runtime
.
get_block
(
child_id
)
# Child should be an instance of MentoringTableColumn
child_frag
=
child
.
render
(
'mentoring_view'
,
context
)
fragment
.
add_frag_resources
(
child_frag
)
context
[
'allow_sharing'
]
=
self
.
allow_sharing
context
[
'allow_download'
]
=
self
.
allow_download
context
[
'allow_download'
]
=
self
.
allow_download
user_service
=
self
.
runtime
.
service
(
self
,
'user'
)
if
user_service
:
context
[
'view_options'
]
=
Share
.
objects
.
filter
(
shared_with__username
=
self
.
current_user_key
,
block_id
=
self
.
block_id
,
)
.
values_list
(
'shared_by__username'
,
flat
=
True
)
context
[
'username'
]
=
self
.
current_user_key
share_notifications
=
Share
.
objects
.
filter
(
shared_with__username
=
self
.
current_user_key
,
notified
=
False
,
block_id
=
self
.
block_id
,
)
.
values_list
(
'shared_by__username'
,
flat
=
True
)
context
[
'share_notifications'
]
=
share_notifications
and
json
.
dumps
(
list
(
share_notifications
))
if
self
.
type
:
if
self
.
type
:
# Load an optional background image:
# Load an optional background image:
...
@@ -116,7 +261,7 @@ class MentoringTableBlock(StudioEditableXBlockMixin, StudioContainerXBlockMixin,
...
@@ -116,7 +261,7 @@ class MentoringTableBlock(StudioEditableXBlockMixin, StudioContainerXBlockMixin,
'course_name'
:
self
.
_get_course_name
(),
'course_name'
:
self
.
_get_course_name
(),
})
})
fragment
.
add_content
(
loader
.
render_template
(
'templates/html/mentoring-table.html'
,
context
))
fragment
.
add_content
(
loader
.
render_template
(
'templates/html/mentoring-table
-container
.html'
,
context
))
fragment
.
add_css_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/css/mentoring-table.css'
))
fragment
.
add_css_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/css/mentoring-table.css'
))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/js/vendor/jquery-shorten.js'
))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/js/vendor/jquery-shorten.js'
))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
self
.
js_path
))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
self
.
js_path
))
...
...
problem_builder/templates/html/mentoring-table-container.html
0 → 100644
View file @
f5953e35
{% load i18n %}
{% if allow_sharing %}
<div
class=
"share-panel-container"
>
{% if allow_download %}
<div
class=
"report-download-container"
><a
class=
"report-download-link"
href=
"#report_download"
download=
"report.html"
>
{% trans "Download report" %}
</a></div>
{% endif %}
<div
class=
"mentoring-share-panel"
>
{% if view_options %}
<span>
{% trans "Display Map from:" %}
</span>
<select
class=
"mentoring-display-dropdown"
>
<option
value=
"{{username}}"
>
{% blocktrans %}You ({{username}}){% endblocktrans %}
</option>
{% for option in view_options %}
<option
value=
"{{option}}"
>
{{option}}
</option>
{% endfor %}
</select>
{% endif %}
<button
class=
"mentoring-share-button"
>
<i
class=
"fa fa-share"
></i>
Share
</button>
<div
class=
"mentoring-share-with"
style=
"display: none;"
>
<div
class=
"share-with-instructions"
>
<p>
{% trans "Enter the username of another student you'd like to share this with." %}
</p>
</div>
<div
class=
"shared-with-container"
></div>
<div
class=
"new-share-container"
><input
class=
"add-share-username"
><button
class=
"add-share-field"
>
+
</button></div>
<div
class=
"share-errors"
></div>
<div
class=
"share-action-buttons"
>
<button
class=
"do-share-button"
>
Share
</button>
</div>
</div>
{% if share_notifications %}
<div
class=
"share-notification"
data-shared=
"{{share_notifications}}"
>
<button
class=
"notification-close"
><i
class=
"fa fa-close"
></i></button>
<p><strong>
{% trans "Map added!" %}
</strong></p>
<p>
{% blocktrans %}
Another user has shared a map with you.
</p>
<p>
You can change the user you're currently displaying using the drop-down selector above.
{% endblocktrans %}
</p>
</div>
{% endif %}
</div>
</div>
<div
class=
"clear"
></div>
{% endif %}
<div
class=
"mentoring-table-container"
>
<div
class=
"mentoring-table {{ self.type }}"
style=
"background-image: url({{ bg_image_url }})"
>
<div
class=
"cont-text-sr"
>
{{ bg_image_description }}
</div>
<div
class=
"mentoring-table-target"
></div>
</div>
</div>
problem_builder/templates/html/mentoring-table-shared-list.html
0 → 100644
View file @
f5953e35
{% load i18n %}
{% if shared_with %}
<div
class=
"share-header"
>
{% trans "Shared with:" %}
</div>
{% endif %}
<ul
class=
"shared-list"
>
{% for username in shared_with %}
<li><span
class=
"username"
>
{{username}}
</span><button
class=
"remove-share"
><i
class=
"fa fa-trash"
></i></button></li>
{% endfor %}
</ul>
\ No newline at end of file
problem_builder/templates/html/mentoring-table.html
View file @
f5953e35
{% load i18n %}
<table>
<div
class=
"mentoring-table-container"
>
{% if header_values %}
<div
class=
"mentoring-table {{ self.type }}"
style=
"background-image: url({{ bg_image_url }})"
>
<thead>
<div
class=
"cont-text-sr"
>
{{ bg_image_description }}
</div>
{% for header in header_values %}
<table>
<th>
{{ header|safe }}
</th>
{% if header_values %}
{% endfor %}
<thead>
</thead>
{% for header in header_values %}
{% endif %}
<th>
{{ header|safe }}
</th>
<tbody>
{% endfor %}
<tr>
</thead>
{% for content in content_values %}
{% endif %}
<td>
<tbody>
<tr>
{% for content in content_values %}
<td>
<div
class=
"mentoring-column"
>
<div
class=
"mentoring-column"
>
{{content|safe}}
{{content|safe}}
</div>
</div>
</td>
</td>
{% endfor %}
{% endfor %}
</tr>
</tr>
</tbody>
</tbody>
</table>
</table>
</div>
\ No newline at end of file
{% if allow_download %}
<p><a
class=
"report-download-link"
href=
"#report_download"
download=
"report.html"
>
{% trans "Download report" %}
</a></p>
{% endif %}
</div>
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment