Merge branch 'master' into revert-edx-enterprise

This commit is contained in:
Feanil Patel
2020-07-14 11:42:08 -04:00
committed by GitHub
19 changed files with 347 additions and 53 deletions

View File

@@ -197,7 +197,6 @@ class MigrationTests(TestCase):
Tests for migrations.
"""
@unittest.skip('migration is purposely out of sync with models atm.')
@override_settings(MIGRATION_MODULES={})
def test_migrations_are_in_sync(self):
"""

View File

@@ -0,0 +1,178 @@
Status
======
Proposed
Context
=======
The ``edx-platform`` repo contains both Studio and the LMS for Open edX. These
two systems roughly correspond to the Content Authoring and Learning subdomains,
but the precise separation of responsibilities is currently unclear in many
cases. This ADR is intended to clarify those boundaries and offer guidelines for
how developers should compose new functionality across these systems, as well as
providing a direction for migrating existing functionality over the long term.
Note that it is likely that we'll further separate content authoring (e.g.
content libraries) from course run administration (e.g. grading policy). It's
possible that both of these will evolve under the umbrella of what users see as
"Studio". Even if that happens, there will still be an architectural split
between the Content Authoring and Learning subdomains within that new Studio.
Decision
========
The high level guidelines for the interaction between the Content Authoring and
Learning subdomains currently represented by Studio and LMS are:
* Studio should not store Learner information.
* Studio and LMS should use different representations of content.
* Decouple content grouping concepts from user/learning grouping concepts.
* Studio Content data acts as an input to LMS policy and Learner Experience data.
* LMS data should not flow backward into Studio.
* Content Authoring changes require explicit publishing of versioned data.
Studio should not store Learner information.
--------------------------------------------
Studio's responsibility centers around the content itself. It should not store
information about students, which brings with it many other concerns around
data sensitivity and scale.
Studio and LMS should use different representations of content.
--------------------------------------------------------------------
Content authoring will require versioned storage of data, ownership tracking,
tagging, and other metadata. The LMS focuses on read-optimization at a much
higher scale. We've long suffered from added code complexity and performance
issues by trying to cover both usage patterns with ModuleStore.
We have already taken steps to create a read-optimized store in the form of
Block Transformers. We should continue this practice and encourage the LMS to
transform content at publish-time into whatever representation its various
systems (courseware, grading, scheduling, etc.) require to be performant.
Decouple content grouping concepts from user/learning grouping concepts.
------------------------------------------------------------------------
A common use case for course content is to show different bits of content to
different cohorts of users. For instance, a university might have a licensing
agreement that allows it to show a set of vidoes only to its own staff and
students, and not a wider MOOC audience. Studio needs to be able to annotate
this data somehow, but the list of available cohorts for a given course is
considered Learner information that may change from run to run.
We solve this by using a level of indirection. Studio doesn't map content into
Cohorts of students (an LMS concept). It maps content into Content Groups. The
LMS is then responsible for both the creation of Cohorts as well as the mapping
of Content Groups to Cohorts.
While this might sound a little cumbersome, it actually allows for a cleaner
separation of concerns. Content Groups describe what the content is: restricted
copyright, advanced material, labratory exercises, etc. Cohorts describe who is
consuming that material: on campus students, alumni, the general MOOC audience,
etc. The Content Group is an Authoring decision based on the properties of the
content itself. The Cohort mapping is a policy decision about the Learning
experience of a particular set of students.
Furthermore, the mapping of Content Groups to Cohorts is not 1:1. You could
decide that both on-campus students and alumni get the same content group
experience, while keeping those Cohorts separate for the purposes of other parts
of the LMS like forums discussions.
A more future looking example might be the interaction between Open edX
courseware and third party forum services. The fact that certain units are
marked as discussable topics might be a Content Authoring decision in Studio,
while the choice of which forum service those discussions happen in might be a
Learning decision in the LMS.
Studio Content data acts as an input to LMS policy and Learner Experience data.
-------------------------------------------------------------------------------
As courseware becomes more dynamic, certain concepts in the LMS are becoming
richer than their equivalent concepts in Studio. In these situations, we should
think of the data relationship as a one way flow of data from Studio to the LMS.
The LMS takes Studio data as an input that it can enrich, transform, or override
as necessary to create the desired student learning experience.
Content scheduling is a good example of this. In the early days of Open edX,
course teams would set start and due dates for subsections in Studio, and that
would be the end of it. Today, we have personalized schedules, individual due
date extensions, and more. The pattern we use to accomplish this is:
* Copy the schedule information from Studio to the LMS at the time a course is
published, transforming it into a more easily queryable form in the process.
* Add additional data models in the LMS to support use cases like individual due
date extensions and personalized rescheduling. This is currently handled by
the edx_when app, developed in the edx-when repository.
* Remap field data so that XBlocks in the LMS runtime query this richer data
model. Accessing an XBlock's ``start`` or ``due`` attribute in the Studio
runtime continues to work with simple key/values in ModuleStore, but the LMS
XBlock runtime will fetch those values from edx-when's in-process API.
This approach allows us to add flexibility to the LMS, while preserving
backwards compatibility with existing XBlock content.
LMS data should not flow backward into Studio.
----------------------------------------------
Since LMS concepts extend Studio ones, we don't want changes to flow backwards
from the LMS back into Studio. Some reasons:
* There is no guarantee that Studio course runs will be 1:1 with LMS course
runs. In fact, one to many mappings of course runs already exist if CCX
courses are enabled.
* A unidirectional data flow makes the system easier to debug and reason about.
* The OLX import/export process stays much simpler if it doesn't have to
consider data that the LMS has added.
Content Authoring changes require explicit publishing of versioned data.
------------------------------------------------------------------------
Changes to content data should be marked with an explicit, versioned publishing
step. Many LMS systems update their representations of content data based on
publish signals from Studio today. Studio also needs to differentiate draft
changes authors want to make from changes that are ready for student use.
The LMS is permitted to modify the learning experience without any such explicit
publishing step. Deadlines may pass, blocking off student access to certain
parts of a course. Individual students may be placed into different teams or
cohorts, given extensions, re-graded, etc.
Goals
=====
* Developers will have a clearer understanding of where to build authoring and
learning experience functionality.
* Improved separation of these subdomains will allow for easier debugging and
better performance.
* Decoupling these subdomains will allow for more rapid interation and
innovation.
Alternatives Considered
=======================
An early alternative approach (that periodically resurfaces) is to make the
content editing and publishing process happen in a much more integrated way. The
learning and authoring experience blend together so closely that the author is
essentially looking at the same interface as the student, supplemented with an
edit button to modify thing in-line.
This approach was rejected early on because:
* Authoring needs differed in the workflow and information that they had to
surface to course authors.
* Separating the authoring and student experience allows multiple authoring
systems (e.g. GitHub based OLX authoring).
* At various points, the content authoring experience has been owned by a
different team than the learning experience.

View File

@@ -501,14 +501,14 @@ def get_course_assignment_date_blocks(course, user, request, num_return=None,
if num_return is None in date increasing order.
"""
date_blocks = []
for assignment in get_course_assignments(course.id, user, request, include_access=include_access):
for assignment in get_course_assignments(course.id, user, include_access=include_access):
date_block = CourseAssignmentDate(course, user)
date_block.date = assignment.date
date_block.contains_gated_content = assignment.contains_gated_content
date_block.complete = assignment.complete
date_block.assignment_type = assignment.assignment_type
date_block.past_due = assignment.past_due
date_block.link = assignment.url
date_block.link = request.build_absolute_uri(assignment.url) if assignment.url else ''
date_block.set_title(assignment.title, link=assignment.url)
date_blocks.append(date_block)
date_blocks = sorted((b for b in date_blocks if b.is_enabled or include_past_dates), key=date_block_key_fn)
@@ -518,7 +518,7 @@ def get_course_assignment_date_blocks(course, user, request, num_return=None,
@request_cached()
def get_course_assignments(course_key, user, request, include_access=False):
def get_course_assignments(course_key, user, include_access=False):
"""
Returns a list of assignment (at the subsection/sequential level) due dates for the given course.
@@ -544,12 +544,11 @@ def get_course_assignments(course_key, user, request, include_access=False):
assignment_type = block_data.get_xblock_field(subsection_key, 'format', None)
url = ''
url = None
start = block_data.get_xblock_field(subsection_key, 'start')
assignment_released = not start or start < now
if assignment_released:
url = reverse('jump_to', args=[course_key, subsection_key])
url = request and request.build_absolute_uri(url)
complete = block_data.get_xblock_field(subsection_key, 'complete', False)
past_due = not complete and due < now

View File

@@ -4,7 +4,7 @@
Status
------
Proposed
Accepted
Context
-------

View File

@@ -4,7 +4,7 @@
Status
------
Proposed
Accepted
Context
-------
@@ -42,9 +42,9 @@ Here are a few organizational relationships that exist in the edX system:
do so either via
a. bulk APIs using the *Client Credentials grant type* (e.g., to
synchronize their own data in a background process) or
synchronize their own data in a background process) or
b. a user-specific API on behalf of a logged-in user via the
b. a user-specific API on behalf of a logged-in user via the
*Authorization grant type* and *edX as the identity provider*
(e.g., to display user-specific data on their own portal).
@@ -100,7 +100,7 @@ organization information to the granting end-user.
"content_org:Microsoft"
* For a token created on behalf of a user (*not* created via a
* For a token created on behalf of a user (*not* created via a
*Client Credentials grant type*), the token
is further restricted specifically for the granting user. And so, a
"user" filter with the value "me" would be added for this grant type.
@@ -137,7 +137,7 @@ Token Examples
Client Credentials (server-to-server) grant type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When a trusted application makes server-to-server calls, the application's
When a trusted application makes server-to-server calls, the application's
service user info is included in the JWT and the *filters* field
includes the organization identifier and type associated with the application.
@@ -166,7 +166,7 @@ filter “me” in the *filters* field.
"version": "1.0",
"preferred_username": "ajay_mehta",
...
}
}
Consequences
------------
@@ -191,7 +191,7 @@ Consequences
make sure there are no security issues introduced where old endpoints
that are not aware of the new filter do not enforce it. Possible
ways of doing so are:
* Endpoints that are highly security sensitive should reject any
token that includes an unrecognized filter.
@@ -206,7 +206,7 @@ Consequences
they don't recognize. For example:
"grades:read:content_org"
Additionally, this alternative would allow tokens to specify different filters
for different scopes.

View File

@@ -1,6 +1,7 @@
"""
Calendar syncing Course dates with a User.
"""
default_app_config = 'openedx.features.calendar_sync.apps.UserCalendarSyncConfig'
def get_calendar_event_id(user, block_key, date_type, hostname):

View File

@@ -0,0 +1,4 @@
from django.contrib import admin
from .models import UserCalendarSyncConfig
admin.site.register(UserCalendarSyncConfig)

View File

@@ -0,0 +1,18 @@
"""
Define the calendar_sync Django App.
"""
# -*- coding: utf-8 -*-
from django.apps import AppConfig
class UserCalendarSyncConfig(AppConfig):
name = 'openedx.features.calendar_sync'
def ready(self):
super(UserCalendarSyncConfig, self).ready()
# noinspection PyUnresolvedReferences
import openedx.features.calendar_sync.signals # pylint: disable=import-outside-toplevel,unused-import

View File

@@ -9,12 +9,13 @@ from icalendar import Calendar, Event, vCalAddress, vText
from lms.djangoapps.courseware.courses import get_course_assignments
from openedx.core.djangoapps.site_configuration.helpers import get_value
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
from openedx.core.djangolib.markup import HTML
from . import get_calendar_event_id
def generate_ics_for_event(uid, title, course_name, now, start, organizer_name, organizer_email):
def generate_ics_for_event(uid, title, course_name, now, start, organizer_name, organizer_email, config):
"""
Generates an ics-formatted bytestring for the given assignment information.
@@ -36,6 +37,7 @@ def generate_ics_for_event(uid, title, course_name, now, start, organizer_name,
event.add('dtstart', start)
event.add('duration', timedelta(0))
event.add('transp', 'TRANSPARENT') # available, rather than busy
event.add('sequence', config.ics_sequence)
cal = Calendar()
cal.add('prodid', '-//Open edX//calendar_sync//EN')
@@ -46,28 +48,31 @@ def generate_ics_for_event(uid, title, course_name, now, start, organizer_name,
return cal.to_ical()
def generate_ics_for_user_course(course, user, request):
def generate_ics_files_for_user_course(course, user, user_calendar_sync_config_instance):
"""
Generates ics-formatted bytestrings of all assignments for a given course and user.
To pretty-print each bytestring, do: `ics.decode('utf8').replace('\r\n', '\n')`
Returns an iterable of ics files, each one representing an assignment.
Returns a dictionary of ics files, each one representing an assignment.
"""
assignments = get_course_assignments(course.id, user, request)
assignments = get_course_assignments(course.id, user)
platform_name = get_value('platform_name', settings.PLATFORM_NAME)
platform_email = get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
now = datetime.now(pytz.utc)
site_config = SiteConfiguration.get_configuration_for_org(course.org)
return (
generate_ics_for_event(
ics_files = {}
for assignment in assignments:
ics_files[assignment.title] = generate_ics_for_event(
now=now,
organizer_name=platform_name,
organizer_email=platform_email,
start=assignment.date,
title=assignment.title,
course_name=course.display_name_with_default,
uid=get_calendar_event_id(user, str(assignment.block_key), 'due', request.site.domain),
uid=get_calendar_event_id(user, str(assignment.block_key), 'due', site_config.site.domain),
config=user_calendar_sync_config_instance,
)
for assignment in assignments
)
return ics_files

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.2.14 on 2020-07-09 17:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('calendar_sync', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='historicalusercalendarsyncconfig',
name='ics_sequence',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='usercalendarsyncconfig',
name='ics_sequence',
field=models.IntegerField(default=0),
),
]

View File

@@ -19,6 +19,7 @@ class UserCalendarSyncConfig(models.Model):
user = models.ForeignKey(User, db_index=True, on_delete=models.CASCADE)
course_key = CourseKeyField(max_length=255, db_index=True)
enabled = models.BooleanField(default=False)
ics_sequence = models.IntegerField(default=0)
history = HistoricalRecords()

View File

@@ -0,0 +1,35 @@
"""
Signal handler for calendar sync models
"""
from django.db.models.signals import post_save
from django.dispatch.dispatcher import receiver
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.features.course_experience import CALENDAR_SYNC_FLAG, RELATIVE_DATES_FLAG
from .ics import generate_ics_files_for_user_course
from .models import UserCalendarSyncConfig
from .utils import send_email_with_attachment
@receiver(post_save, sender=UserCalendarSyncConfig)
def handle_calendar_sync_email(sender, instance, created, **kwargs):
if (
CALENDAR_SYNC_FLAG.is_enabled(instance.course_key) and
RELATIVE_DATES_FLAG.is_enabled(instance.course_key) and
created
):
user = instance.user
email = user.email
course_overview = CourseOverview.objects.get(id=instance.course_key)
ics_files = generate_ics_files_for_user_course(course_overview, user, instance)
send_email_with_attachment(
[email],
ics_files,
course_overview.display_name,
created
)
post_save.disconnect(handle_calendar_sync_email, sender=UserCalendarSyncConfig)
instance.ics_sequence = instance.ics_sequence + 1
instance.save()
post_save.connect(handle_calendar_sync_email, sender=UserCalendarSyncConfig)

View File

@@ -0,0 +1,12 @@
from factory.django import DjangoModelFactory
from openedx.features.calendar_sync.models import UserCalendarSyncConfig
class UserCalendarSyncConfigFactory(DjangoModelFactory):
"""
Factory class for SiteConfiguration model
"""
class Meta(object):
model = UserCalendarSyncConfig
enabled = True

View File

@@ -9,9 +9,10 @@ from mock import patch
from lms.djangoapps.courseware.courses import _Assignment
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
from openedx.features.calendar_sync import get_calendar_event_id
from openedx.features.calendar_sync.ics import generate_ics_for_user_course
from openedx.features.calendar_sync.ics import generate_ics_files_for_user_course
from openedx.features.calendar_sync.tests.factories import UserCalendarSyncConfigFactory
from student.tests.factories import UserFactory
@@ -30,6 +31,13 @@ class TestIcsGeneration(TestCase):
self.request = RequestFactory().request()
self.request.site = SiteFactory()
self.request.user = self.user
self.site_config = SiteConfigurationFactory.create(
site_values={'course_org_filter': self.course.org}
)
self.user_calendar_sync_config = UserCalendarSyncConfigFactory.create(
user=self.user,
course_key=self.course.id,
)
def make_assigment(
self, block_key=None, title=None, url=None, date=None, contains_gated_content=False, complete=False,
@@ -50,6 +58,7 @@ DTSTART;VALUE=DATE-TIME:{timedue}
DURATION:P0D
DTSTAMP;VALUE=DATE-TIME:20131003T082455Z
UID:{uid}
SEQUENCE:{sequence}
DESCRIPTION:{summary} is due for {course}.
ORGANIZER;CN=édX:mailto:registration@example.com
TRANSP:TRANSPARENT
@@ -61,7 +70,8 @@ END:VCALENDAR
summary=assignment.title,
course=self.course.display_name_with_default,
timedue=assignment.date.strftime('%Y%m%dT%H%M%SZ'),
uid=get_calendar_event_id(self.user, str(assignment.block_key), 'due', self.request.site.domain),
uid=get_calendar_event_id(self.user, str(assignment.block_key), 'due', self.site_config.site.domain),
sequence=self.user_calendar_sync_config.ics_sequence
)
for assignment in assignments
)
@@ -70,11 +80,13 @@ END:VCALENDAR
""" Uses generate_ics_for_user_course to create ics files for the given assignments """
with patch('openedx.features.calendar_sync.ics.get_course_assignments') as mock_get_assignments:
mock_get_assignments.return_value = assignments
return generate_ics_for_user_course(self.course, self.user, self.request)
return generate_ics_files_for_user_course(self.course, self.user, self.user_calendar_sync_config)
def assert_ics(self, *assignments):
""" Asserts that the generated and expected ics for the given assignments are equal """
generated = [ics.decode('utf8').replace('\r\n', '\n') for ics in self.generate_ics(*assignments)]
generated = [
file.decode('utf8').replace('\r\n', '\n') for file in sorted(self.generate_ics(*assignments).values())
]
self.assertEqual(len(generated), len(assignments))
self.assertListEqual(generated, list(self.expected_ics(*assignments)))

View File

@@ -6,43 +6,50 @@ from django.conf import settings
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
import os
import boto3
logger = logging.getLogger(__name__)
def calendar_sync_initial_email_content(course_name):
subject = _('Stay on Track')
body_text = _('Sticking to a schedule is the best way to ensure that you successfully complete your self-paced '
'course. This schedule of assignment due dates for {course} will help you stay on track!'
).format(course=course_name)
body = format_html('<div>{text}</div>', text=body_text)
subject = _('Sync {course} to your calendar').format(course=course_name)
body_paragraph_1 = _('Sticking to a schedule is the best way to ensure that you successfully complete your '
'self-paced course. This schedule for {course} will help you stay on track!'
).format(course=course_name)
body_paragraph_2 = _('Once you sync your course schedule to your calendar, any updates to the course from your '
'instructor will be automatically reflected. You can remove the course from your calendar '
'at any time.')
body = format_html(
'<div style="margin-bottom:10px">{bp1}</div><div>{bp2}</div>',
bp1=body_paragraph_1,
bp2=body_paragraph_2
)
return subject, body
def calendar_sync_update_email_content(course_name):
subject = _('Updates for Your {course} Schedule').format(course=course_name)
body_text = _('Your assignment due dates for {course} were recently adjusted. Update your calendar with your new '
'schedule to ensure that you stay on track!').format(course=course_name)
body = format_html('<div>{text}</div>', text=body_text)
subject = _('{course} dates have been updated on your calendar').format(course=course_name)
body_paragraph = _('You have successfully shifted your course schedule and your calendar is up to date.'
).format(course=course_name)
body = format_html('<div>{text}</div>', text=body_paragraph)
return subject, body
def prepare_attachments(attachment_data):
def prepare_attachments(attachment_data, file_ext=''):
"""
Helper function to create a list contain file attachment objects
for use with MIMEMultipart
Returns a list of MIMEApplication objects
"""
attachments = []
for filename, data in attachment_data.items():
msg_attachment = MIMEApplication(data)
msg_attachment.add_header(
'Content-Disposition',
'attachment',
filename=os.path.basename(filename)
filename=os.path.basename(filename) + file_ext
)
msg_attachment.set_type('text/calendar')
attachments.append(msg_attachment)
@@ -50,17 +57,17 @@ def prepare_attachments(attachment_data):
return attachments
def send_email_with_attachment(to_emails, attachment_data, course_name, is_update=False):
def send_email_with_attachment(to_emails, attachment_data, course_name, is_initial):
# connect to SES
client = boto3.client('ses', region_name=settings.AWS_SES_REGION_NAME)
subject, body = (calendar_sync_update_email_content(course_name) if is_update else
calendar_sync_initial_email_content(course_name))
subject, body = (calendar_sync_initial_email_content(course_name) if is_initial else
calendar_sync_update_email_content(course_name))
# build email body as html
msg_body = MIMEText(body, 'html')
attachments = prepare_attachments(attachment_data)
attachments = prepare_attachments(attachment_data, '.ics')
# iterate over each email in the list to send emails independently
for email in to_emails:

View File

@@ -6,7 +6,7 @@
#
-e git+https://github.com/edx/acid-block.git@98aecba94ecbfa934e2d00262741c0ea9f557fc9#egg=acid-xblock # via -r requirements/edx/github.in
-e common/lib/capa # via -r requirements/edx/local.in
-e git+https://github.com/edx/codejail.git@4127fc4bd5775cc72aee8d7f0a70e31405e22439#egg=codejail # via -r requirements/edx/github.in
-e git+https://github.com/edx/codejail.git@56abd45fcf68c6ab7b523768aa5991d6e04d8241#egg=codejail==3.0.0 # via -r requirements/edx/github.in
-e git+https://github.com/edx/django-wiki.git@0.0.27#egg=django-wiki # via -r requirements/edx/github.in
-e git+https://github.com/edx/DoneXBlock.git@2.0.2#egg=done-xblock # via -r requirements/edx/github.in
-e git+https://github.com/jazkarta/edx-jsme.git@690dbf75441fa91c7c4899df0b83d77f7deb5458#egg=edx-jsme # via -r requirements/edx/github.in
@@ -111,7 +111,7 @@ edx-rest-api-client==5.2.1 # via -r requirements/edx/base.in, edx-enterprise, e
edx-search==1.4.1 # via -r requirements/edx/base.in
edx-sga==0.11.0 # via -r requirements/edx/base.in
edx-submissions==3.1.11 # via -r requirements/edx/base.in, ora2
edx-tincan-py35==0.0.5 # via edx-enterprise
edx-tincan-py35==0.0.9 # via edx-enterprise
edx-user-state-client==1.2.0 # via -r requirements/edx/base.in
edx-when==1.2.9 # via -r requirements/edx/base.in, edx-proctoring
edxval==1.3.8 # via -r requirements/edx/base.in

View File

@@ -6,7 +6,7 @@
#
-e git+https://github.com/edx/acid-block.git@98aecba94ecbfa934e2d00262741c0ea9f557fc9#egg=acid-xblock # via -r requirements/edx/testing.txt
-e common/lib/capa # via -r requirements/edx/testing.txt
-e git+https://github.com/edx/codejail.git@4127fc4bd5775cc72aee8d7f0a70e31405e22439#egg=codejail # via -r requirements/edx/testing.txt
-e git+https://github.com/edx/codejail.git@56abd45fcf68c6ab7b523768aa5991d6e04d8241#egg=codejail==3.0.0 # via -r requirements/edx/testing.txt
-e git+https://github.com/edx/django-wiki.git@0.0.27#egg=django-wiki # via -r requirements/edx/testing.txt
-e git+https://github.com/edx/DoneXBlock.git@2.0.2#egg=done-xblock # via -r requirements/edx/testing.txt
-e git+https://github.com/jazkarta/edx-jsme.git@690dbf75441fa91c7c4899df0b83d77f7deb5458#egg=edx-jsme # via -r requirements/edx/testing.txt
@@ -125,7 +125,7 @@ edx-search==1.4.1 # via -r requirements/edx/testing.txt
edx-sga==0.11.0 # via -r requirements/edx/testing.txt
edx-sphinx-theme==1.5.0 # via -r requirements/edx/development.in
edx-submissions==3.1.11 # via -r requirements/edx/testing.txt, ora2
edx-tincan-py35==0.0.5 # via -r requirements/edx/testing.txt, edx-enterprise
edx-tincan-py35==0.0.9 # via -r requirements/edx/testing.txt, edx-enterprise
edx-user-state-client==1.2.0 # via -r requirements/edx/testing.txt
edx-when==1.2.9 # via -r requirements/edx/testing.txt, edx-proctoring
edxval==1.3.8 # via -r requirements/edx/testing.txt

View File

@@ -59,7 +59,7 @@ git+https://github.com/edx/MongoDBProxy.git@d92bafe9888d2940f647a7b2b2383b29c752
-e git+https://github.com/jazkarta/edx-jsme.git@690dbf75441fa91c7c4899df0b83d77f7deb5458#egg=edx-jsme
# Our libraries:
-e git+https://github.com/edx/codejail.git@4127fc4bd5775cc72aee8d7f0a70e31405e22439#egg=codejail
-e git+https://github.com/edx/codejail.git@56abd45fcf68c6ab7b523768aa5991d6e04d8241#egg=codejail==3.0.0
-e git+https://github.com/edx/acid-block.git@98aecba94ecbfa934e2d00262741c0ea9f557fc9#egg=acid-xblock
-e git+https://github.com/edx/RateXBlock.git@2.0#egg=rate-xblock
-e git+https://github.com/edx/DoneXBlock.git@2.0.2#egg=done-xblock

View File

@@ -6,7 +6,7 @@
#
-e git+https://github.com/edx/acid-block.git@98aecba94ecbfa934e2d00262741c0ea9f557fc9#egg=acid-xblock # via -r requirements/edx/base.txt
-e common/lib/capa # via -r requirements/edx/base.txt
-e git+https://github.com/edx/codejail.git@4127fc4bd5775cc72aee8d7f0a70e31405e22439#egg=codejail # via -r requirements/edx/base.txt
-e git+https://github.com/edx/codejail.git@56abd45fcf68c6ab7b523768aa5991d6e04d8241#egg=codejail==3.0.0 # via -r requirements/edx/base.txt
-e git+https://github.com/edx/django-wiki.git@0.0.27#egg=django-wiki # via -r requirements/edx/base.txt
-e git+https://github.com/edx/DoneXBlock.git@2.0.2#egg=done-xblock # via -r requirements/edx/base.txt
-e git+https://github.com/jazkarta/edx-jsme.git@690dbf75441fa91c7c4899df0b83d77f7deb5458#egg=edx-jsme # via -r requirements/edx/base.txt
@@ -121,7 +121,7 @@ edx-rest-api-client==5.2.1 # via -r requirements/edx/base.txt, edx-enterprise,
edx-search==1.4.1 # via -r requirements/edx/base.txt
edx-sga==0.11.0 # via -r requirements/edx/base.txt
edx-submissions==3.1.11 # via -r requirements/edx/base.txt, ora2
edx-tincan-py35==0.0.5 # via -r requirements/edx/base.txt, edx-enterprise
edx-tincan-py35==0.0.9 # via -r requirements/edx/base.txt, edx-enterprise
edx-user-state-client==1.2.0 # via -r requirements/edx/base.txt
edx-when==1.2.9 # via -r requirements/edx/base.txt, edx-proctoring
edxval==1.3.8 # via -r requirements/edx/base.txt