Commit 1af53b13 by Waheed Ahmed Committed by GitHub

Merge pull request #219 from edx/waheed/ecom-4979-create-workflow-states

Create workflow states
parents f8bde79f c89826d0
from django.contrib import admin from django.contrib import admin
from course_discovery.apps.publisher.models import Course, CourseRun, Seat from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State
admin.site.register(Course) admin.site.register(Course)
admin.site.register(CourseRun) admin.site.register(CourseRun)
admin.site.register(Seat) admin.site.register(Seat)
admin.site.register(State)
...@@ -45,6 +45,7 @@ class CourseRunForm(BaseCourseForm): ...@@ -45,6 +45,7 @@ class CourseRunForm(BaseCourseForm):
class Meta: class Meta:
model = CourseRun model = CourseRun
fields = '__all__' fields = '__all__'
exclude = ('state',)
class SeatForm(BaseCourseForm): class SeatForm(BaseCourseForm):
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion
import django_fsm
import course_discovery.apps.publisher.models
import django_extensions.db.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('publisher', '0003_auto_20160801_1757'),
]
operations = [
migrations.CreateModel(
name='HistoricalState',
fields=[
('id', models.IntegerField(blank=True, auto_created=True, verbose_name='ID', db_index=True)),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('name', django_fsm.FSMField(choices=[('draft', 'Draft'), ('needs_review', 'Needs Review'), ('needs_final_approval', 'Needs Final Approval'), ('finalized', 'Finalized'), ('published', 'Published')], default='draft', max_length=50)),
('history_id', models.AutoField(serialize=False, primary_key=True)),
('history_date', models.DateTimeField()),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True, related_name='+')),
],
options={
'get_latest_by': 'history_date',
'ordering': ('-history_date', '-history_id'),
'verbose_name': 'historical state',
},
),
migrations.CreateModel(
name='State',
fields=[
('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('name', django_fsm.FSMField(choices=[('draft', 'Draft'), ('needs_review', 'Needs Review'), ('needs_final_approval', 'Needs Final Approval'), ('finalized', 'Finalized'), ('published', 'Published')], default='draft', max_length=50)),
],
options={
'get_latest_by': 'modified',
'ordering': ('-modified', '-created'),
'abstract': False,
},
bases=(models.Model, course_discovery.apps.publisher.models.ChangedByMixin),
),
migrations.AddField(
model_name='courserun',
name='state',
field=models.ForeignKey(blank=True, to='publisher.State', null=True),
),
migrations.AddField(
model_name='historicalcourserun',
name='state',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, db_constraint=False, blank=True, to='publisher.State', related_name='+', null=True),
),
]
import logging import logging
from django.db import models from django.db import models
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_extensions.db.models import TimeStampedModel from django_extensions.db.models import TimeStampedModel
from django_fsm import FSMField, transition
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
...@@ -18,6 +21,55 @@ class ChangedByMixin(object): ...@@ -18,6 +21,55 @@ class ChangedByMixin(object):
changed_by = models.ForeignKey(User, null=True, blank=True) changed_by = models.ForeignKey(User, null=True, blank=True)
class State(TimeStampedModel, ChangedByMixin):
""" Publisher Workflow State Model. """
DRAFT = 'draft'
NEEDS_REVIEW = 'needs_review'
NEEDS_FINAL_APPROVAL = 'needs_final_approval'
FINALIZED = 'finalized'
PUBLISHED = 'published'
CHOICES = (
(DRAFT, _('Draft')),
(NEEDS_REVIEW, _('Needs Review')),
(NEEDS_FINAL_APPROVAL, _('Needs Final Approval')),
(FINALIZED, _('Finalized')),
(PUBLISHED, _('Published'))
)
name = FSMField(default=DRAFT, choices=CHOICES)
history = HistoricalRecords()
def __str__(self):
return self.get_name_display()
@transition(field=name, source='*', target=DRAFT)
def draft(self):
# TODO: send email etc.
pass
@transition(field=name, source=DRAFT, target=NEEDS_REVIEW)
def needs_review(self):
# TODO: send email etc.
pass
@transition(field=name, source=NEEDS_REVIEW, target=NEEDS_FINAL_APPROVAL)
def needs_final_approval(self):
# TODO: send email etc.
pass
@transition(field=name, source=NEEDS_FINAL_APPROVAL, target=FINALIZED)
def finalized(self):
# TODO: send email etc.
pass
@transition(field=name, source=FINALIZED, target=PUBLISHED)
def publish(self):
# TODO: send email etc.
pass
class Course(TimeStampedModel, ChangedByMixin): class Course(TimeStampedModel, ChangedByMixin):
""" Publisher Course model. It contains fields related to the course intake form.""" """ Publisher Course model. It contains fields related to the course intake form."""
...@@ -70,6 +122,8 @@ class CourseRun(TimeStampedModel, ChangedByMixin): ...@@ -70,6 +122,8 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
(PRIORITY_LEVEL_5, _('Level 5')), (PRIORITY_LEVEL_5, _('Level 5')),
) )
state = models.ForeignKey(State, null=True, blank=True)
course = models.ForeignKey(Course, related_name='publisher_course_runs') course = models.ForeignKey(Course, related_name='publisher_course_runs')
lms_course_id = models.CharField(max_length=255, unique=True, null=True, blank=True) lms_course_id = models.CharField(max_length=255, unique=True, null=True, blank=True)
...@@ -132,6 +186,24 @@ class CourseRun(TimeStampedModel, ChangedByMixin): ...@@ -132,6 +186,24 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
def __str__(self): def __str__(self):
return '{course}: {start_date}'.format(course=self.course.title, start_date=self.start) return '{course}: {start_date}'.format(course=self.course.title, start_date=self.start)
def change_state(self, target=State.DRAFT):
if target == State.NEEDS_REVIEW:
self.state.needs_review()
elif target == State.NEEDS_FINAL_APPROVAL:
self.state.needs_final_approval()
elif target == State.FINALIZED:
self.state.finalized()
elif target == State.PUBLISHED:
self.state.publish()
else:
self.state.draft()
self.state.save()
@property
def current_state(self):
return self.state.get_name_display()
class Seat(TimeStampedModel, ChangedByMixin): class Seat(TimeStampedModel, ChangedByMixin):
""" Seat model. """ """ Seat model. """
...@@ -169,3 +241,17 @@ class Seat(TimeStampedModel, ChangedByMixin): ...@@ -169,3 +241,17 @@ class Seat(TimeStampedModel, ChangedByMixin):
def __str__(self): def __str__(self):
return '{course}: {type}'.format(course=self.course_run.course.title, type=self.type) return '{course}: {type}'.format(course=self.course_run.course.title, type=self.type)
@receiver(pre_save, sender=CourseRun)
def initialize_workflow(sender, instance, **kwargs): # pylint: disable=unused-argument
""" Create Workflow State For CourseRun Before Saving. """
create_workflow_state(instance)
def create_workflow_state(course_run):
""" Create Workflow State If Not Present."""
if not course_run.state:
state = State()
state.save()
course_run.state = state
...@@ -8,7 +8,13 @@ from course_discovery.apps.core.models import Currency ...@@ -8,7 +8,13 @@ from course_discovery.apps.core.models import Currency
from course_discovery.apps.course_metadata.tests import factories from course_discovery.apps.course_metadata.tests import factories
from course_discovery.apps.course_metadata.models import CourseRun as CourseMetadataCourseRun from course_discovery.apps.course_metadata.models import CourseRun as CourseMetadataCourseRun
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.models import Course, CourseRun, Seat from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State
class StateFactory(factory.DjangoModelFactory):
class Meta:
model = State
class CourseFactory(factory.DjangoModelFactory): class CourseFactory(factory.DjangoModelFactory):
...@@ -32,6 +38,7 @@ class CourseFactory(factory.DjangoModelFactory): ...@@ -32,6 +38,7 @@ class CourseFactory(factory.DjangoModelFactory):
class CourseRunFactory(factory.DjangoModelFactory): class CourseRunFactory(factory.DjangoModelFactory):
course = factory.SubFactory(CourseFactory) course = factory.SubFactory(CourseFactory)
state = factory.SubFactory(StateFactory)
start = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC)) start = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC))
end = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC)).end_dt end = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC)).end_dt
enrollment_start = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC)) enrollment_start = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC))
......
# pylint: disable=no-member # pylint: disable=no-member
import ddt
from django.test import TestCase from django.test import TestCase
from django_fsm import TransitionNotAllowed
from course_discovery.apps.publisher.models import State
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
@ddt.ddt
class CourseRunTests(TestCase): class CourseRunTests(TestCase):
""" Tests for the publisher `CourseRun` model. """ """ Tests for the publisher `CourseRun` model. """
def setUp(self): @classmethod
super(CourseRunTests, self).setUp() def setUpClass(cls):
self.course_run = factories.CourseRunFactory() super(CourseRunTests, cls).setUpClass()
cls.course_run = factories.CourseRunFactory()
def test_str(self): 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 start date. """
...@@ -20,6 +25,26 @@ class CourseRunTests(TestCase): ...@@ -20,6 +25,26 @@ class CourseRunTests(TestCase):
) )
) )
@ddt.unpack
@ddt.data(
(State.DRAFT, State.NEEDS_REVIEW),
(State.NEEDS_REVIEW, State.NEEDS_FINAL_APPROVAL),
(State.NEEDS_FINAL_APPROVAL, State.FINALIZED),
(State.FINALIZED, State.PUBLISHED),
(State.PUBLISHED, State.DRAFT),
)
def test_workflow_change_state(self, source_state, target_state):
""" Verify that we can change the workflow states according to allowed transition. """
self.assertEqual(self.course_run.state.name, source_state)
self.course_run.change_state(target=target_state)
self.assertEqual(self.course_run.state.name, target_state)
def test_workflow_change_state_not_allowed(self):
""" Verify that we can't change the workflow state from `DRAFT` to `PUBLISHED` directly. """
self.assertEqual(self.course_run.state.name, State.DRAFT)
with self.assertRaises(TransitionNotAllowed):
self.course_run.change_state(target=State.PUBLISHED)
class CourseTests(TestCase): class CourseTests(TestCase):
""" Tests for the publisher `Course` model. """ """ Tests for the publisher `Course` model. """
...@@ -48,3 +73,18 @@ class SeatTests(TestCase): ...@@ -48,3 +73,18 @@ class SeatTests(TestCase):
course=self.seat.course_run.course.title, type=self.seat.type course=self.seat.course_run.course.title, type=self.seat.type
) )
) )
class StateTests(TestCase):
""" Tests for the publisher `State` model. """
def setUp(self):
super(StateTests, self).setUp()
self.state = factories.StateFactory()
def test_str(self):
""" Verify casting an instance to a string returns a string containing the current state display name. """
self.assertEqual(
str(self.state),
self.state.get_name_display()
)
import ddt
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.forms import model_to_dict from django.forms import model_to_dict
from django.test import TestCase from django.test import TestCase
from course_discovery.apps.publisher.models import Course, CourseRun, Seat from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher.wrappers import CourseRunWrapper from course_discovery.apps.publisher.wrappers import CourseRunWrapper
...@@ -171,7 +170,7 @@ class SeatsCreateUpdateViewTests(TestCase): ...@@ -171,7 +170,7 @@ class SeatsCreateUpdateViewTests(TestCase):
self.seat_dict self.seat_dict
) )
seat = Seat.objects.get(id=self.seat.id) seat = Seat.objects.get(id=self.seat.id)
# Assert that we change seat type. # Assert that we can change seat type.
self.assertEqual(seat.type, Seat.HONOR) self.assertEqual(seat.type, Seat.HONOR)
self.assertRedirects( self.assertRedirects(
...@@ -182,7 +181,6 @@ class SeatsCreateUpdateViewTests(TestCase): ...@@ -182,7 +181,6 @@ class SeatsCreateUpdateViewTests(TestCase):
) )
@ddt.ddt
class CourseRunDetailTests(TestCase): class CourseRunDetailTests(TestCase):
""" Tests for the course-run detail view. """ """ Tests for the course-run detail view. """
...@@ -319,3 +317,37 @@ class CourseRunDetailTests(TestCase): ...@@ -319,3 +317,37 @@ class CourseRunDetailTests(TestCase):
""" Helper method to test course subjects. """ """ Helper method to test course subjects. """
for subject in self.wrapped_course_run.subjects: for subject in self.wrapped_course_run.subjects:
self.assertContains(response, subject.name) self.assertContains(response, subject.name)
class ChangeStateViewTests(TestCase):
""" Tests for the `ChangeStateView`. """
def setUp(self):
super(ChangeStateViewTests, self).setUp()
self.course = factories.CourseFactory()
self.course_run = factories.CourseRunFactory(course=self.course)
self.page_url = reverse('publisher:publisher_course_run_detail', args=[self.course_run.id])
self.change_state_url = reverse('publisher:publisher_change_state', args=[self.course_run.id])
def test_detail_page_change_state(self):
""" Verify that we can change workflow state from detail page. """
response = self.client.get(self.page_url)
self.assertContains(response, 'Status:')
self.assertContains(response, State.DRAFT.title())
# change workflow state from `DRAFT` to `NEEDS_REVIEW`
response = self.client.post(self.change_state_url, data={'state': State.NEEDS_REVIEW}, follow=True)
# assert that state is changed to `NEEDS_REVIEW`
self.assertContains(response, State.NEEDS_REVIEW.title().replace('_', ' '))
def test_detail_page_change_state_not_allowed(self):
""" Verify that we can't change workflow state from `DRAFT` to `PUBLISHED`. """
response = self.client.get(self.page_url)
self.assertContains(response, 'Status:')
self.assertContains(response, State.DRAFT.title())
# change workflow state from `DRAFT` to `PUBLISHED`
response = self.client.post(self.change_state_url, data={'state': State.PUBLISHED}, follow=True)
# assert that state is not changed to `PUBLISHED`
self.assertNotContains(response, State.PUBLISHED.title())
self.assertContains(response, 'There was an error in changing state.')
...@@ -5,7 +5,7 @@ from unittest import mock ...@@ -5,7 +5,7 @@ from unittest import mock
from django.test import TestCase from django.test import TestCase
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory
from course_discovery.apps.publisher.models import Seat from course_discovery.apps.publisher.models import Seat, State
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher.wrappers import CourseRunWrapper from course_discovery.apps.publisher.wrappers import CourseRunWrapper
...@@ -96,3 +96,7 @@ class CourseRunWrapperTests(TestCase): ...@@ -96,3 +96,7 @@ class CourseRunWrapperTests(TestCase):
wrapped_course_run = CourseRunWrapper(self.course_run) wrapped_course_run = CourseRunWrapper(self.course_run)
self.assertEqual(wrapped_course_run.credit_seat, seat) self.assertEqual(wrapped_course_run.credit_seat, seat)
def test_workflow_state(self):
""" Verify that the wrapper can return workflow state. """
self.assertEqual(self.wrapped_course_run.workflow_state, State.DRAFT.title())
...@@ -13,6 +13,11 @@ urlpatterns = [ ...@@ -13,6 +13,11 @@ urlpatterns = [
url(r'^course_runs/new$', views.CreateCourseRunView.as_view(), name='publisher_course_runs_new'), url(r'^course_runs/new$', views.CreateCourseRunView.as_view(), name='publisher_course_runs_new'),
url(r'^course_runs/(?P<pk>\d+)/$', views.CourseRunDetailView.as_view(), name='publisher_course_run_detail'), url(r'^course_runs/(?P<pk>\d+)/$', views.CourseRunDetailView.as_view(), name='publisher_course_run_detail'),
url(r'^course_runs/(?P<pk>\d+)/edit/$', views.UpdateCourseRunView.as_view(), name='publisher_course_runs_edit'), url(r'^course_runs/(?P<pk>\d+)/edit/$', views.UpdateCourseRunView.as_view(), name='publisher_course_runs_edit'),
url(
r'^course_runs/(?P<course_run_id>\d+)/change_state/$',
views.ChangeStateView.as_view(),
name='publisher_change_state'
),
url(r'^seats/new$', views.CreateSeatView.as_view(), name='publisher_seats_new'), url(r'^seats/new$', views.CreateSeatView.as_view(), name='publisher_seats_new'),
url(r'^seats/(?P<pk>\d+)/edit/$', views.UpdateSeatView.as_view(), name='publisher_seats_edit'), url(r'^seats/(?P<pk>\d+)/edit/$', views.UpdateSeatView.as_view(), name='publisher_seats_edit'),
] ]
""" """
Course publisher views. Course publisher views.
""" """
from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.views.generic import View
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView from django.views.generic.edit import CreateView, UpdateView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django_fsm import TransitionNotAllowed
from course_discovery.apps.publisher.forms import CourseForm, CourseRunForm, SeatForm from course_discovery.apps.publisher.forms import CourseForm, CourseRunForm, SeatForm
from course_discovery.apps.publisher.models import Course, CourseRun, Seat from course_discovery.apps.publisher.models import Course, CourseRun, Seat
from course_discovery.apps.publisher.wrappers import CourseRunWrapper from course_discovery.apps.publisher.wrappers import CourseRunWrapper
...@@ -87,8 +91,16 @@ class UpdateCourseRunView(UpdateView): ...@@ -87,8 +91,16 @@ class UpdateCourseRunView(UpdateView):
template_name = 'publisher/course_run_form.html' template_name = 'publisher/course_run_form.html'
success_url = 'publisher:publisher_course_runs_edit' success_url = 'publisher:publisher_course_runs_edit'
def get_context_data(self, **kwargs):
context = super(UpdateCourseRunView, self).get_context_data(**kwargs)
if not self.object:
self.object = self.get_object()
context['workflow_state'] = self.object.current_state
return context
def form_valid(self, form): def form_valid(self, form):
self.object = form.save() self.object = form.save()
self.object.change_state()
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
def get_success_url(self): def get_success_url(self):
...@@ -133,3 +145,20 @@ class UpdateSeatView(UpdateView): ...@@ -133,3 +145,20 @@ class UpdateSeatView(UpdateView):
def get_success_url(self): def get_success_url(self):
return reverse(self.success_url, kwargs={'pk': self.object.id}) return reverse(self.success_url, kwargs={'pk': self.object.id})
class ChangeStateView(View):
""" Change Workflow State View"""
def post(self, request, course_run_id):
state = request.POST.get('state')
try:
course_run = CourseRun.objects.get(id=course_run_id)
course_run.change_state(target=state)
messages.success(
request, 'Content moved to `{state}` successfully.'.format(state=course_run.current_state),
)
return HttpResponseRedirect(reverse('publisher:publisher_course_run_detail', kwargs={'pk': course_run_id}))
except (CourseRun.DoesNotExist, TransitionNotAllowed):
messages.error(request, 'There was an error in changing state.')
return HttpResponseRedirect(reverse('publisher:publisher_course_run_detail', kwargs={'pk': course_run_id}))
"""Publisher Wrapper Classes""" """Publisher Wrapper Classes"""
from course_discovery.apps.publisher.models import Seat from django.utils.translation import ugettext_lazy as _
from course_discovery.apps.publisher.models import Seat, State
CHANGE_STATE_BUTTON_VALUES = {
State.DRAFT: {'value': State.NEEDS_REVIEW, 'text': _('Send For Review')},
State.NEEDS_REVIEW: {'value': State.NEEDS_FINAL_APPROVAL, 'text': _('Send For Final Approval')},
State.NEEDS_FINAL_APPROVAL: {'value': State.FINALIZED, 'text': _('Finalize')},
State.FINALIZED: {'value': State.PUBLISHED, 'text': _('Publish')}
}
class BaseWrapper(object): class BaseWrapper(object):
...@@ -94,7 +102,7 @@ class CourseRunWrapper(BaseWrapper): ...@@ -94,7 +102,7 @@ class CourseRunWrapper(BaseWrapper):
@property @property
def subject_names(self): def subject_names(self):
return ', '.join([subject.name for subject in self.subjects]) return ', '.join([subject.name for subject in self.subjects if subject])
@property @property
def course_type(self): def course_type(self):
...@@ -115,3 +123,11 @@ class CourseRunWrapper(BaseWrapper): ...@@ -115,3 +123,11 @@ class CourseRunWrapper(BaseWrapper):
if not organizations: if not organizations:
return None return None
return organizations[0].key return organizations[0].key
@property
def workflow_state(self):
return self.wrapped_obj.current_state
@property
def change_state_button(self):
return CHANGE_STATE_BUTTON_VALUES.get(self.wrapped_obj.state.name)
...@@ -42,6 +42,7 @@ THIRD_PARTY_APPS = [ ...@@ -42,6 +42,7 @@ THIRD_PARTY_APPS = [
'dry_rest_permissions', 'dry_rest_permissions',
'compressor', 'compressor',
'django_filters', 'django_filters',
'django_fsm',
] ]
PROJECT_APPS = [ PROJECT_APPS = [
......
$(".container > button").click(function(event) { $(".administration-navbar .container > button").click(function(event) {
event.preventDefault();
$(this).addClass("selected"); $(this).addClass("selected");
$(this).siblings().removeClass("selected"); $(this).siblings().removeClass("selected");
var tab = $(this).data("tab"); var tab = $(this).data("tab");
$(".tab-content").not(tab).css("display", "none"); $(".tab-content").not(tab).css("display", "none");
$(tab).fadeIn(); $(tab).fadeIn();
}); });
function alertTimeout(wait) {
setTimeout(function(){
$('.alert-messages').html('');
}, wait);
}
\ No newline at end of file
// ------------------------------ // ------------------------------
// // edX Course Discovery: Base // // edX Course Discovery: Course Detail
// About: Base resets and definitons (using shared resources from elements when appropriate).
// ------------------------------ // ------------------------------
// #RESET
// ------------------------------
//
//
//// ------------------------------
// Fontawesome icons
// ------------------------------
* {
margin: 0;
padding: 0;
text-decoration: none;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
body {
color: #434242;
background: #fafafa;
font-family: "Open Sans";
line-height: 1.42857;
}
// ------------------------------
// #BASE
// ------------------------------
html, body {
height: 100%;
margin: 0;
padding: 0;
background: #fafafa;
}
// ------------------------------ // ------------------------------
// #TYPOGRAPHY // #TYPOGRAPHY
...@@ -50,6 +16,10 @@ html, body { ...@@ -50,6 +16,10 @@ html, body {
width: 1170px; width: 1170px;
margin: auto; margin: auto;
overflow: auto; overflow: auto;
.actions {
margin-top: 30px;
}
} }
nav { nav {
...@@ -86,7 +56,11 @@ nav { ...@@ -86,7 +56,11 @@ nav {
padding: 0 30px 30px; padding: 0 30px 30px;
} }
.course-information { .alert-messages .alert {
margin-bottom: 20px;
}
.course-information, .status-information {
margin-bottom: 30px; margin-bottom: 30px;
.info-item { .info-item {
...@@ -108,7 +82,12 @@ nav { ...@@ -108,7 +82,12 @@ nav {
} }
} }
.btn-edit {
@include float(right);
@include margin-right(30px);
@include padding-left(20px);
@include padding-right(20px);
}
.breadcrumb { .breadcrumb {
padding: 8px 15px; padding: 8px 15px;
......
...@@ -25,6 +25,33 @@ ...@@ -25,6 +25,33 @@
<span class="course-name">{{ object.title }}</span> <span class="course-name">{{ object.title }}</span>
</h2> </h2>
</div> </div>
<div class="alert-messages">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}" role="alert" aria-labelledby="alert-title-{{ message.tags }}" tabindex="-1">
<div class="alert-message-with-action">
<p class="alert-copy">
{{ message }}
</p>
</div>
</div>
{% endfor %}
{% endif %}
</div>
<div class="status-information">
<div class="info-item">
<span class="item">
<span class="heading">{% trans "Status" %}:</span>
<span>{{ object.workflow_state }}</span>
</span>
<span class="item">
<a href="{% url 'publisher:publisher_course_runs_edit' pk=object.id %}" target="_blank" class="btn btn-small btn-edit">
<span class="icon fa fa-edit" aria-hidden="true"></span>&nbsp;&nbsp;{% trans "Edit" %}
</a>
</span>
</div>
</div>
<div class="tab"> <div class="tab">
<div id="tab-1" class="tab-content active"> <div id="tab-1" class="tab-content active">
...@@ -40,6 +67,12 @@ ...@@ -40,6 +67,12 @@
{% include 'publisher/course_run_detail/_drupal.html' %} {% include 'publisher/course_run_detail/_drupal.html' %}
</div> </div>
</div> </div>
<div class="actions">
<form action="{% url 'publisher:publisher_change_state' course_run_id=object.id %}" method="post"> {% csrf_token %}
<button type="submit" value="{{ object.change_state_button.value }}" class="btn-brand btn-small btn-states" name="state">{{ object.change_state_button.text }}</button>
</form>
</div>
</div> </div>
{% endblock %} {% endblock %}
...@@ -52,5 +85,7 @@ ...@@ -52,5 +85,7 @@
return $(trigger).parent().next('.copy').text().trim(); return $(trigger).parent().next('.copy').text().trim();
} }
}); });
alertTimeout(5000);
</script> </script>
{% endblock %} {% endblock %}
...@@ -8,11 +8,22 @@ ...@@ -8,11 +8,22 @@
<div class="layout-full layout"> <div class="layout-full layout">
<div class="card course-form"> <div class="card course-form">
<h4 class="hd-4">{% trans "Course Run Form" %}</h4> <h4 class="hd-4">{% trans "Course Run Form" %}</h4>
<div class="status-information">
{% if object.id %} {% if object.id %}
<div class="info-item">
<span class="item">
<span class="heading">{% trans "Status" %}:</span>
<span>{{ workflow_state }}</span>
</span>
<span class="item">
<a href="{% url 'publisher:publisher_seats_new' %}" target="_blank" class="btn btn-neutral btn-add"> <a href="{% url 'publisher:publisher_seats_new' %}" target="_blank" class="btn btn-neutral btn-add">
{% trans "Add Seat" %} {% trans "Add Seat" %}
</a> </a>
</span>
</div>
{% endif %} {% endif %}
</div>
<form class="form" method="post" action=""> {% csrf_token %} <form class="form" method="post" action=""> {% csrf_token %}
<fieldset class="form-group"> <fieldset class="form-group">
{% for field in form %} {% for field in form %}
......
...@@ -15,18 +15,20 @@ ...@@ -15,18 +15,20 @@
<th>{% trans "Partner" %}</th> <th>{% trans "Partner" %}</th>
<th>{% trans "Target Content?" %}</th> <th>{% trans "Target Content?" %}</th>
<th>{% trans "Priority" %}</th> <th>{% trans "Priority" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Last Updated" %}</th> <th>{% trans "Last Updated" %}</th>
</tr> </tr>
{% for course_run in object_list %} {% for course_run in object_list %}
{% url 'publisher:publisher_course_run_detail' course_run.id as detail_url %} {% url 'publisher:publisher_course_run_detail' course_run.id as detail_url %}
<tr> <tr>
<td> <td>
<a href={{detail_url}}>{{ course_run.title }}</a> <a target="_blank" href="{{ detail_url }}">{{ course_run.title }}</a>
</td> </td>
<td>{{ course_run.start }}</td> <td>{{ course_run.start }}</td>
<td>{{ course_run.partner }}</td> <td>{{ course_run.partner }}</td>
<td>{{ course_run.target_content }}</td> <td>{{ course_run.target_content }}</td>
<td>{{ course_run.priority }}</td> <td>{{ course_run.priority }}</td>
<td>{{ course_run.workflow_state }}</td>
<td>{{ course_run.modified }}</td> <td>{{ course_run.modified }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
......
...@@ -4,6 +4,7 @@ django-choices==1.4.3 ...@@ -4,6 +4,7 @@ django-choices==1.4.3
django-compressor==2.0 django-compressor==2.0
django-extensions==1.6.7 django-extensions==1.6.7
django-filter==0.13.0 django-filter==0.13.0
django-fsm==2.2.0
django-guardian==1.4.4 django-guardian==1.4.4
django-haystack==2.4.1 django-haystack==2.4.1
django-libsass==0.7 django-libsass==0.7
......
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