Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: setup.py update using script #195

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
include requirements/base.in
include requirements/base.in
include requirements/constraints.txt
5 changes: 5 additions & 0 deletions build/lib/staff_graded/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
""" Loading Xblock class"""

from .staff_graded import StaffGradedXBlock

__version__ = '2.1.1'
301 changes: 301 additions & 0 deletions build/lib/staff_graded/staff_graded.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
"""
XBlock for Staff Graded Points
"""


import io
import json
import logging

import markdown
import pkg_resources
from web_fragments.fragment import Fragment
from webob import Response
from xblock.core import XBlock
from xblock.fields import Float, Scope, String
from xblock.runtime import NoSuchServiceError
from xblock.scorable import ScorableXBlockMixin, Score
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin

try:
from openedx.core.djangoapps.course_groups.cohorts import \
get_course_cohorts
except ImportError:
get_course_cohorts = lambda course_key: [] # pylint: disable=unnecessary-lambda-assignment

try:
from common.djangoapps.course_modes.models import CourseMode
modes_for_course = CourseMode.modes_for_course
except ImportError:
modes_for_course = lambda course_key: [('audit', 'Audit Track'), ('masters', "Master's Track"), # pylint: disable=unnecessary-lambda-assignment
('verified', "Verified Track")]

from bulk_grades.api import ScoreCSVProcessor, get_score, set_score

_ = lambda text: text # pylint: disable=unnecessary-lambda-assignment

log = logging.getLogger(__name__)


@XBlock.needs('settings')
@XBlock.needs('i18n')
@XBlock.needs('user')
class StaffGradedXBlock(StudioEditableXBlockMixin, ScorableXBlockMixin, XBlock): # pylint: disable=abstract-method
"""
Staff Graded Points block
"""
display_name = String(
display_name=_("Display Name"),
help=_("The display name for this component."),
scope=Scope.settings,
default=_("Staff Graded Points"),
)
instructions = String(
display_name=_("Instructions"),
help=_("The instructions to the learner. Markdown format"),
scope=Scope.content,
multiline_editor=True,
default=_("Your results will be graded offline"),
runtime_options={'multiline_editor': 'html'},
)
weight = Float(
display_name="Problem Weight",
help=_(
"Enter the number of points possible for this component. "
"The default value is 1.0. "
),
default=1.0,
scope=Scope.settings,
values={"min": 0},
)
has_score = True

editable_fields = ('display_name', 'instructions', 'weight')

def _get_current_username(self):
return self.runtime.service(self, 'user').get_current_user().opt_attrs.get(
'edx-platform.username')

def resource_string(self, path):
"""Handy helper for getting resources from our kit."""
data = pkg_resources.resource_string(__name__, path)
return data.decode("utf8")

def student_view(self, context=None):
"""
The primary view of the StaffGradedXBlock, shown to students
when viewing courses.
"""
frag = Fragment()
frag.add_css(self.resource_string("static/css/staff_graded.css"))
loader = ResourceLoader(__name__)
_ = self.runtime.service(self, "i18n").ugettext

# Add i18n js
statici18n_js_url = self._get_statici18n_js_url()
if statici18n_js_url:
frag.add_javascript_url(self.runtime.local_resource_url(self, statici18n_js_url))

frag.add_javascript(self.resource_string("static/js/src/staff_graded.js"))
frag.initialize_js('StaffGradedXBlock')

context['id'] = self.location.html_id() # pylint: disable=no-member
context['instructions'] = markdown.markdown(self.instructions)
context['display_name'] = self.display_name
context['is_staff'] = self.runtime.user_is_staff

course_id = self.location.course_key # pylint: disable=no-member
context['available_cohorts'] = [cohort.name for cohort in get_course_cohorts(course_id=course_id)]
context['available_tracks'] = [
(mode.slug, mode.name) for mode in # pylint: disable=no-member
modes_for_course(course_id, only_selectable=False)
]

if context['is_staff']:
from crum import get_current_request # pylint: disable=import-outside-toplevel
from django.middleware.csrf import get_token # pylint: disable=import-outside-toplevel
context['import_url'] = self.runtime.handler_url(self, "csv_import_handler")
context['export_url'] = self.runtime.handler_url(self, "csv_export_handler")
context['poll_url'] = self.runtime.handler_url(self, "get_results_handler")
context['csrf_token'] = get_token(get_current_request())
frag.add_javascript(loader.load_unicode('static/js/src/staff_graded.js'))
frag.initialize_js('StaffGradedProblem',
json_args={k: context[k]
for k
in ('csrf_token', 'import_url', 'export_url', 'poll_url', 'id')})

try:
score = get_score(self.location, self.runtime.user_id) or {} # pylint: disable=no-member
context['grades_available'] = True
except NoSuchServiceError:
context['grades_available'] = False
else:
if score:
grade = score['score']
context['score_string'] = _('{score} / {total} points').format(score=grade, total=self.weight)
else:
context['score_string'] = _('{total} points possible').format(total=self.weight)
frag.add_content(loader.render_django_template('static/html/staff_graded.html', context))
return frag

# TO-DO: change this to create the scenarios you'd like to see in the
# workbench while developing your XBlock.
@staticmethod
def workbench_scenarios():
"""A canned scenario for display in the workbench."""
return [
("StaffGradedXBlock",
"""<staffgradedxblock/>
"""),
("Multiple StaffGradedXBlock",
"""<vertical_demo>
<staffgradedxblock/>
<staffgradedxblock/>
<staffgradedxblock/>
</vertical_demo>
"""),
]

@staticmethod
def _get_statici18n_js_url():
"""
Returns the Javascript translation file for the currently selected language, if any.
Defaults to English if available.
"""
from django.utils import translation # pylint: disable=import-outside-toplevel
locale_code = translation.get_language()
if locale_code is None:
return None
text_js = 'public/js/translations/{locale_code}/text.js'
lang_code = locale_code.split('-')[0]
for code in (locale_code, lang_code, 'en'):
loader = ResourceLoader(__name__)
if pkg_resources.resource_exists(
loader.module_name, text_js.format(locale_code=code)):
return text_js.format(locale_code=code)
return None

@staticmethod
def get_dummy():
"""
Dummy method to generate initial i18n
"""
from django.utils import translation # pylint: disable=import-outside-toplevel
return translation.gettext_noop('Dummy')

@XBlock.handler
def csv_import_handler(self, request, suffix=''): # pylint: disable=unused-argument
"""
Endpoint that handles CSV uploads.
"""
if not self.runtime.user_is_staff:
return Response('not allowed', status_code=403)

_ = self.runtime.service(self, "i18n").ugettext

try:
score_file = request.POST['csv'].file
except KeyError:
data = {'error_rows': [1], 'error_messages': [_('missing file')]}
else:
log.info('Processing %d byte score file %s for %s', score_file.size, score_file.name, self.location) # pylint: disable=no-member
block_id = self.location # pylint: disable=no-member
block_weight = self.weight
processor = ScoreCSVProcessor(
block_id=str(block_id),
max_points=block_weight,
user_id=self.runtime.user_id)
processor.process_file(score_file, autocommit=True)
data = processor.status()
log.info('Processed file %s for %s -> %s saved, %s processed, %s error. (async=%s)',
score_file.name,
block_id,
data.get('saved', 0),
data.get('total', 0),
len(data.get('error_rows', [])),
data.get('waiting', False))
return Response(json_body=data)

@XBlock.handler
def csv_export_handler(self, request, suffix=''): # pylint: disable=unused-argument
"""
Endpoint that handles CSV downloads.
"""
if not self.runtime.user_is_staff:
return Response('not allowed', status_code=403)

track = request.GET.get('track', None)
cohort = request.GET.get('cohort', None)

buf = io.StringIO()
ScoreCSVProcessor(
block_id=str(self.location), # pylint: disable=no-member
max_points=self.weight,
display_name=self.display_name,
track=track,
cohort=cohort).write_file(buf)
resp = Response(buf.getvalue())
resp.content_type = 'text/csv'
resp.content_disposition = f'attachment; filename="{self.location}.csv"' # pylint: disable=no-member
return resp

@XBlock.handler
def get_results_handler(self, request, suffix=''): # pylint: disable=unused-argument
"""
Endpoint to poll for celery results.
"""
if not self.runtime.user_is_staff:
return Response('not allowed', status_code=403)
try:
result_id = request.POST['result_id']
except KeyError:
data = {'message': 'missing'}
else:
results = ScoreCSVProcessor().get_deferred_result(result_id)
if results.ready():
data = results.get()
log.info('Got results from celery %r', data)
else:
data = {'waiting': True, 'result_id': result_id}
log.info('Still waiting for %s', result_id)
return Response(json_body=data)

def max_score(self):
return self.weight

def get_score(self):
"""
Return a raw score already persisted on the XBlock. Should not
perform new calculations.

Returns:
Score(raw_earned=float, raw_possible=float)
"""
score = get_score(self.runtime.user_id, self.location) # pylint: disable=no-member
score = score or {'grade': 0, 'max_grade': 1}
return Score(raw_earned=score['grade'], raw_possible=score['max_grade'])

def set_score(self, score):
"""
Persist a score to the XBlock.

The score is a named tuple with a raw_earned attribute and a
raw_possible attribute, reflecting the raw earned score and the maximum
raw score the student could have earned respectively.

Arguments:
score: Score(raw_earned=float, raw_possible=float)

Returns:
None
"""
state = json.dumps({'grader': self._get_current_username()})
set_score(self.location, # pylint: disable=no-member
self.runtime.user_id,
score.raw_earned,
score.raw_possible,
state=state)

def publish_grade(self):
pass
19 changes: 19 additions & 0 deletions build/lib/staff_graded/static/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
This static directory is for files that should be included in your kit as plain
static files.

You can ask the runtime for a URL that will retrieve these files with:

url = self.runtime.local_resource_url(self, "static/js/lib.js")

The default implementation is very strict though, and will not serve files from
the static directory. It will serve files from a directory named "public".
Create a directory alongside this one named "public", and put files there.
Then you can get a url with code like this:

url = self.runtime.local_resource_url(self, "public/js/lib.js")

The sample code includes a function you can use to read the content of files
in the static directory, like this:

frag.add_javascript(self.resource_string("static/js/my_block.js"))

14 changes: 14 additions & 0 deletions build/lib/staff_graded/static/css/staff_graded.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* CSS for StaffGradedXBlock */

.staff_graded_block .count {
font-weight: bold;
}

.staff_graded_block p {
cursor: pointer;
}

.staff_graded_block .message {
color: red;
margin-left: 1.1em;
}
Loading
Loading