Commit dfe38973 by Sanford Student Committed by sanfordstudent

use LMS date for course start/end

parent 28f54cfa
......@@ -335,6 +335,7 @@ class CourseRunWithProgramsSerializerTests(TestCase):
"""
ProgramFactory(courses=[self.course_run.course], status=ProgramStatus.Deleted)
serializer = CourseRunWithProgramsSerializer(self.course_run, context=self.serializer_context)
self.assertEqual(serializer.data['programs'], [])
def test_include_deleted_programs(self):
......
......@@ -87,3 +87,46 @@ class LMSAPIClient(object):
exception.__class__.__name__, user.username)
return api_access_request
def get_course_details(self, course_key):
"""
Get details for a course.
Arguments:
course_key (string): LMS identifier for the course.
Returns:
Body consists of the following fields:
* effort: A textual description of the weekly hours of effort expected
in the course.
* end: Date the course ends, in ISO 8601 notation
* enrollment_end: Date enrollment ends, in ISO 8601 notation
* enrollment_start: Date enrollment begins, in ISO 8601 notation
* id: A unique identifier of the course; a serialized representation
of the opaque key identifying the course.
* media: An object that contains named media items. Included here:
* course_image: An image to show for the course. Represented
as an object with the following fields:
* uri: The location of the image
* name: Name of the course
* number: Catalog number of the course
* org: Name of the organization that owns the course
* overview: A possibly verbose HTML textual description of the course.
Note: this field is only included in the Course Detail view, not
the Course List view.
* short_description: A textual description of the course
* start: Date the course begins, in ISO 8601 notation
* start_display: Readably formatted start of the course
* start_type: Hint describing how `start_display` is set. One of:
* `"string"`: manually set by the course author
* `"timestamp"`: generated from the `start` timestamp
* `"empty"`: no start date is specified
* pacing: Course pacing. Possible values: instructor, self
"""
resource = '/api/courses/v1/courses/{}'.format(course_key)
try:
return getattr(self.client, resource).get()
except (SlumberBaseException, ConnectionError, Timeout, KeyError) as exception:
logger.exception('%s: Failed to fetch CourseDetails from LMS for course [%s].',
exception.__class__.__name__, course_key)
......@@ -8,7 +8,9 @@ from django.db import DatabaseError, connection, transaction
from django.http import Http404, JsonResponse
from django.shortcuts import redirect
from django.views.generic import View
from course_discovery.apps.core.constants import Status
try:
import newrelic.agent
except ImportError: # pragma: no cover
......
......@@ -21,6 +21,7 @@ from course_discovery.apps.publisher.tests.factories import (
CourseFactory, CourseRunFactory, CourseRunStateFactory, CourseStateFactory, CourseUserRoleFactory,
OrganizationExtensionFactory, SeatFactory
)
from course_discovery.apps.publisher.tests.utils import MockedStartEndDateTestCase
class CourseUserRoleSerializerTests(SiteMixin, TestCase):
......@@ -292,7 +293,7 @@ class CourseStateSerializerTests(SiteMixin, TestCase):
serializer.update(self.course_state, data)
class CourseRunStateSerializerTests(SiteMixin, TestCase):
class CourseRunStateSerializerTests(SiteMixin, MockedStartEndDateTestCase):
serializer_class = CourseRunStateSerializer
def setUp(self):
......
......@@ -26,6 +26,7 @@ from course_discovery.apps.publisher.models import (
Course, CourseRun, CourseRunState, CourseState, OrganizationExtension, Seat
)
from course_discovery.apps.publisher.tests import JSON_CONTENT_TYPE, factories
from course_discovery.apps.publisher.tests.utils import MockedStartEndDateTestCase
@ddt.ddt
......@@ -588,7 +589,7 @@ class ChangeCourseStateViewTests(SiteMixin, TestCase):
self._assert_email_sent(course_team_user, subject)
class ChangeCourseRunStateViewTests(SiteMixin, TestCase):
class ChangeCourseRunStateViewTests(SiteMixin, MockedStartEndDateTestCase):
def setUp(self):
super(ChangeCourseRunStateViewTests, self).setUp()
......
......@@ -121,7 +121,7 @@ class CourseRunViewSetTests(APITestCase):
log.check((LOGGER_NAME, 'INFO',
'Published course run with id: [{}] lms_course_id: [{}], user: [{}], date: [{}]'.format(
publisher_course_run.id, publisher_course_run.lms_course_id, self.user, date.today())))
assert len(responses.calls) == 3
assert len(responses.calls) == 5
expected = {
'discovery': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS,
'ecommerce': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS,
......@@ -130,7 +130,7 @@ class CourseRunViewSetTests(APITestCase):
assert response.data == expected
# Verify the correct deadlines were sent to the E-Commerce API
ecommerce_body = json.loads(responses.calls[2].request.body)
ecommerce_body = json.loads(responses.calls[4].request.body)
expected = [
serialize_seat_for_ecommerce_api(audit_seat),
serialize_seat_for_ecommerce_api(professional_seat),
......@@ -289,13 +289,16 @@ class CourseRunViewSetTests(APITestCase):
root=partner.studio_url.strip('/'),
key=publisher_course_run.lms_course_id
)
responses.add(responses.PATCH, url, json=expected_error, status=500)
self._mock_ecommerce_api(publisher_course_run)
url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': publisher_course_run.pk})
response = self.client.post(url, {})
assert response.status_code == 502
assert len(responses.calls) == 2
assert len(responses.calls) == 4
expected = {
'discovery': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS,
'ecommerce': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS,
......@@ -317,7 +320,7 @@ class CourseRunViewSetTests(APITestCase):
url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': publisher_course_run.pk})
response = self.client.post(url, {})
assert response.status_code == 502
assert len(responses.calls) == 3
assert len(responses.calls) == 5
expected = {
'discovery': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS,
'ecommerce': 'FAILED: ' + json.dumps(expected_error),
......
......@@ -95,7 +95,7 @@ class CourseRunViewSet(viewsets.GenericViewSet):
'id': course_run.lms_course_id,
'uuid': str(discovery_course.uuid),
'name': course_run.title_override or course_run.course.title,
'verification_deadline': serialize_datetime(course_run.end),
'verification_deadline': serialize_datetime(course_run.lms_end),
}
# NOTE: We only order here to aid testing. The E-Commerce API does NOT care about ordering.
......@@ -146,7 +146,7 @@ class CourseRunViewSet(viewsets.GenericViewSet):
defaults = {
'start': course_run.start,
'end': course_run.end,
'end': course_run.lms_end,
'pacing_type': course_run.pacing_type,
'title_override': course_run.title_override,
'min_effort': course_run.min_effort,
......
......@@ -2,6 +2,7 @@ import datetime
import logging
from urllib.parse import urljoin
import pytz
import waffle
from django.conf import settings
from django.contrib.auth.models import Group
......@@ -18,6 +19,7 @@ from sortedm2m.fields import SortedManyToManyField
from stdimage.models import StdImageField
from taggit.managers import TaggableManager
from course_discovery.apps.core.api_client.lms import LMSAPIClient
from course_discovery.apps.core.models import Currency, User
from course_discovery.apps.course_metadata.choices import CourseRunPacing
from course_discovery.apps.course_metadata.models import Course as DiscoveryCourse
......@@ -28,7 +30,9 @@ from course_discovery.apps.publisher import emails
from course_discovery.apps.publisher.choices import (
CourseRunStateChoices, CourseStateChoices, InternalUserRole, PublisherUserRole
)
from course_discovery.apps.publisher.utils import is_email_notification_enabled, is_internal_user, is_publisher_admin
from course_discovery.apps.publisher.utils import (
is_email_notification_enabled, is_internal_user, is_publisher_admin, parse_datetime_field
)
from course_discovery.apps.publisher.validators import ImageMultiSizeValidator
logger = logging.getLogger(__name__)
......@@ -355,13 +359,33 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
history = HistoricalRecords()
def __str__(self):
return '{course}: {start_date}'.format(course=self.course.title, start_date=self.start)
return '{course}: {lms_course_id}'.format(course=self.course.title, lms_course_id=self.lms_course_id)
@property
def post_back_url(self):
return reverse('publisher:publisher_course_runs_edit', kwargs={'pk': self.id})
@property
def lms_start(self):
if self.course.partner and self.course.partner.site:
lms = LMSAPIClient(self.course.partner.site)
details = lms.get_course_details(self.lms_course_id)
if details and details['start']:
return pytz.utc.localize(parse_datetime_field(details['start']))
return self.start
@property
def lms_end(self):
if self.course.partner and self.course.partner.site:
lms = LMSAPIClient(self.course.partner.site)
details = lms.get_course_details(self.lms_course_id)
if details and details['end']:
return pytz.utc.localize(parse_datetime_field(details['end']))
return self.end
@property
def created_by(self):
history_user = self.history.order_by('history_date').first().history_user # pylint: disable=no-member
if history_user:
......@@ -497,7 +521,7 @@ class Seat(TimeStampedModel, ChangedByMixin):
if self.upgrade_deadline:
return self.upgrade_deadline
deadline = self.course_run.end - datetime.timedelta(days=settings.PUBLISHER_UPGRADE_DEADLINE_DAYS)
deadline = self.course_run.lms_end - datetime.timedelta(days=settings.PUBLISHER_UPGRADE_DEADLINE_DAYS)
deadline = deadline.replace(hour=23, minute=59, second=59, microsecond=99999)
return deadline
......@@ -800,8 +824,8 @@ class CourseRunState(TimeStampedModel, ChangedByMixin):
"""
course_run = self.course_run
return all([
course_run.course.course_state.is_approved, course_run.has_valid_seats, course_run.start, course_run.end,
course_run.pacing_type, course_run.has_valid_staff, course_run.is_valid_micromasters,
course_run.course.course_state.is_approved, course_run.has_valid_seats, course_run.lms_start,
course_run.lms_end, course_run.pacing_type, course_run.has_valid_staff, course_run.is_valid_micromasters,
course_run.is_valid_professional_certificate, course_run.is_valid_xseries, course_run.language,
course_run.transcript_languages.all(), course_run.lms_course_id, course_run.min_effort,
course_run.video_language, course_run.length
......
......@@ -20,21 +20,22 @@ from course_discovery.apps.publisher.models import (
Course, CourseUserRole, OrganizationExtension, OrganizationUserRole, Seat
)
from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher.tests.utils import MockedStartEndDateTestCase
@ddt.ddt
class CourseRunTests(TestCase):
class CourseRunTests(MockedStartEndDateTestCase):
@classmethod
def setUpClass(cls):
super(CourseRunTests, cls).setUpClass()
cls.course_run = factories.CourseRunFactory()
def test_str(self):
""" Verify casting an instance to a string returns a string containing the course title and start date. """
""" Verify casting an instance to a string returns a string containing the course title and LMS ID. """
self.assertEqual(
str(self.course_run),
'{title}: {date}'.format(
title=self.course_run.course.title, date=self.course_run.start
'{title}: {lms_course_id}'.format(
title=self.course_run.course.title, lms_course_id=self.course_run.lms_course_id
)
)
......@@ -368,7 +369,7 @@ class CourseTests(TestCase):
@pytest.mark.django_db
class TestSeatModel:
class TestSeatModel():
def test_str(self):
seat = factories.SeatFactory()
assert str(seat) == '{course}: {type}'.format(course=seat.course_run.course.title, type=seat.type)
......@@ -383,6 +384,7 @@ class TestSeatModel:
settings.PUBLISHER_UPGRADE_DEADLINE_DAYS = random.randint(1, 21)
now = datetime.datetime.utcnow()
seat = factories.SeatFactory(type=Seat.VERIFIED, upgrade_deadline=None, course_run__end=now)
expected = now - datetime.timedelta(days=settings.PUBLISHER_UPGRADE_DEADLINE_DAYS)
expected = expected.replace(hour=23, minute=59, second=59, microsecond=99999)
assert seat.calculated_upgrade_deadline == expected
......@@ -626,7 +628,7 @@ class CourseStateTests(TestCase):
@ddt.ddt
class CourseRunStateTests(TestCase):
class CourseRunStateTests:
""" Tests for the publisher `CourseRunState` model. """
@classmethod
......
......@@ -43,7 +43,7 @@ from course_discovery.apps.publisher.models import (
Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, OrganizationExtension, Seat
)
from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher.tests.utils import create_non_staff_user_and_login
from course_discovery.apps.publisher.tests.utils import MockedStartEndDateTestCase, create_non_staff_user_and_login
from course_discovery.apps.publisher.utils import is_email_notification_enabled
from course_discovery.apps.publisher.views import logger as publisher_views_logger
from course_discovery.apps.publisher.views import (
......@@ -55,7 +55,7 @@ from course_discovery.apps.publisher_comments.tests.factories import CommentFact
@ddt.ddt
class CreateCourseViewTests(SiteMixin, TestCase):
class CreateCourseViewTests(SiteMixin, MockedStartEndDateTestCase):
""" Tests for the publisher `CreateCourseView`. """
def setUp(self):
......@@ -359,7 +359,7 @@ class CreateCourseViewTests(SiteMixin, TestCase):
@ddt.ddt
class CreateCourseRunViewTests(SiteMixin, TestCase):
class CreateCourseRunViewTests(SiteMixin, MockedStartEndDateTestCase):
""" Tests for the publisher `CreateCourseRunView`. """
def setUp(self):
......@@ -840,7 +840,7 @@ class CreateCourseRunViewTests(SiteMixin, TestCase):
@ddt.ddt
class CourseRunDetailTests(SiteMixin, TestCase):
class CourseRunDetailTests(SiteMixin, MockedStartEndDateTestCase):
""" Tests for the course-run detail view. """
def setUp(self):
......@@ -1185,7 +1185,7 @@ class CourseRunDetailTests(SiteMixin, TestCase):
self.assertContains(
response, '{type}: {start}'.format(
type=course_run.get_pacing_type_display(),
start=course_run.start.strftime("%B %d, %Y")
start=course_run.lms_start.strftime("%B %d, %Y")
)
)
......@@ -1534,7 +1534,7 @@ class CourseRunDetailTests(SiteMixin, TestCase):
# pylint: disable=attribute-defined-outside-init
@ddt.ddt
class CourseRunListViewTests(SiteMixin, TestCase):
class CourseRunListViewTests(SiteMixin, MockedStartEndDateTestCase):
def setUp(self):
super(CourseRunListViewTests, self).setUp()
Site.objects.exclude(id=self.site.id).delete()
......@@ -2671,7 +2671,7 @@ class CourseDetailViewTests(TestCase):
@ddt.ddt
class CourseEditViewTests(SiteMixin, TestCase):
class CourseEditViewTests(SiteMixin, MockedStartEndDateTestCase):
""" Tests for the course edit view. """
def setUp(self):
......@@ -3357,7 +3357,7 @@ class CourseEditViewTests(SiteMixin, TestCase):
@ddt.ddt
class CourseRunEditViewTests(SiteMixin, TestCase):
class CourseRunEditViewTests(SiteMixin, MockedStartEndDateTestCase):
""" Tests for the course run edit view. """
def setUp(self):
......@@ -4075,7 +4075,7 @@ class CourseRevisionViewTests(SiteMixin, TestCase):
@ddt.ddt
class CreateRunFromDashboardViewTests(SiteMixin, TestCase):
class CreateRunFromDashboardViewTests(SiteMixin, MockedStartEndDateTestCase):
""" Tests for the publisher `CreateRunFromDashboardView`. """
def setUp(self):
......
import datetime
import mock
from django.test import TestCase
from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory
from course_discovery.apps.publisher.tests import factories
class MockedStartEndDateTestCase(TestCase):
def setUp(self):
super(MockedStartEndDateTestCase, self).setUp()
start_date_patcher = mock.patch(
'course_discovery.apps.publisher.models.CourseRun.lms_start', new_callable=mock.PropertyMock
)
self.addCleanup(start_date_patcher.stop)
self.start_date_mock = start_date_patcher.start()
self.start_date_mock.return_value = datetime.datetime.utcnow()
end_date_patcher = mock.patch(
'course_discovery.apps.publisher.models.CourseRun.lms_end', new_callable=mock.PropertyMock
)
self.addCleanup(end_date_patcher.stop)
self.end_date_mock = end_date_patcher.start()
self.end_date_mock.return_value = datetime.datetime.utcnow()
def create_non_staff_user_and_login(test_class):
""" Create non staff user and login and return user and group. """
non_staff_user = UserFactory()
......
......@@ -195,7 +195,7 @@ class CourseRunDetailView(mixins.LoginRequiredMixin, mixins.PublisherPermissionM
if history_object:
context['publish_date'] = history_object.modified
start_date = course_run.start.strftime("%B %d, %Y") if course_run.start else None
start_date = course_run.lms_start.strftime("%B %d, %Y") if course_run.lms_start else None
context['breadcrumbs'] = make_bread_crumbs(
[
(reverse('publisher:publisher_courses'), _('Courses')),
......@@ -430,7 +430,7 @@ class CourseEditView(mixins.PublisherPermissionMixin, UpdateView):
published_runs = set()
for course_run in self._get_active_course_runs(course):
if course_run.course_run_state.is_published:
start_date = course_run.start.strftime("%B %d, %Y") if course_run.start else None
start_date = course_run.lms_start.strftime("%B %d, %Y") if course_run.lms_start else None
published_runs.add('{type} - {start}'.format(
type=course_run.get_pacing_type_display(),
start=start_date
......@@ -460,12 +460,12 @@ class CourseEditView(mixins.PublisherPermissionMixin, UpdateView):
if not type_is_valid:
misconfigured_seat_type_runs.add('{type} - {start}'.format(
type=course_run.get_pacing_type_display(),
start=course_run.start.strftime("%B %d, %Y")
start=course_run.lms_start.strftime("%B %d, %Y")
))
if not price_is_valid:
misconfigured_price_runs.add('{type} - {start}'.format(
type=course_run.get_pacing_type_display(),
start=course_run.start.strftime("%B %d, %Y")
start=course_run.lms_start.strftime("%B %d, %Y")
))
return misconfigured_price_runs, misconfigured_seat_type_runs
......@@ -1005,7 +1005,7 @@ class CourseRunEditView(mixins.LoginRequiredMixin, mixins.PublisherPermissionMix
course_run_seat = self.get_latest_course_run_seat(course_run)
context['seat_form'] = self.seat_form(instance=course_run_seat)
start_date = course_run.start.strftime("%B %d, %Y") if course_run.start else None
start_date = course_run.lms_start.strftime("%B %d, %Y") if course_run.lms_start else None
context['breadcrumbs'] = make_bread_crumbs(
[
(reverse('publisher:publisher_courses'), 'Courses'),
......
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