Commit 486b157d by Matthew Piatetsky

Allow course length in program to be represented as a range of weeks

ECOM-5946,ECOM-6019,ECOM-6020,ECOM-6021,ECOM-6024
parent e60e33c9
......@@ -674,10 +674,11 @@ class ProgramSerializer(MinimalProgramSerializer):
class Meta(MinimalProgramSerializer.Meta):
model = Program
fields = MinimalProgramSerializer.Meta.fields + (
'overview', 'weeks_to_complete', 'min_hours_effort_per_week', 'max_hours_effort_per_week', 'video',
'expected_learning_items', 'faq', 'credit_backing_organizations', 'corporate_endorsements',
'job_outlook_items', 'individual_endorsements', 'languages', 'transcript_languages', 'subjects',
'price_ranges', 'staff', 'credit_redemption_overview',
'overview', 'weeks_to_complete', 'weeks_to_complete_min', 'weeks_to_complete_max',
'min_hours_effort_per_week', 'max_hours_effort_per_week', 'video', 'expected_learning_items',
'faq', 'credit_backing_organizations', 'corporate_endorsements', 'job_outlook_items',
'individual_endorsements', 'languages', 'transcript_languages', 'subjects', 'price_ranges',
'staff', 'credit_redemption_overview',
)
......
......@@ -592,6 +592,8 @@ class ProgramSerializerTests(MinimalProgramSerializerTests):
'job_outlook_items': [item.value for item in program.job_outlook_items.all()],
'languages': [serialize_language_to_code(l) for l in program.languages],
'weeks_to_complete': program.weeks_to_complete,
'weeks_to_complete_min': program.weeks_to_complete_min,
'weeks_to_complete_max': program.weeks_to_complete_max,
'max_hours_effort_per_week': program.max_hours_effort_per_week,
'min_hours_effort_per_week': program.min_hours_effort_per_week,
'overview': program.overview,
......
......@@ -66,7 +66,7 @@ class ProgramViewSetTests(SerializationMixin, APITestCase):
def test_retrieve(self):
""" Verify the endpoint returns the details for a single program. """
program = self.create_program()
with self.assertNumQueries(72):
with self.assertNumQueries(75):
self.assert_retrieve_success(program)
@ddt.data(True, False)
......@@ -76,7 +76,7 @@ class ProgramViewSetTests(SerializationMixin, APITestCase):
for course in course_list:
CourseRunFactory(course=course)
program = ProgramFactory(courses=course_list, order_courses_by_start_date=order_courses_by_start_date)
with self.assertNumQueries(82):
with self.assertNumQueries(87):
self.assert_retrieve_success(program)
self.assertEqual(course_list, list(program.courses.all())) # pylint: disable=no-member
......@@ -84,7 +84,7 @@ class ProgramViewSetTests(SerializationMixin, APITestCase):
""" Verify the endpoint returns data for a program even if the program's courses have no course runs. """
course = CourseFactory()
program = ProgramFactory(courses=[course])
with self.assertNumQueries(46):
with self.assertNumQueries(49):
self.assert_retrieve_success(program)
def assert_list_results(self, url, expected, expected_query_count, extra_context=None):
......
......@@ -5,6 +5,7 @@ import logging
from urllib.parse import parse_qs, urlencode, urlparse
from uuid import UUID
from dateutil import rrule
import pytz
import requests
from django.db.models import Q
......@@ -449,6 +450,9 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
language = language_tags[0] if language_tags else None
start = data.get('field_course_start_date')
start = datetime.datetime.fromtimestamp(int(start), tz=pytz.UTC) if start else None
end = data.get('field_course_end_date')
end = datetime.datetime.fromtimestamp(int(end), tz=pytz.UTC) if end else None
weeks_to_complete = data.get('field_course_required_weeks')
defaults = {
'key': key,
......@@ -464,6 +468,12 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
'hidden': self.get_hidden(data),
}
if weeks_to_complete:
defaults['weeks_to_complete'] = int(weeks_to_complete)
elif start and end:
weeks_to_complete = rrule.rrule(rrule.WEEKLY, dtstart=start, until=end).count()
defaults['weeks_to_complete'] = int(weeks_to_complete)
try:
course_run, __ = CourseRun.objects.update_or_create(key__iexact=key, defaults=defaults)
except TypeError:
......
......@@ -4,6 +4,7 @@ import math
from urllib.parse import parse_qs, urlparse
from uuid import UUID
from dateutil import rrule
import ddt
import mock
import pytz
......@@ -470,6 +471,9 @@ class CourseMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi
language = self.loader.get_language_tags_from_names(language_names).first()
start = data.get('field_course_start_date')
start = datetime.datetime.fromtimestamp(int(start), tz=pytz.UTC) if start else None
end = data.get('field_course_end_date')
end = datetime.datetime.fromtimestamp(int(end), tz=pytz.UTC) if end else None
weeks_to_complete = data.get('field_course_required_weeks')
expected_values = {
'key': data['field_course_id'],
......@@ -483,6 +487,12 @@ class CourseMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi
'hidden': self.loader.get_hidden(data),
}
if weeks_to_complete:
expected_values['weeks_to_complete'] = int(weeks_to_complete)
elif start and end:
weeks_to_complete = rrule.rrule(rrule.WEEKLY, dtstart=start, until=end).count()
expected_values['weeks_to_complete'] = int(weeks_to_complete)
for field, value in expected_values.items():
self.assertEqual(getattr(course_run, field), value)
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course_metadata', '0030_create_refresh_command_switches'),
]
operations = [
migrations.AddField(
model_name='courserun',
name='weeks_to_complete',
field=models.PositiveSmallIntegerField(null=True, blank=True, help_text='This field is now deprecated (ECOM-6021).Estimated number of weeks needed to complete a course run.'),
),
]
......@@ -9,6 +9,7 @@ import pytz
import waffle
from django.db import models, transaction
from django.db.models.query_utils import Q
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django_extensions.db.fields import AutoSlugField
from django_extensions.db.models import TimeStampedModel
......@@ -352,6 +353,9 @@ class CourseRun(TimeStampedModel):
max_effort = models.PositiveSmallIntegerField(
null=True, blank=True,
help_text=_('Estimated maximum number of hours per week needed to complete a course run.'))
weeks_to_complete = models.PositiveSmallIntegerField(
null=True, blank=True,
help_text=_('Estimated number of weeks needed to complete this course run.'))
language = models.ForeignKey(LanguageTag, null=True, blank=True)
transcript_languages = models.ManyToManyField(LanguageTag, blank=True, related_name='transcript_courses')
pacing_type = models.CharField(max_length=255, db_index=True, null=True, blank=True,
......@@ -599,7 +603,11 @@ class Program(TimeStampedModel):
excluded_course_runs = models.ManyToManyField(CourseRun, blank=True)
partner = models.ForeignKey(Partner, null=True, blank=False)
overview = models.TextField(null=True, blank=True)
weeks_to_complete = models.PositiveSmallIntegerField(null=True, blank=True)
# The weeks_to_complete field is now deprecated
weeks_to_complete = models.PositiveSmallIntegerField(
null=True, blank=True,
help_text=_('This field is now deprecated (ECOM-6021).'
'Estimated number of weeks needed to complete a course run belonging to this program.'))
min_hours_effort_per_week = models.PositiveSmallIntegerField(null=True, blank=True)
max_hours_effort_per_week = models.PositiveSmallIntegerField(null=True, blank=True)
authoring_organizations = SortedManyToManyField(Organization, blank=True, related_name='authored_programs')
......@@ -636,6 +644,19 @@ class Program(TimeStampedModel):
def __str__(self):
return self.title
@cached_property
def _course_run_weeks_to_complete(self):
return [course_run.weeks_to_complete for course_run in self.course_runs
if course_run.weeks_to_complete is not None]
@property
def weeks_to_complete_min(self):
return min(self._course_run_weeks_to_complete) if self._course_run_weeks_to_complete else None
@property
def weeks_to_complete_max(self):
return max(self._course_run_weeks_to_complete) if self._course_run_weeks_to_complete else None
@property
def marketing_url(self):
if self.marketing_slug:
......
......@@ -126,6 +126,7 @@ class CourseRunFactory(factory.DjangoModelFactory):
max_effort = FuzzyInteger(10, 20)
pacing_type = FuzzyChoice([name for name, __ in CourseRunPacing.choices])
slug = FuzzyText()
weeks_to_complete = FuzzyInteger(1)
@factory.post_generation
def staff(self, create, extracted, **kwargs):
......
......@@ -311,6 +311,14 @@ class ProgramTests(MarketingSitePublisherTestMixin):
"""Verify that a program is properly converted to a str."""
self.assertEqual(str(self.program), self.program.title)
def test_weeks_to_complete_range(self):
""" Verify that weeks to complete range works correctly """
weeks_to_complete_values = [course_run.weeks_to_complete for course_run in self.course_runs]
expected_min = min(weeks_to_complete_values) if weeks_to_complete_values else None
expected_max = max(weeks_to_complete_values) if weeks_to_complete_values else None
self.assertEqual(self.program.weeks_to_complete_min, expected_min)
self.assertEqual(self.program.weeks_to_complete_max, expected_max)
def test_marketing_url(self):
""" Verify the property creates a complete marketing URL. """
expected = '{root}/{type}/{slug}'.format(root=self.program.partner.marketing_site_url_root.strip('/'),
......
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-10-13 14:43+0000\n"
"POT-Creation-Date: 2016-10-19 18:33+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -28,31 +28,31 @@ msgid ""
"parameter."
msgstr ""
#: course_discovery/apps/api/serializers.py:292
#: course_discovery/apps/api/serializers.py:293
msgid "Number of courses contained in this catalog"
msgstr ""
#: course_discovery/apps/api/serializers.py:295
#: course_discovery/apps/api/serializers.py:296
msgid "Usernames of users with explicit access to view this catalog"
msgstr ""
#: course_discovery/apps/api/serializers.py:351
#: course_discovery/apps/api/serializers.py:352
msgid "Language in which the course is administered"
msgstr ""
#: course_discovery/apps/api/serializers.py:400
#: course_discovery/apps/api/serializers.py:407
msgid "Dictionary mapping course run IDs to boolean values"
msgstr ""
#: course_discovery/apps/api/serializers.py:480
#: course_discovery/apps/api/serializers.py:495
msgid "Dictionary mapping course IDs to boolean values"
msgstr ""
#: course_discovery/apps/api/serializers.py:623
#: course_discovery/apps/api/serializers.py:638
msgid "Languages that course runs in this program are offered in."
msgstr ""
#: course_discovery/apps/api/serializers.py:627
#: course_discovery/apps/api/serializers.py:642
msgid ""
"Languages that course runs in this program have available transcripts in."
msgstr ""
......@@ -231,128 +231,132 @@ msgstr ""
msgid "Deleted"
msgstr ""
#: course_discovery/apps/course_metadata/forms.py:76
#: course_discovery/apps/course_metadata/forms.py:79
msgid ""
"Programs can only be activated if they have a marketing slug and a banner "
"image."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:74
#: course_discovery/apps/course_metadata/models.py:75
msgid "Facebook"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:75
#: course_discovery/apps/course_metadata/models.py:76
msgid "Twitter"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:76
#: course_discovery/apps/course_metadata/models.py:77
msgid "Blog"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:77
#: course_discovery/apps/course_metadata/models.py:78
msgid "Others"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:111
#: course_discovery/apps/course_metadata/models.py:155
#: course_discovery/apps/course_metadata/models.py:185
#: course_discovery/apps/course_metadata/models.py:233
#: course_discovery/apps/course_metadata/models.py:324
#: course_discovery/apps/course_metadata/models.py:574
#: course_discovery/apps/course_metadata/models.py:112
#: course_discovery/apps/course_metadata/models.py:156
#: course_discovery/apps/course_metadata/models.py:186
#: course_discovery/apps/course_metadata/models.py:234
#: course_discovery/apps/course_metadata/models.py:325
#: course_discovery/apps/course_metadata/models.py:578
msgid "UUID"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:118
#: course_discovery/apps/course_metadata/models.py:119
msgid "Leave this field blank to have the value generated automatically."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:197
#: course_discovery/apps/course_metadata/models.py:198
msgid "People"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:249
#: course_discovery/apps/course_metadata/models.py:250
msgid "Course number format e.g CS002x, BIO1.1x, BIO1.2x"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:332
#: course_discovery/apps/course_metadata/models.py:333
msgid ""
"Title specific for this run of a course. Leave this value blank to default "
"to the parent course's title."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:341
#: course_discovery/apps/course_metadata/models.py:342
msgid ""
"Short description specific for this run of a course. Leave this value blank "
"to default to the parent course's short_description attribute."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:346
#: course_discovery/apps/course_metadata/models.py:347
msgid ""
"Full description specific for this run of a course. Leave this value blank "
"to default to the parent course's full_description attribute."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:351
#: course_discovery/apps/course_metadata/models.py:352
#: course_discovery/apps/publisher/models.py:193
msgid ""
"Estimated minimum number of hours per week needed to complete a course run."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:354
#: course_discovery/apps/course_metadata/models.py:355
#: course_discovery/apps/publisher/models.py:196
msgid ""
"Estimated maximum number of hours per week needed to complete a course run."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:461
#: course_discovery/apps/course_metadata/models.py:358
msgid "Estimated number of weeks needed to complete this course run."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:465
msgid "Archived"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:463
#: course_discovery/apps/course_metadata/models.py:467
msgid "Current"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:465
#: course_discovery/apps/course_metadata/models.py:469
msgid "Starting Soon"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:467
#: course_discovery/apps/course_metadata/models.py:471
msgid "Upcoming"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:503
#: course_discovery/apps/course_metadata/models.py:507
#: course_discovery/apps/publisher/models.py:280
msgid "Honor"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:504
#: course_discovery/apps/course_metadata/models.py:508
#: course_discovery/apps/publisher/models.py:281
msgid "Audit"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:505
#: course_discovery/apps/course_metadata/models.py:509
#: course_discovery/apps/publisher/models.py:282
msgid "Verified"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:506
#: course_discovery/apps/course_metadata/models.py:510
msgid "Professional"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:507
#: course_discovery/apps/course_metadata/models.py:511
#: course_discovery/apps/publisher/models.py:285
msgid "Credit"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:554
#: course_discovery/apps/course_metadata/models.py:558
msgid "FAQ"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:555
#: course_discovery/apps/course_metadata/models.py:559
msgid "FAQs"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:564
#: course_discovery/apps/course_metadata/models.py:568
msgid ""
"Seat types that qualify for completion of programs of this type. Learners "
"completing associated courses, but enrolled in other seat types, will NOT "
......@@ -360,37 +364,43 @@ msgid ""
"program."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:576
#: course_discovery/apps/course_metadata/models.py:580
msgid "The user-facing display title for this Program."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:578
#: course_discovery/apps/course_metadata/models.py:582
msgid "A brief, descriptive subtitle for the Program."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:581
#: course_discovery/apps/course_metadata/models.py:585
msgid "The lifecycle status of this Program."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:585
#: course_discovery/apps/course_metadata/models.py:589
msgid "Slug used to generate links to the marketing site"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:589
#: course_discovery/apps/course_metadata/models.py:593
msgid ""
"If this box is not checked, courses will be ordered as in the courses select "
"box above."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:611
#: course_discovery/apps/course_metadata/models.py:603
msgid ""
"This field is now deprecated (ECOM-6021).Estimated number of weeks needed to "
"complete a course run belonging to this program."
msgstr ""
#: course_discovery/apps/course_metadata/models.py:619
msgid "Image used atop detail pages"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:612
#: course_discovery/apps/course_metadata/models.py:620
msgid "Image used for discovery cards"
msgstr ""
#: course_discovery/apps/course_metadata/models.py:624
#: course_discovery/apps/course_metadata/models.py:632
msgid "The description of credit redemption for courses in program"
msgstr ""
......
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-10-13 14:43+0000\n"
"POT-Creation-Date: 2016-10-19 18:34+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-10-13 14:43+0000\n"
"POT-Creation-Date: 2016-10-19 18:33+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -376,6 +376,12 @@ msgstr ""
"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυ#"
#: course_discovery/apps/course_metadata/models.py
msgid "Estimated number of weeks needed to complete this course run."
msgstr ""
"Éstïmätéd nümßér öf wééks néédéd tö çömplété thïs çöürsé rün. Ⱡ'σяєм ιρѕυм "
"∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
#: course_discovery/apps/course_metadata/models.py
msgid "Archived"
msgstr "Àrçhïvéd Ⱡ'σяєм ιρѕυм ∂#"
......@@ -472,6 +478,14 @@ msgstr ""
" ßöx äßövé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"
#: course_discovery/apps/course_metadata/models.py
msgid ""
"This field is now deprecated (ECOM-6021).Estimated number of weeks needed to"
" complete a course run belonging to this program."
msgstr ""
"Thïs fïéld ïs nöw dépréçätéd (ÉÇÖM-6021).Éstïmätéd nümßér öf wééks néédéd tö"
" çömplété ä çöürsé rün ßélöngïng tö thïs prögräm. Ⱡ'σяєм #"
#: course_discovery/apps/course_metadata/models.py
msgid "Image used atop detail pages"
msgstr "Ìmägé üséd ätöp détäïl pägés Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-10-13 14:43+0000\n"
"POT-Creation-Date: 2016-10-19 18:34+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment