Commit 33198721 by Awais Qureshi Committed by GitHub

Merge pull request #228 from edx/awais786/ECOM-5101-comment-package

ECOM-5101
parents 9c1f4a2a 1c622180
import logging import logging
from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.db.models.signals import pre_save from django.db.models.signals import pre_save
...@@ -105,6 +106,10 @@ class Course(TimeStampedModel, ChangedByMixin): ...@@ -105,6 +106,10 @@ class Course(TimeStampedModel, ChangedByMixin):
def __str__(self): def __str__(self):
return self.title return self.title
@property
def post_back_url(self):
return reverse('publisher:publisher_courses_edit', kwargs={'pk': self.id})
class CourseRun(TimeStampedModel, ChangedByMixin): class CourseRun(TimeStampedModel, ChangedByMixin):
""" Publisher CourseRun model. It contains fields related to the course run intake form.""" """ Publisher CourseRun model. It contains fields related to the course run intake form."""
...@@ -204,6 +209,10 @@ class CourseRun(TimeStampedModel, ChangedByMixin): ...@@ -204,6 +209,10 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
def current_state(self): def current_state(self):
return self.state.get_name_display() return self.state.get_name_display()
@property
def post_back_url(self):
return reverse('publisher:publisher_course_runs_edit', kwargs={'pk': self.id})
class Seat(TimeStampedModel, ChangedByMixin): class Seat(TimeStampedModel, ChangedByMixin):
""" Seat model. """ """ Seat model. """
...@@ -242,6 +251,10 @@ class Seat(TimeStampedModel, ChangedByMixin): ...@@ -242,6 +251,10 @@ 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)
@property
def post_back_url(self):
return reverse('publisher:publisher_seats_edit', kwargs={'pk': self.id})
@receiver(pre_save, sender=CourseRun) @receiver(pre_save, sender=CourseRun)
def initialize_workflow(sender, instance, **kwargs): # pylint: disable=unused-argument def initialize_workflow(sender, instance, **kwargs): # pylint: disable=unused-argument
......
# pylint: disable=no-member # pylint: disable=no-member
import ddt import ddt
from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django_fsm import TransitionNotAllowed from django_fsm import TransitionNotAllowed
...@@ -25,6 +26,12 @@ class CourseRunTests(TestCase): ...@@ -25,6 +26,12 @@ class CourseRunTests(TestCase):
) )
) )
def test_post_back_url(self):
self.assertEqual(
self.course_run.post_back_url,
reverse('publisher:publisher_course_runs_edit', kwargs={'pk': self.course_run.id})
)
@ddt.unpack @ddt.unpack
@ddt.data( @ddt.data(
(State.DRAFT, State.NEEDS_REVIEW), (State.DRAFT, State.NEEDS_REVIEW),
...@@ -57,6 +64,12 @@ class CourseTests(TestCase): ...@@ -57,6 +64,12 @@ class CourseTests(TestCase):
""" Verify casting an instance to a string returns a string containing the course title. """ """ Verify casting an instance to a string returns a string containing the course title. """
self.assertEqual(str(self.course), self.course.title) self.assertEqual(str(self.course), self.course.title)
def test_post_back_url(self):
self.assertEqual(
self.course.post_back_url,
reverse('publisher:publisher_courses_edit', kwargs={'pk': self.course.id})
)
class SeatTests(TestCase): class SeatTests(TestCase):
""" Tests for the publisher `Seat` model. """ """ Tests for the publisher `Seat` model. """
...@@ -74,6 +87,12 @@ class SeatTests(TestCase): ...@@ -74,6 +87,12 @@ class SeatTests(TestCase):
) )
) )
def test_post_back_url(self):
self.assertEqual(
self.seat.post_back_url,
reverse('publisher:publisher_seats_edit', kwargs={'pk': self.seat.id})
)
class StateTests(TestCase): class StateTests(TestCase):
""" Tests for the publisher `State` model. """ """ Tests for the publisher `State` model. """
......
import ddt
from django.conf import settings
from django.contrib.sites.models import Site
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.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State 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
from course_discovery.apps.publisher_comments.tests.factories import CommentFactory
class CreateUpdateCourseViewTests(TestCase): class CreateUpdateCourseViewTests(TestCase):
...@@ -14,6 +18,9 @@ class CreateUpdateCourseViewTests(TestCase): ...@@ -14,6 +18,9 @@ class CreateUpdateCourseViewTests(TestCase):
def setUp(self): def setUp(self):
super(CreateUpdateCourseViewTests, self).setUp() super(CreateUpdateCourseViewTests, self).setUp()
self.course = factories.CourseFactory() self.course = factories.CourseFactory()
self.user = UserFactory(is_staff=True, is_superuser=True)
self.site = Site.objects.get(pk=settings.SITE_ID)
self.client.login(username=self.user.username, password=USER_PASSWORD)
def test_create_course(self): def test_create_course(self):
""" Verify that we can create a new course. """ """ Verify that we can create a new course. """
...@@ -32,6 +39,9 @@ class CreateUpdateCourseViewTests(TestCase): ...@@ -32,6 +39,9 @@ class CreateUpdateCourseViewTests(TestCase):
) )
self.assertEqual(course.number, course_number) self.assertEqual(course.number, course_number)
response = self.client.get(reverse('publisher:publisher_courses_new'))
self.assertNotContains(response, 'Add new comment')
self.assertNotContains(response, 'Total Comments')
def test_update_course(self): def test_update_course(self):
""" Verify that we can update an existing course. """ """ Verify that we can update an existing course. """
...@@ -55,6 +65,13 @@ class CreateUpdateCourseViewTests(TestCase): ...@@ -55,6 +65,13 @@ class CreateUpdateCourseViewTests(TestCase):
# Assert that course is updated. # Assert that course is updated.
self.assertEqual(course.title, updated_course_title) self.assertEqual(course.title, updated_course_title)
# add new and check the comment on edit page.
comment = CommentFactory(content_object=self.course, user=self.user, site=self.site)
response = self.client.get(reverse('publisher:publisher_courses_edit', kwargs={'pk': self.course.id}))
self.assertContains(response, 'Total Comments 1')
self.assertContains(response, 'Add new comment')
self.assertContains(response, comment.comment)
class CreateUpdateCourseRunViewTests(TestCase): class CreateUpdateCourseRunViewTests(TestCase):
""" Tests for the publisher `CreateCourseRunView` and `UpdateCourseRunView`. """ """ Tests for the publisher `CreateCourseRunView` and `UpdateCourseRunView`. """
...@@ -67,6 +84,9 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -67,6 +84,9 @@ class CreateUpdateCourseRunViewTests(TestCase):
self.course_run_dict, self.course_run_dict,
['start', 'end', 'enrollment_start', 'enrollment_end', 'priority', 'certificate_generation'] ['start', 'end', 'enrollment_start', 'enrollment_end', 'priority', 'certificate_generation']
) )
self.user = UserFactory(is_staff=True, is_superuser=True)
self.site = Site.objects.get(pk=settings.SITE_ID)
self.client.login(username=self.user.username, password=USER_PASSWORD)
def _pop_valuse_from_dict(self, data_dict, key_list): def _pop_valuse_from_dict(self, data_dict, key_list):
for key in key_list: for key in key_list:
...@@ -88,6 +108,10 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -88,6 +108,10 @@ class CreateUpdateCourseRunViewTests(TestCase):
self.assertEqual(course_run.lms_course_id, lms_course_id) self.assertEqual(course_run.lms_course_id, lms_course_id)
response = self.client.get(reverse('publisher:publisher_course_runs_new'))
self.assertNotContains(response, 'Add new comment')
self.assertNotContains(response, 'Total Comments')
def test_update_course_run(self): def test_update_course_run(self):
""" Verify that we can update an existing course run. """ """ Verify that we can update an existing course run. """
updated_lms_course_id = 'course-v1:testX+AS121+2018_q1' updated_lms_course_id = 'course-v1:testX+AS121+2018_q1'
...@@ -109,6 +133,13 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -109,6 +133,13 @@ class CreateUpdateCourseRunViewTests(TestCase):
# Assert that course run is updated. # Assert that course run is updated.
self.assertEqual(course_run.lms_course_id, updated_lms_course_id) self.assertEqual(course_run.lms_course_id, updated_lms_course_id)
# add new and check the comment on edit page.
comment = CommentFactory(content_object=self.course_run, user=self.user, site=self.site)
response = self.client.get(reverse('publisher:publisher_course_runs_edit', kwargs={'pk': self.course_run.id}))
self.assertContains(response, 'Total Comments 1')
self.assertContains(response, 'Add new comment')
self.assertContains(response, comment.comment)
class SeatsCreateUpdateViewTests(TestCase): class SeatsCreateUpdateViewTests(TestCase):
""" Tests for the publisher `CreateSeatView` and `UpdateSeatView`. """ """ Tests for the publisher `CreateSeatView` and `UpdateSeatView`. """
...@@ -118,6 +149,9 @@ class SeatsCreateUpdateViewTests(TestCase): ...@@ -118,6 +149,9 @@ class SeatsCreateUpdateViewTests(TestCase):
self.seat = factories.SeatFactory(type=Seat.PROFESSIONAL, credit_hours=0) self.seat = factories.SeatFactory(type=Seat.PROFESSIONAL, credit_hours=0)
self.seat_dict = model_to_dict(self.seat) self.seat_dict = model_to_dict(self.seat)
self.seat_dict.pop('upgrade_deadline') self.seat_dict.pop('upgrade_deadline')
self.user = UserFactory(is_staff=True, is_superuser=True)
self.site = Site.objects.get(pk=settings.SITE_ID)
self.client.login(username=self.user.username, password=USER_PASSWORD)
def test_seat_view_page(self): def test_seat_view_page(self):
""" Verify that we can open new seat page. """ """ Verify that we can open new seat page. """
...@@ -180,7 +214,15 @@ class SeatsCreateUpdateViewTests(TestCase): ...@@ -180,7 +214,15 @@ class SeatsCreateUpdateViewTests(TestCase):
target_status_code=200 target_status_code=200
) )
# add new and check the comment on edit page.
comment = CommentFactory(content_object=self.seat, user=self.user, site=self.site)
response = self.client.get(reverse('publisher:publisher_seats_edit', kwargs={'pk': self.seat.id}))
self.assertContains(response, 'Total Comments 1')
self.assertContains(response, 'Add new comment')
self.assertContains(response, comment.comment)
@ddt.ddt
class CourseRunDetailTests(TestCase): class CourseRunDetailTests(TestCase):
""" Tests for the course-run detail view. """ """ Tests for the course-run detail view. """
...@@ -318,6 +360,25 @@ class CourseRunDetailTests(TestCase): ...@@ -318,6 +360,25 @@ class CourseRunDetailTests(TestCase):
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)
def test_detail_page_with_comments(self):
""" Verify that detail page contains all the data along with comments
for course.
"""
user = UserFactory(is_staff=True, is_superuser=True)
site = Site.objects.get(pk=settings.SITE_ID)
comment = CommentFactory(content_object=self.course, user=user, site=site)
response = self.client.get(self.page_url)
self.assertEqual(response.status_code, 200)
self._assert_credits_seats(response, self.wrapped_course_run.credit_seat)
self._assert_non_credits_seats(response, self.wrapped_course_run.non_credit_seats)
self._assert_studio_fields(response)
self._assert_cat(response)
self._assert_drupal(response)
self._assert_subjects(response)
self.assertContains(response, 'Total Comments 1')
self.assertContains(response, comment.comment)
class ChangeStateViewTests(TestCase): class ChangeStateViewTests(TestCase):
""" Tests for the `ChangeStateView`. """ """ Tests for the `ChangeStateView`. """
......
...@@ -14,6 +14,7 @@ from course_discovery.apps.publisher.forms import CourseForm, CourseRunForm, Sea ...@@ -14,6 +14,7 @@ from course_discovery.apps.publisher.forms import CourseForm, CourseRunForm, Sea
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
SEATS_HIDDEN_FIELDS = ['price', 'currency', 'upgrade_deadline', 'credit_provider', 'credit_hours'] SEATS_HIDDEN_FIELDS = ['price', 'currency', 'upgrade_deadline', 'credit_provider', 'credit_hours']
...@@ -35,6 +36,7 @@ class CourseRunDetailView(DetailView): ...@@ -35,6 +36,7 @@ class CourseRunDetailView(DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CourseRunDetailView, self).get_context_data(**kwargs) context = super(CourseRunDetailView, self).get_context_data(**kwargs)
context['object'] = CourseRunWrapper(context['object']) context['object'] = CourseRunWrapper(context['object'])
context['comment_object'] = self.object.course
return context return context
...@@ -68,6 +70,11 @@ class UpdateCourseView(UpdateView): ...@@ -68,6 +70,11 @@ class UpdateCourseView(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})
def get_context_data(self, **kwargs):
context = super(UpdateCourseView, self).get_context_data(**kwargs)
context['comment_object'] = self.object
return context
class CreateCourseRunView(CreateView): class CreateCourseRunView(CreateView):
""" Create Course Run View.""" """ Create Course Run View."""
...@@ -96,6 +103,7 @@ class UpdateCourseRunView(UpdateView): ...@@ -96,6 +103,7 @@ class UpdateCourseRunView(UpdateView):
if not self.object: if not self.object:
self.object = self.get_object() self.object = self.get_object()
context['workflow_state'] = self.object.current_state context['workflow_state'] = self.object.current_state
context['comment_object'] = self.object
return context return context
def form_valid(self, form): def form_valid(self, form):
...@@ -137,6 +145,7 @@ class UpdateSeatView(UpdateView): ...@@ -137,6 +145,7 @@ class UpdateSeatView(UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(UpdateSeatView, self).get_context_data(**kwargs) context = super(UpdateSeatView, self).get_context_data(**kwargs)
context['hidden_fields'] = SEATS_HIDDEN_FIELDS context['hidden_fields'] = SEATS_HIDDEN_FIELDS
context['comment_object'] = self.object
return context return context
def form_valid(self, form): def form_valid(self, form):
......
"""
Change the attributes you want to customize
"""
def get_model():
from course_discovery.apps.publisher_comments.models import Comments
return Comments
def get_form():
from course_discovery.apps.publisher_comments.forms import CommentsForm
return CommentsForm
from django.contrib import admin
from course_discovery.apps.publisher_comments.forms import CommentsAdminForm
from course_discovery.apps.publisher_comments.models import Comments
class CommentsAdmin(admin.ModelAdmin):
form = CommentsAdminForm
readonly_fields = ('modified',)
admin.site.register(Comments, CommentsAdmin)
from django import forms
from django.contrib.contenttypes.models import ContentType
from django_comments.forms import CommentForm
from course_discovery.apps.publisher_comments.models import Comments
# pylint: disable=no-member
class CommentsForm(CommentForm):
modified = forms.DateTimeField(required=False, widget=forms.HiddenInput)
def get_comment_model(self):
return Comments
def get_comment_create_data(self):
# Use the data of the superclass, and add in the title field
data = super(CommentsForm, self).get_comment_create_data()
data['modified'] = self.cleaned_data['modified']
return data
class CommentEditForm(forms.ModelForm):
""" Comment edit form. """
submit_date = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'}))
class Meta:
model = Comments
fields = ('comment', 'submit_date', )
class CommentsAdminForm(forms.ModelForm):
""" Comment form for admin.It will load only required content types models in drop down. """
class Meta:
model = Comments
fields = '__all__'
def __init__(self, *args, **kwargs):
super(CommentsAdminForm, self).__init__(*args, **kwargs)
self.fields['content_type'].queryset = ContentType.objects.filter(
model__in=['course', 'courserun', 'seat'],
app_label='publisher'
)
# -*- 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_extensions.db.fields
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('sites', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Comments',
fields=[
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
('object_pk', models.TextField(verbose_name='object ID')),
('user_name', models.CharField(verbose_name="user's name", blank=True, max_length=50)),
('user_email', models.EmailField(verbose_name="user's email address", blank=True, max_length=254)),
('user_url', models.URLField(verbose_name="user's URL", blank=True)),
('comment', models.TextField(verbose_name='comment', max_length=3000)),
('submit_date', models.DateTimeField(verbose_name='date/time submitted', default=None, db_index=True)),
('ip_address', models.GenericIPAddressField(verbose_name='IP address', blank=True, unpack_ipv4=True, null=True)),
('is_public', models.BooleanField(help_text='Uncheck this box to make the comment effectively disappear from the site.', verbose_name='is public', default=True)),
('is_removed', models.BooleanField(help_text='Check this box if the comment is inappropriate. A "This comment has been removed" message will be displayed instead.', verbose_name='is removed', default=False)),
('modified', django_extensions.db.fields.ModificationDateTimeField(verbose_name='modified', auto_now=True)),
('content_type', models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType', related_name='content_type_set_for_comments')),
('site', models.ForeignKey(to='sites.Site')),
('user', models.ForeignKey(verbose_name='user', blank=True, to=settings.AUTH_USER_MODEL, null=True, related_name='comments_comments', on_delete=django.db.models.deletion.SET_NULL)),
],
options={
'verbose_name': 'comment',
'permissions': [('can_moderate', 'Can moderate comments')],
'verbose_name_plural': 'comments',
'ordering': ('submit_date',),
'abstract': False,
},
),
]
from django.utils.translation import ugettext_lazy as _
from django_comments.models import CommentAbstractModel
from django_extensions.db.fields import ModificationDateTimeField
class Comments(CommentAbstractModel):
modified = ModificationDateTimeField(_('modified'))
from django.contrib.sites.models import Site
import factory
from factory.fuzzy import FuzzyText
from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.publisher.tests.factories import CourseRunFactory
from course_discovery.apps.publisher_comments.models import Comments
class SiteFactory(factory.DjangoModelFactory): # pylint: disable=missing-docstring
class Meta(object): # pylint: disable=missing-docstring
model = Site
class CommentFactory(factory.DjangoModelFactory):
comment = FuzzyText(prefix="Test Comment for çօմɾʂҽ")
content_object = factory.SubFactory(CourseRunFactory)
user = UserFactory()
site = factory.SubFactory(SiteFactory)
class Meta:
model = Comments
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.test import TestCase
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher_comments.tests.factories import CommentFactory
from course_discovery.apps.publisher_comments.forms import CommentsAdminForm
class AdminTests(TestCase):
""" Tests Admin page and customize form."""
def setUp(self):
super(AdminTests, self).setUp()
self.user = UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=self.user.username, password=USER_PASSWORD)
self.site = Site.objects.get(pk=settings.SITE_ID)
self.course = factories.CourseFactory()
self.comment = CommentFactory(content_object=self.course, user=self.user, site=self.site)
def test_comment_detail_form(self):
""" Verify in admin panel comment detail form contain the custom modified field. """
resp = self.client.get(reverse('admin:publisher_comments_comments_change', args=(self.comment.id,)))
self.assertContains(resp, 'modified')
def test_comment_admin_form(self):
""" Verify in admin panel for comments loads only three models in content type drop down. """
form = CommentsAdminForm(instance=self.comment)
self.assertListEqual(
sorted([con.model for con in form.fields['content_type']._queryset]),
sorted(['courserun', 'seat', 'course'])
)
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.forms import model_to_dict
from django.test import TestCase
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.publisher.models import Seat
from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher_comments.tests.factories import CommentFactory
class CommentsTests(TestCase):
""" Tests for the Comment functionality on `Courser`, `CourseRun` And `Seat` edit pages. """
def setUp(self):
super(CommentsTests, self).setUp()
self.user = UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=self.user.username, password=USER_PASSWORD)
self.site = Site.objects.get(pk=settings.SITE_ID)
self.course_edit_page = 'publisher:publisher_courses_edit'
self.course_run_edit_page = 'publisher:publisher_course_runs_edit'
self.seat_edit_page = 'publisher:publisher_seats_edit'
self.edit_comment_page = 'publisher_comments:comment_edit'
self.course = factories.CourseFactory()
self.course_run = factories.CourseRunFactory()
self.seat = factories.SeatFactory(type=Seat.PROFESSIONAL, credit_hours=0)
def test_course_edit_page_with_multiple_comments(self):
""" Verify course edit page can load multiple comments"""
self._add_assert_multiple_comments(self.course, self.course_edit_page)
def test_course_run_edit_page_with_multiple_comments(self):
""" Verify course-run edit page can load multiple comments"""
self._add_assert_multiple_comments(self.course_run, self.course_run_edit_page)
def test_seat_edit_page_with_multiple_comments(self):
""" Verify seat edit page can load multiple comments"""
self._add_assert_multiple_comments(self.seat, self.seat_edit_page)
def _add_assert_multiple_comments(self, content_object, page_path):
""" DRY method to add comments on edit page for specific object. """
response = self.client.get(reverse(page_path, kwargs={'pk': content_object.id}))
self.assertContains(response, 'Total Comments 0')
comments = []
for num in range(1, 10):
comments.append(self._generate_comment(content_object=content_object, user=self.user))
response = self.client.get(reverse(page_path, kwargs={'pk': content_object.id}))
for comment in comments:
self.assertContains(response, comment.comment)
self.assertContains(response, 'Total Comments 9')
def test_comment_edit_with_course(self):
""" Verify that only comments attached with specific course appears on edited page. """
comments = self._generate_comments_for_all_content_types()
response = self.client.get(reverse(self.course_edit_page, kwargs={'pk': self.course.id}))
self.assertContains(response, comments.get(self.course).comment)
self.assertNotContains(response, comments.get(self.course_run).comment)
self.assertNotContains(response, comments.get(self.seat).comment)
def test_comment_edit_with_courserun(self):
""" Verify that only comments attached with specific course run appears on edited page. """
comments = self._generate_comments_for_all_content_types()
response = self.client.get(reverse(self.course_run_edit_page, kwargs={'pk': self.course_run.id}))
self.assertContains(response, comments.get(self.course_run).comment)
self.assertNotContains(response, comments.get(self.course).comment)
self.assertNotContains(response, comments.get(self.seat).comment)
def test_comment_edit_with_seat(self):
""" Verify that only comments attached with specific seat appears on edited page. """
comments = self._generate_comments_for_all_content_types()
response = self.client.get(reverse(self.seat_edit_page, kwargs={'pk': self.seat.id}))
self.assertContains(response, comments.get(self.seat).comment)
self.assertNotContains(response, comments.get(self.course).comment)
self.assertNotContains(response, comments.get(self.course_run).comment)
def test_edit_course_comment(self):
""" Verify that course comment can be edited. """
self._edit_comment_page(
self.course, reverse(self.course_edit_page, kwargs={'pk': self.course.id})
)
def test_edit_course_run_comment(self):
""" Verify that course run comment can be edited. """
self._edit_comment_page(
self.course_run, reverse(self.course_run_edit_page, kwargs={'pk': self.course_run.id})
)
def test_edit_seat_comment(self):
""" Verify that seat comment can be edited. """
self._edit_comment_page(
self.seat, reverse(self.seat_edit_page, kwargs={'pk': self.seat.id})
)
def test_edit_comment_of_other_user(self):
""" Verify that comment can be edited by the comment author only. """
comment = self._generate_comment(content_object=self.course, user=self.user)
comment_url = reverse(self.edit_comment_page, kwargs={'pk': comment.id})
response = self.client.get(comment_url)
self.assertEqual(response.status_code, 200)
# logout and login with other user.
self.client.logout()
user = UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=user.username, password=USER_PASSWORD)
response = self.client.get(reverse(self.edit_comment_page, kwargs={'pk': comment.id}))
self.assertEqual(response.status_code, 404)
def _edit_comment_page(self, content_object, expected_url):
""" DRY method for posting the edited comment."""
comment = self._generate_comment(content_object=content_object, user=self.user)
comment_url = reverse(self.edit_comment_page, kwargs={'pk': comment.id})
response = self.client.get(comment_url)
self._assert_edit_comment(response, comment)
new_comment = "This is updated comment"
content_object_dict = model_to_dict(comment)
content_object_dict['comment'] = new_comment
response = self.client.post(comment_url, content_object_dict)
self.assertRedirects(
response,
expected_url=expected_url,
status_code=302, target_status_code=200
)
response = self.client.get(comment_url)
self.assertContains(response, new_comment)
def _generate_comment(self, content_object, user):
""" DRY method to generate the comment."""
return CommentFactory(content_object=content_object, user=user, site=self.site)
def _assert_edit_comment(self, response, comment):
""" DRY method for asserting the edited comment page."""
self.assertContains(response, 'Edit Comment')
self.assertContains(response, 'Submit date')
self.assertContains(response, comment.comment)
self.assertContains(response, comment.submit_date)
# assert the customize fields exists in comment object
self.assertTrue(hasattr(comment, 'modified'))
def _generate_comments_for_all_content_types(self):
""" DRY method generate the comments for all available content types"""
data = {}
for content in [self.course, self.course_run, self.seat]:
data[content] = self._generate_comment(content_object=content, user=self.user)
return data
"""
URLs for the course publisher views.
"""
from django.conf.urls import url
from course_discovery.apps.publisher_comments import views
urlpatterns = [
url(r'^(?P<pk>\d+)/edit/$', views.UpdateCommentView.as_view(), name='comment_edit'),
]
"""
Customize custom views.
"""
from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponseRedirect
from django.views.generic.edit import UpdateView
from course_discovery.apps.publisher_comments.forms import CommentEditForm
from course_discovery.apps.publisher_comments.models import Comments
# pylint: disable=attribute-defined-outside-init
class UpdateCommentView(UpdateView):
""" Update Comment View."""
model = Comments
form_class = CommentEditForm
template_name = 'comments/edit_comment.html'
success_url = 'publisher:publisher_seats_edit'
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if request.user != self.object.user:
raise Http404
return super(UpdateCommentView, self).get(request, *args, **kwargs)
def form_valid(self, form):
self.object = form.save()
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
url = reverse('publisher:publisher_seats_edit', kwargs={'pk': self.object.object_pk})
if self.object.content_type.model == 'seat':
url = url
elif self.object.content_type.model == 'course':
url = reverse('publisher:publisher_courses_edit', kwargs={'pk': self.object.object_pk})
elif self.object.content_type.model == 'courserun':
url = reverse('publisher:publisher_course_runs_edit', kwargs={'pk': self.object.object_pk})
return url
...@@ -27,7 +27,8 @@ INSTALLED_APPS = [ ...@@ -27,7 +27,8 @@ INSTALLED_APPS = [
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles' 'django.contrib.staticfiles',
'django.contrib.sites',
] ]
THIRD_PARTY_APPS = [ THIRD_PARTY_APPS = [
...@@ -54,6 +55,8 @@ PROJECT_APPS = [ ...@@ -54,6 +55,8 @@ PROJECT_APPS = [
'course_discovery.apps.course_metadata', 'course_discovery.apps.course_metadata',
'course_discovery.apps.edx_haystack_extensions', 'course_discovery.apps.edx_haystack_extensions',
'course_discovery.apps.publisher', 'course_discovery.apps.publisher',
'course_discovery.apps.publisher_comments',
'django_comments',
] ]
...@@ -352,3 +355,7 @@ HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' ...@@ -352,3 +355,7 @@ HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
DEFAULT_PARTNER_ID = None DEFAULT_PARTNER_ID = None
# See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1
COMMENTS_APP = 'course_discovery.apps.publisher_comments'
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// // edX Course Discovery: Course Detail // // edX Course Discovery: Course Detail
// ------------------------------ // ------------------------------
// ------------------------------ // ------------------------------
// #TYPOGRAPHY // #TYPOGRAPHY
// ------------------------------ // ------------------------------
...@@ -60,8 +61,10 @@ nav { ...@@ -60,8 +61,10 @@ nav {
margin-bottom: 20px; margin-bottom: 20px;
} }
.course-information, .status-information { .course-information {
margin-bottom: 30px; margin-bottom: 30px;
@include float(left);
width: 50%;
.info-item { .info-item {
margin-bottom: 15px; margin-bottom: 15px;
...@@ -82,6 +85,22 @@ nav { ...@@ -82,6 +85,22 @@ nav {
} }
} }
.comments-container {
width: 50%;
@include float(left);
}
.clearfix {
&:after {
content: " "; /* Older browser do not support empty content */
visibility: hidden;
display: block;
height: 0;
clear: both;
}
}
.btn-edit { .btn-edit {
@include float(right); @include float(right);
@include margin-right(30px); @include margin-right(30px);
...@@ -167,3 +186,10 @@ nav { ...@@ -167,3 +186,10 @@ nav {
width: 100%; width: 100%;
} }
} }
.status-information{
margin-bottom: 15px;
.heading {
font-weight: bold;
font-size: 16px;
}
}
...@@ -3,9 +3,22 @@ ...@@ -3,9 +3,22 @@
.course-form { .course-form {
@include margin(20px); @include margin(20px);
overflow: auto;
.has-error {
border-color: #a94442;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
} }
.btn-add { .btn-add {
@include float(right);
@include margin-right(30px); @include margin-right(30px);
margin-bottom: 20px;
}
#comments dt {
font-weight: bold;
color: #757575;
} }
{% load i18n %}
{% load staticfiles %}
{% load comments %}
{% if user.is_authenticated and comment_object %}
<div class="comments-container">
<p>{% trans 'Add new comment' %}</p>
<div>
{% get_comment_form for comment_object as form %}
<form id="frm_comment" action="{% comment_form_target %}" method="POST">
{% csrf_token %}
{{ form.comment }}
{{ form.content_type }}
{{ form.object_pk }}
{{ form.timestamp }}
{{ form.security_hash }}
<input type="hidden" name="next" value="{{ comment_object.post_back_url }}"/>
<div>
<input type="button" value="Add comment" id="id_submit" class="btn-brand btn-base" />
</div>
</form>
</div>
</div>
{% endif %}
{% block extra_js %}
<script type="text/javascript">
$(document).ready(function() {
var box = $('#id_comment');
$("#id_submit").click(function(event){
if( !box.val() ) {
box.addClass('has-error');
box.focus();
}
else{
$("#frm_comment").submit();
}
});
});
</script>
{% endblock %}
{% load i18n %}
{% load comments %}
{% if comment_object %}
<div class="comments-container">
{% get_comment_count for comment_object as comment_count %}
<h4 class="hd-4">
{% blocktrans with comment_count=comment_count %}
Total Comments {{ comment_count }}
{% endblocktrans %}
</h4>
{% get_comment_list for comment_object as comment_list %}
<dl class="comments">
{% for comment in comment_list reversed %}
<dt id="c{{ comment.comment_list }}">
{{ comment.modified }} - {{ comment.name }}
{% ifequal user comment.user %}
- <a href="{% url 'publisher_comments:comment_edit' comment.id %}" class="">Edit</a>
{% endifequal %}
</dt>
<dd>
<p>{{ comment.comment }}</p>
</dd>
{% endfor %}
</dl>
</div>
{% endif %}
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
{% trans "Course Run Form" %}
{% endblock title %}
{% block content %}
<div class="layout-full layout">
<div class="card course-form">
<h4 class="hd-4">{% trans "Edit Comment" %}</h4>
<form class="form" method="post" action="">
{% csrf_token %}
<fieldset class="form-group">
{% for field in form %}
{% include "publisher/form_field.html" %}
{% endfor %}
</fieldset>
<button class="btn-brand btn-base" type="submit">{% trans "Save" %}</button>
</form>
</div>
</div>
{% endblock content %}
...@@ -7,13 +7,10 @@ ...@@ -7,13 +7,10 @@
{% block content %} {% block content %}
<div class="layout-full layout"> <div class="layout-full layout">
<div class="card course-form"> <div class="card course-form">
<div class="course-information">
<h4 class="hd-4">{% trans "Course Form" %}</h4> <h4 class="hd-4">{% trans "Course Form" %}</h4>
{% if object.id %} <form class="form" method="post" action="">
<a href="{% url 'publisher:publisher_course_runs_new' %}" target="_blank" class="btn btn-neutral btn-add"> {% csrf_token %}
{% trans "Add Course Run" %}
</a>
{% endif %}
<form class="form" method="post" action=""> {% csrf_token %}
<fieldset class="form-group"> <fieldset class="form-group">
{% for field in form %} {% for field in form %}
{% include "publisher/form_field.html" %} {% include "publisher/form_field.html" %}
...@@ -22,6 +19,16 @@ ...@@ -22,6 +19,16 @@
<button class="btn-brand btn-base" type="submit">{% trans "Save" %}</button> <button class="btn-brand btn-base" type="submit">{% trans "Save" %}</button>
</form> </form>
</div> </div>
<div class="comment-container">
{% if object.id %}
<a href="{% url 'publisher:publisher_course_runs_new' %}" target="_blank" class="btn btn-neutral btn-add">
{% trans "Add Course Run" %}
</a>
{% endif %}
{% include 'comments/comments_list.html' %}
{% include 'comments/add_auth_comments.html' %}
</div>
</div>
</div> </div>
{% endblock content %} {% endblock content %}
...@@ -45,11 +45,6 @@ ...@@ -45,11 +45,6 @@
<span class="heading">{% trans "Status" %}:</span> <span class="heading">{% trans "Status" %}:</span>
<span>{{ object.workflow_state }}</span> <span>{{ object.workflow_state }}</span>
</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> </div>
......
...@@ -256,3 +256,12 @@ ...@@ -256,3 +256,12 @@
<div class="copy">{{ object.notes }}</div> <div class="copy">{{ object.notes }}</div>
</div> </div>
</div> </div>
<div class="comment-container">
<a href="{% url 'publisher:publisher_course_runs_edit' pk=object.id %}" target="_blank" class="btn btn-neutral btn-add">
<span class="icon fa fa-edit" aria-hidden="true"></span>&nbsp;&nbsp;{% trans "Edit" %}
</a>
{% include 'comments/comments_list.html' %}
{% include 'comments/add_auth_comments.html' %}
</div>
<div class="clearfix"></div>
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
{% block content %} {% block content %}
<div class="layout-full layout"> <div class="layout-full layout">
<div class="card course-form"> <div class="card course-form">
<div class="course-information">
<h4 class="hd-4">{% trans "Course Run Form" %}</h4> <h4 class="hd-4">{% trans "Course Run Form" %}</h4>
<div class="status-information"> <div class="status-information">
{% if object.id %} {% if object.id %}
<div class="info-item"> <div class="info-item">
...@@ -16,11 +16,7 @@ ...@@ -16,11 +16,7 @@
<span class="heading">{% trans "Status" %}:</span> <span class="heading">{% trans "Status" %}:</span>
<span>{{ workflow_state }}</span> <span>{{ workflow_state }}</span>
</span> </span>
<span class="item">
<a href="{% url 'publisher:publisher_seats_new' %}" target="_blank" class="btn btn-neutral btn-add">
{% trans "Add Seat" %}
</a>
</span>
</div> </div>
{% endif %} {% endif %}
</div> </div>
...@@ -33,6 +29,18 @@ ...@@ -33,6 +29,18 @@
<button class="btn-brand btn-base" type="submit">{% trans "Save" %}</button> <button class="btn-brand btn-base" type="submit">{% trans "Save" %}</button>
</form> </form>
</div> </div>
<div class="comment-container">
<span class="item">
{% if object.id %}
<a href="{% url 'publisher:publisher_seats_new' %}" target="_blank" class="btn btn-neutral btn-add">
{% trans "Add Seat" %}
</a>
{% endif %}
</span>
{% include 'comments/comments_list.html' %}
{% include 'comments/add_auth_comments.html' %}
</div>
</div>
</div> </div>
{% endblock content %} {% endblock content %}
...@@ -8,8 +8,10 @@ ...@@ -8,8 +8,10 @@
{% block content %} {% block content %}
<div class="layout-full layout"> <div class="layout-full layout">
<div class="card course-form"> <div class="card course-form">
<div class="course-information">
<h4 class="hd-4">{% trans "Seat Form" %}</h4> <h4 class="hd-4">{% trans "Seat Form" %}</h4>
<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 %}
{% include "publisher/form_field.html" %} {% include "publisher/form_field.html" %}
...@@ -18,6 +20,11 @@ ...@@ -18,6 +20,11 @@
<button class="btn-brand btn-base" type="submit">{% trans "Save" %}</button> <button class="btn-brand btn-base" type="submit">{% trans "Save" %}</button>
</form> </form>
</div> </div>
<div class="comment-container">
{% include 'comments/comments_list.html' %}
{% include 'comments/add_auth_comments.html' %}
</div>
</div>
</div> </div>
{% endblock content %} {% endblock content %}
......
...@@ -35,6 +35,12 @@ urlpatterns = auth_urlpatterns + [ ...@@ -35,6 +35,12 @@ urlpatterns = auth_urlpatterns + [
url(r'^health/$', core_views.health, name='health'), url(r'^health/$', core_views.health, name='health'),
url('^$', QueryPreviewView.as_view()), url('^$', QueryPreviewView.as_view()),
url(r'^publisher/', include('course_discovery.apps.publisher.urls', namespace='publisher')), url(r'^publisher/', include('course_discovery.apps.publisher.urls', namespace='publisher')),
url(
r'^publisher/comments/', include(
'course_discovery.apps.publisher_comments.urls', namespace='publisher_comments'
)
),
url(r'^comments/', include('django_comments.urls')),
] ]
if settings.DEBUG and os.environ.get('ENABLE_DJANGO_TOOLBAR', False): # pragma: no cover if settings.DEBUG and os.environ.get('ENABLE_DJANGO_TOOLBAR', False): # pragma: no cover
......
...@@ -32,3 +32,4 @@ pillow==3.3.0 ...@@ -32,3 +32,4 @@ pillow==3.3.0
pycountry==1.20 pycountry==1.20
python-dateutil==2.5.3 python-dateutil==2.5.3
pytz==2016.4 pytz==2016.4
django-contrib-comments==1.7.2
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