diff --git a/common/djangoapps/util/tests/test_db.py b/common/djangoapps/util/tests/test_db.py index 7567a83aba..108b66a827 100644 --- a/common/djangoapps/util/tests/test_db.py +++ b/common/djangoapps/util/tests/test_db.py @@ -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): """ diff --git a/docs/decisions/0005-studio-lms-subdomain-boundaries.rst b/docs/decisions/0005-studio-lms-subdomain-boundaries.rst new file mode 100644 index 0000000000..1090e3f0bb --- /dev/null +++ b/docs/decisions/0005-studio-lms-subdomain-boundaries.rst @@ -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. diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 4beabc17b1..58db73568e 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -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 diff --git a/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0006-enforce-scopes-in-LMS-APIs.rst b/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0006-enforce-scopes-in-LMS-APIs.rst index d9d1679889..547d8a7427 100644 --- a/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0006-enforce-scopes-in-LMS-APIs.rst +++ b/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0006-enforce-scopes-in-LMS-APIs.rst @@ -4,7 +4,7 @@ Status ------ -Proposed +Accepted Context ------- diff --git a/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0007-include-organizations-in-tokens.rst b/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0007-include-organizations-in-tokens.rst index 134bdc4509..5f3e4d181c 100644 --- a/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0007-include-organizations-in-tokens.rst +++ b/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0007-include-organizations-in-tokens.rst @@ -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. diff --git a/openedx/features/calendar_sync/__init__.py b/openedx/features/calendar_sync/__init__.py index 77fd2dd0b2..47f76c832d 100644 --- a/openedx/features/calendar_sync/__init__.py +++ b/openedx/features/calendar_sync/__init__.py @@ -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): diff --git a/openedx/features/calendar_sync/admin.py b/openedx/features/calendar_sync/admin.py new file mode 100644 index 0000000000..02609df7da --- /dev/null +++ b/openedx/features/calendar_sync/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import UserCalendarSyncConfig + +admin.site.register(UserCalendarSyncConfig) diff --git a/openedx/features/calendar_sync/apps.py b/openedx/features/calendar_sync/apps.py new file mode 100644 index 0000000000..ac835080de --- /dev/null +++ b/openedx/features/calendar_sync/apps.py @@ -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 diff --git a/openedx/features/calendar_sync/ics.py b/openedx/features/calendar_sync/ics.py index 706b092768..fc465443e7 100644 --- a/openedx/features/calendar_sync/ics.py +++ b/openedx/features/calendar_sync/ics.py @@ -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 diff --git a/openedx/features/calendar_sync/migrations/0002_auto_20200709_1743.py b/openedx/features/calendar_sync/migrations/0002_auto_20200709_1743.py new file mode 100644 index 0000000000..5b78da35c7 --- /dev/null +++ b/openedx/features/calendar_sync/migrations/0002_auto_20200709_1743.py @@ -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), + ), + ] diff --git a/openedx/features/calendar_sync/models.py b/openedx/features/calendar_sync/models.py index 49608c6a42..b3c817575a 100644 --- a/openedx/features/calendar_sync/models.py +++ b/openedx/features/calendar_sync/models.py @@ -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() diff --git a/openedx/features/calendar_sync/signals.py b/openedx/features/calendar_sync/signals.py new file mode 100644 index 0000000000..86915623f0 --- /dev/null +++ b/openedx/features/calendar_sync/signals.py @@ -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) diff --git a/openedx/features/calendar_sync/tests/factories.py b/openedx/features/calendar_sync/tests/factories.py new file mode 100644 index 0000000000..4240219620 --- /dev/null +++ b/openedx/features/calendar_sync/tests/factories.py @@ -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 diff --git a/openedx/features/calendar_sync/tests/test_ics.py b/openedx/features/calendar_sync/tests/test_ics.py index bd0892ce36..8e053134d1 100644 --- a/openedx/features/calendar_sync/tests/test_ics.py +++ b/openedx/features/calendar_sync/tests/test_ics.py @@ -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))) diff --git a/openedx/features/calendar_sync/utils.py b/openedx/features/calendar_sync/utils.py index 7bade9b7fd..4ecbbf6285 100644 --- a/openedx/features/calendar_sync/utils.py +++ b/openedx/features/calendar_sync/utils.py @@ -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('
{text}
', 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( + '
{bp1}
{bp2}
', + 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('
{text}
', 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('
{text}
', 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: diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index be86ce5c74..efad80c60c 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -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 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index c5ed78ed57..8f7ff7565d 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -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 diff --git a/requirements/edx/github.in b/requirements/edx/github.in index 767e19f786..fcbb4f4044 100644 --- a/requirements/edx/github.in +++ b/requirements/edx/github.in @@ -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 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 4fd02cbf9e..a040b3b7fc 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -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